0)) {
$im=$re*$im*2+$imc;
$re=$re2-$im2+$rec;
$re2=$re*$re;
$im2=$im*$im;
$color=$color-1;
}
if ( $color==0 ) {
print "_";
} else {
print "#";
}
}
print " ";
flush();
}
}
/****/
function mandel2() {
$b = " .:,;!/>)|&IH%*#";
//float r, i, z, Z, t, c, C;
for ($y=30; printf("\n"), $C = $y*0.1 - 1.5, $y--;){
for ($x=0; $c = $x*0.04 - 2, $z=0, $Z=0, $x++ < 75;){
for ($r=$c, $i=$C, $k=0; $t = $z*$z - $Z*$Z + $r, $Z = 2*$z*$Z + $i, $z=$t, $k<5000; $k++)
if ($z*$z + $Z*$Z > 500000) break;
echo $b[$k%16];
}
}
}
/****/
function Ack($m, $n){
if($m == 0) return $n+1;
if($n == 0) return Ack($m-1, 1);
return Ack($m - 1, Ack($m, ($n - 1)));
}
function ackermann($n) {
$r = Ack(3,$n);
print "Ack(3,$n): $r\n";
}
/****/
function ary($n) {
for ($i=0; $i<$n; $i++) {
$X[$i] = $i;
}
for ($i=$n-1; $i>=0; $i--) {
$Y[$i] = $X[$i];
}
$last = $n-1;
print "$Y[$last]\n";
}
/****/
function ary2($n) {
for ($i=0; $i<$n;) {
$X[$i] = $i; ++$i;
$X[$i] = $i; ++$i;
$X[$i] = $i; ++$i;
$X[$i] = $i; ++$i;
$X[$i] = $i; ++$i;
$X[$i] = $i; ++$i;
$X[$i] = $i; ++$i;
$X[$i] = $i; ++$i;
$X[$i] = $i; ++$i;
$X[$i] = $i; ++$i;
}
for ($i=$n-1; $i>=0;) {
$Y[$i] = $X[$i]; --$i;
$Y[$i] = $X[$i]; --$i;
$Y[$i] = $X[$i]; --$i;
$Y[$i] = $X[$i]; --$i;
$Y[$i] = $X[$i]; --$i;
$Y[$i] = $X[$i]; --$i;
$Y[$i] = $X[$i]; --$i;
$Y[$i] = $X[$i]; --$i;
$Y[$i] = $X[$i]; --$i;
$Y[$i] = $X[$i]; --$i;
}
$last = $n-1;
print "$Y[$last]\n";
}
/****/
function ary3($n) {
for ($i=0; $i<$n; $i++) {
$X[$i] = $i + 1;
$Y[$i] = 0;
}
for ($k=0; $k<1000; $k++) {
for ($i=$n-1; $i>=0; $i--) {
$Y[$i] += $X[$i];
}
}
$last = $n-1;
print "$Y[0] $Y[$last]\n";
}
/****/
function fibo_r($n){
return(($n < 2) ? 1 : fibo_r($n - 2) + fibo_r($n - 1));
}
function fibo($n) {
$r = fibo_r($n);
print "$r\n";
}
/****/
function hash1($n) {
for ($i = 1; $i <= $n; $i++) {
$X[dechex($i)] = $i;
}
$c = 0;
for ($i = $n; $i > 0; $i--) {
if ($X[dechex($i)]) { $c++; }
}
print "$c\n";
}
/****/
function hash2($n) {
for ($i = 0; $i < $n; $i++) {
$hash1["foo_$i"] = $i;
$hash2["foo_$i"] = 0;
}
for ($i = $n; $i > 0; $i--) {
foreach($hash1 as $key => $value) $hash2[$key] += $value;
}
$first = "foo_0";
$last = "foo_".($n-1);
print "$hash1[$first] $hash1[$last] $hash2[$first] $hash2[$last]\n";
}
/****/
function gen_random ($n) {
global $LAST;
return( ($n * ($LAST = ($LAST * IA + IC) % IM)) / IM );
}
function heapsort_r($n, &$ra) {
$l = ($n >> 1) + 1;
$ir = $n;
while (1) {
if ($l > 1) {
$rra = $ra[--$l];
} else {
$rra = $ra[$ir];
$ra[$ir] = $ra[1];
if (--$ir == 1) {
$ra[1] = $rra;
return;
}
}
$i = $l;
$j = $l << 1;
while ($j <= $ir) {
if (($j < $ir) && ($ra[$j] < $ra[$j+1])) {
$j++;
}
if ($rra < $ra[$j]) {
$ra[$i] = $ra[$j];
$j += ($i = $j);
} else {
$j = $ir + 1;
}
}
$ra[$i] = $rra;
}
}
function heapsort($N) {
global $LAST;
define("IM", 139968);
define("IA", 3877);
define("IC", 29573);
$LAST = 42;
for ($i=1; $i<=$N; $i++) {
$ary[$i] = gen_random(1);
}
heapsort_r($N, $ary);
printf("%.10f\n", $ary[$N]);
}
/****/
function mkmatrix ($rows, $cols) {
$count = 1;
$mx = array();
for ($i=0; $i<$rows; $i++) {
for ($j=0; $j<$cols; $j++) {
$mx[$i][$j] = $count++;
}
}
return($mx);
}
function mmult ($rows, $cols, $m1, $m2) {
$m3 = array();
for ($i=0; $i<$rows; $i++) {
for ($j=0; $j<$cols; $j++) {
$x = 0;
for ($k=0; $k<$cols; $k++) {
$x += $m1[$i][$k] * $m2[$k][$j];
}
$m3[$i][$j] = $x;
}
}
return($m3);
}
function matrix($n) {
$SIZE = 30;
$m1 = mkmatrix($SIZE, $SIZE);
$m2 = mkmatrix($SIZE, $SIZE);
while ($n--) {
$mm = mmult($SIZE, $SIZE, $m1, $m2);
}
print "{$mm[0][0]} {$mm[2][3]} {$mm[3][2]} {$mm[4][4]}\n";
}
/****/
function nestedloop($n) {
$x = 0;
for ($a=0; $a<$n; $a++)
for ($b=0; $b<$n; $b++)
for ($c=0; $c<$n; $c++)
for ($d=0; $d<$n; $d++)
for ($e=0; $e<$n; $e++)
for ($f=0; $f<$n; $f++)
$x++;
print "$x\n";
}
/****/
function sieve($n) {
$count = 0;
while ($n-- > 0) {
$count = 0;
$flags = range (0,8192);
for ($i=2; $i<8193; $i++) {
if ($flags[$i] > 0) {
for ($k=$i+$i; $k <= 8192; $k+=$i) {
$flags[$k] = 0;
}
$count++;
}
}
}
print "Count: $count\n";
}
/****/
function strcat($n) {
$str = "";
while ($n-- > 0) {
$str .= "hello\n";
}
$len = strlen($str);
print "$len\n";
}
/*****/
function gethrtime()
{
$hrtime = hrtime();
return (($hrtime[0]*1000000000 + $hrtime[1]) / 1000000000);
}
function start_test()
{
ob_start();
return gethrtime();
}
function end_test($start, $name)
{
global $total;
$end = gethrtime();
ob_end_clean();
$total += $end-$start;
$num = number_format($end-$start,3);
$pad = str_repeat(" ", 24-strlen($name)-strlen($num));
echo $name.$pad.$num."\n";
ob_start();
return gethrtime();
}
function total()
{
global $total;
$pad = str_repeat("-", 24);
echo $pad."\n";
$num = number_format($total,3);
$pad = str_repeat(" ", 24-strlen("Total")-strlen($num));
echo "Total".$pad.$num."\n";
}
$t0 = $t = start_test();
simple();
$t = end_test($t, "simple");
simplecall();
$t = end_test($t, "simplecall");
simpleucall();
$t = end_test($t, "simpleucall");
simpleudcall();
$t = end_test($t, "simpleudcall");
mandel();
$t = end_test($t, "mandel");
mandel2();
$t = end_test($t, "mandel2");
ackermann(7);
$t = end_test($t, "ackermann(7)");
ary(50000);
$t = end_test($t, "ary(50000)");
ary2(50000);
$t = end_test($t, "ary2(50000)");
ary3(2000);
$t = end_test($t, "ary3(2000)");
fibo(30);
$t = end_test($t, "fibo(30)");
hash1(50000);
$t = end_test($t, "hash1(50000)");
hash2(500);
$t = end_test($t, "hash2(500)");
heapsort(20000);
$t = end_test($t, "heapsort(20000)");
matrix(20);
$t = end_test($t, "matrix(20)");
nestedloop(12);
$t = end_test($t, "nestedloop(12)");
sieve(30);
$t = end_test($t, "sieve(30)");
strcat(200000);
$t = end_test($t, "strcat(200000)");
total();
?>
# Drupal editor configuration normalization
# @see http://editorconfig.org/
# This is the top-most .editorconfig file; do not search in parent directories.
root = true
# All files.
[*]
end_of_line = LF
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
# Ignore configuration files that may contain sensitive information.
sites/*/settings*.php
# Ignore paths that contain user-generated content.
sites/*/files
sites/*/private
#
# Apache/PHP/Drupal settings:
#
# Protect files and directories from prying eyes.
Require all denied
Order allow,deny
# Don't show directory listings for URLs which map to a directory.
Options -Indexes
# Follow symbolic links in this directory.
Options +FollowSymLinks
# Make Drupal handle any 404 errors.
ErrorDocument 404 /index.php
# Set the default handler.
DirectoryIndex index.php index.html index.htm
# Override PHP settings that cannot be changed at runtime. See
# sites/default/default.settings.php and drupal_environment_initialize() in
# includes/bootstrap.inc for settings that can be changed at runtime.
# PHP 5, Apache 1 and 2.
php_flag magic_quotes_gpc off
php_flag magic_quotes_sybase off
php_flag register_globals off
php_flag session.auto_start off
php_value mbstring.http_input pass
php_value mbstring.http_output pass
php_flag mbstring.encoding_translation off
# Requires mod_expires to be enabled.
# Enable expirations.
ExpiresActive On
# Cache all files for 2 weeks after access (A).
ExpiresDefault A1209600
# Do not allow PHP scripts to be cached unless they explicitly send cache
# headers themselves. Otherwise all scripts would have to overwrite the
# headers set by mod_expires if they want another caching behavior. This may
# fail if an error occurs early in the bootstrap process, and it may cause
# problems if a non-Drupal PHP file is installed in a subdirectory.
ExpiresActive Off
# Various rewrite rules.
RewriteEngine on
# Set "protossl" to "s" if we were accessed via https://. This is used later
# if you enable "www." stripping or enforcement, in order to ensure that
# you don't bounce between http and https.
RewriteRule ^ - [E=protossl]
RewriteCond %{HTTPS} on
RewriteRule ^ - [E=protossl:s]
# Make sure Authorization HTTP header is available to PHP
# even when running as CGI or FastCGI.
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Block access to "hidden" directories whose names begin with a period. This
# includes directories used by version control systems such as Subversion or
# Git to store control files. Files whose names begin with a period, as well
# as the control files used by CVS, are protected by the FilesMatch directive
# above.
#
# NOTE: This only works when mod_rewrite is loaded. Without mod_rewrite, it is
# not possible to block access to entire directories from .htaccess, because
# is not allowed here.
#
# If you do not have mod_rewrite installed, you should remove these
# directories from your webroot or otherwise protect them from being
# downloaded.
RewriteRule "/\.|^\.(?!well-known/)" - [F]
# If your site can be accessed both with and without the 'www.' prefix, you
# can use one of the following settings to redirect users to your preferred
# URL, either WITH or WITHOUT the 'www.' prefix. Choose ONLY one option:
#
# To redirect all users to access the site WITH the 'www.' prefix,
# (http://example.com/... will be redirected to http://www.example.com/...)
# uncomment the following:
# RewriteCond %{HTTP_HOST} .
# RewriteCond %{HTTP_HOST} !^www\. [NC]
# RewriteRule ^ http%{ENV:protossl}://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
#
# To redirect all users to access the site WITHOUT the 'www.' prefix,
# (http://www.example.com/... will be redirected to http://example.com/...)
# uncomment the following:
# RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
# RewriteRule ^ http%{ENV:protossl}://%1%{REQUEST_URI} [L,R=301]
# Modify the RewriteBase if you are using Drupal in a subdirectory or in a
# VirtualDocumentRoot and the rewrite rules are not working properly.
# For example if your site is at http://example.com/drupal uncomment and
# modify the following line:
# RewriteBase /drupal
#
# If your site is running in a VirtualDocumentRoot at http://example.com/,
# uncomment the following line:
# RewriteBase /
# Pass all requests not referring directly to files in the filesystem to
# index.php. Clean URLs are handled in drupal_environment_initialize().
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !=/favicon.ico
RewriteRule ^ index.php [L]
# Rules to correctly serve gzip compressed CSS and JS files.
# Requires both mod_rewrite and mod_headers to be enabled.
# Serve gzip compressed CSS files if they exist and the client accepts gzip.
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond %{REQUEST_FILENAME}\.gz -s
RewriteRule ^(.*)\.css $1\.css\.gz [QSA]
# Serve gzip compressed JS files if they exist and the client accepts gzip.
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond %{REQUEST_FILENAME}\.gz -s
RewriteRule ^(.*)\.js $1\.js\.gz [QSA]
# Serve correct content types, and prevent double compression.
RewriteRule \.css\.gz$ - [T=text/css,E=no-gzip:1,E=no-brotli:1]
RewriteRule \.js\.gz$ - [T=text/javascript,E=no-gzip:1,E=no-brotli:1]
# Serve correct encoding type.
Header set Content-Encoding gzip
# Force proxies to cache gzipped & non-gzipped css/js files separately.
Header append Vary Accept-Encoding
# Various header fixes.
# Disable content sniffing, since it's an attack vector.
Header always set X-Content-Type-Options nosniff
# Disable Proxy header, since it's an attack vector.
RequestHeader unset Proxy
Drupal 7.95, 2023-03-15
-----------------------
- Fixed security issues:
- SA-CORE-2023-004
Drupal 7.94, 2022-12-14
-----------------------
- Hotfix for book.module and Select query properties
Drupal 7.93, 2022-12-07
-----------------------
- Improved support for PHP 8.2
- Minimum PHP version changed to PHP 5.3
- Various security hardenings
- Various bug fixes, optimizations and improvements
Drupal 7.92, 2022-09-07
-----------------------
- Improved support for PHP 8.1
- Various security hardenings
- Various bug fixes, optimizations and improvements
Drupal 7.91, 2022-07-20
-----------------------
- Fixed security issues:
- SA-CORE-2022-012
Drupal 7.90, 2022-06-01
-----------------------
- Improved support for PHP 8.1
- Improved support for PostgreSQL
- Various bug fixes, optimizations and improvements
Drupal 7.89, 2022-03-02
-----------------------
- Bug fixes for PHP 8.1
- Fix tests for PostgreSQL
Drupal 7.88, 2022-02-15
-----------------------
- Fixed security issues:
- SA-CORE-2022-003
Drupal 7.87, 2022-01-19
-----------------------
- Fix regression caused by jQuery UI position() backport
Drupal 7.86, 2022-01-18
-----------------------
- Fixed security issues:
- SA-CORE-2022-001
- SA-CORE-2022-002
Drupal 7.85, 2022-01-12
-----------------------
- Fix session cookies for sites with different base_urls but a shared domain
Drupal 7.84, 2021-12-13
-----------------------
- Hotfix for session cookie domain on www subdomains
Drupal 7.83, 2021-12-01
-----------------------
- Initial support for PHP 8.1
- The has_js cookie has been removed (but can be re-enabled)
- The leading www. is no longer stripped from cookie domain by default
- The user entity now has a "changed" property
- Introduced a skip_permissions_hardening setting
- Changes to the password reset process to avoid email and username enumeration
- Various bug fixes, optimizations and improvements
Drupal 7.82, 2021-07-21
-----------------------
- Fixed security issues:
- SA-CORE-2021-004
Drupal 7.81, 2021-06-02
-----------------------
- Block Google FLoC by default
- Testing and accessibility enhancements
- Various bug fixes, optimizations and improvements
Drupal 7.80, 2021-04-20
-----------------------
- Fixed security issues:
- SA-CORE-2021-002
Drupal 7.79, 2021-04-07
-----------------------
- Initial support for PHP 8
- Support for SameSite cookie attribute
- Avoid write for unchanged fields (opt-in)
Drupal 7.78, 2021-01-19
-----------------------
- Fixed security issues:
- SA-CORE-2021-001
Drupal 7.77, 2020-12-03
-----------------------
- Hotfix for schema.prefixed tables
Drupal 7.76, 2020-12-02
-----------------------
- Support for MySQL 8
- Core tests pass in SQLite
- Better user flood control logging
Drupal 7.75, 2020-11-26
-----------------------
- Fixed security issues:
- SA-CORE-2020-013
Drupal 7.74, 2020-11-17
-----------------------
- Fixed security issues:
- SA-CORE-2020-012
Drupal 7.73, 2020-09-16
-----------------------
- Fixed security issues:
- SA-CORE-2020-007
Drupal 7.72, 2020-06-17
-----------------------
- Fixed security issues:
- SA-CORE-2020-004
Drupal 7.71, 2020-06-03
-----------------------
- Fix for jQuery Form bug in Chromium-based browsers
- Full support for PHP 7.4
Drupal 7.70, 2020-05-19
-----------------------
- Fixed security issues:
- SA-CORE-2020-002
- SA-CORE-2020-003
Drupal 7.69, 2019-12-18
-----------------------
- Fixed security issues:
- SA-CORE-2019-012
Drupal 7.68, 2019-12-04
-----------------------
- Fixed: Hide toolbar when printing
- Fixed: Settings returned via ajax are not run through hook_js_alter()
- Fixed: Use drupal_http_build_query() in drupal_http_request()
- Fixed: DrupalRequestSanitizer not found fatal error when bootstrap phase order is changed
- Fixed: Block web.config in .htaccess (and vice-versa)
- Fixed: Create "scripts" element to align rendering workflow to how "styles" are handled
- PHP 7.3: Fixed 'Cannot change session id when session is active'
- PHP 7.1: Fixed 'A non-numeric value encountered in theme_pager()'
- PHP 7.x: Fixed file.inc generated .htaccess does not cover PHP 7
- PHP 5.3: Fixed check_plain() 'Invalid multibyte sequence in argument' test failures
- Fixed: Allow passing data as array to drupal_http_request()
- Fixed: Skip module_invoke/module_hook in calling hook_watchdog (excessive function_exist)
- Fixed: HTTP status 200 returned for 'Additional uncaught exception thrown while handling exception'
- Fixed: theme_table() should take an optional footer variable and produce
- Fixed: 'uasort() expects parameter 1 to be array, null given in node_view_multiple()'
- [regression] Fix default.settings.php permission
Drupal 7.67, 2019-05-08
-----------------------
- Fixed security issues:
- SA-CORE-2019-007
Drupal 7.66, 2019-04-17
-----------------------
- Fixed security issues:
- SA-CORE-2019-006
Drupal 7.65, 2019-03-20
-----------------------
- Fixed security issues:
- SA-CORE-2019-004
Drupal 7.64, 2019-02-06
-----------------------
- [regression] Unset the 'host' header in drupal_http_request() during redirect
- Fixed: 7.x does not have Phar protection and Phar tests are failing on Drupal 7
- Fixed: Notice: Undefined index: display_field in file_field_widget_value() (line 582 of /module/file/file.field.inc)
- Performance improvement: Registry rebuild should not parse the same file twice in the same request
- Fixed _registry_update() to clear caches after transaction is committed
Drupal 7.63, 2019-01-16
-----------------------
- Fixed a fatal error for some Drush users introduced by SA-CORE-2019-002.
Drupal 7.62, 2019-01-15
-----------------------
- Fixed security issues:
- SA-CORE-2019-001
- SA-CORE-2019-002
Drupal 7.61, 2018-11-07
-----------------------
- File upload validation functions and hook_file_validate() implementations are
now always passed the correct file URI.
- The default form cache expiration of 6 hours is now configurable (API
addition: https://www.drupal.org/node/2857751).
- Allowed callers of drupal_http_request() to optionally specify an explicit
Host header.
- Allowed the + character to appear in usernames.
- PHP 7.2: Fixed Archive_Tar incompatibility.
- PHP 7.2: Removed deprecated function each().
- PHP 7.2: Avoid count() calls on uncountable variables.
- PHP 7.2: Removed deprecated create_function() call.
- PHP 7.2: Make sure variables are arrays in theme_links().
- Fixed theme-settings.php not being loaded on cached forms
- Fixed problem with IE11 & Chrome(PointerEvents enabled) & some Firefox scroll to the top of the page after dragging the bottom item with jquery 1.5 <-> 1.11
Drupal 7.60, 2018-10-18
------------------------
- Fixed security issues. See SA-CORE-2018-006.
Drupal 7.59, 2018-04-25
-----------------------
- Fixed security issues (remote code execution). See SA-CORE-2018-004.
Drupal 7.58, 2018-03-28
-----------------------
- Fixed security issues (remote code execution). See SA-CORE-2018-002.
Drupal 7.57, 2018-02-21
-----------------------
- Fixed security issues (multiple vulnerabilities). See SA-CORE-2018-001.
Drupal 7.56, 2017-06-21
-----------------------
- Fixed security issues (access bypass). See SA-CORE-2017-003.
Drupal 7.55, 2017-06-07
-----------------------
- Fixed incompatibility with PHP versions 7.0.19 and 7.1.5 due to duplicate
DATE_RFC7231 definition.
- Made Drupal core pass all automated tests on PHP 7.1.
- Allowed services such as Let's Encrypt to work with Drupal on Apache, by
making Drupal's .htaccess file allow access to the .well-known directory
defined by RFC 5785.
- Made new Drupal sites work correctly on Apache 2.4 when the mod_access_compat
Apache module is disabled.
- Fixed Drupal's URL-generating functions to always encode '[' and ']' so that
the URLs will pass HTML5 validation.
- Various additional bug fixes.
- Various API documentation improvements.
- Additional automated test coverage.
Drupal 7.54, 2017-02-01
-----------------------
- Modules are now able to define theme engines (API addition:
https://www.drupal.org/node/2826480).
- Logging of searches can now be disabled (new option in the administrative
interface).
- Added menu tree render structure to (pre-)process hooks for theme_menu_tree()
(API addition: https://www.drupal.org/node/2827134).
- Added new function for determining whether an HTTPS request is being served
(API addition: https://www.drupal.org/node/2824590).
- Fixed incorrect default value for short and medium date formats on the date
type configuration page.
- File validation error message is now removed after subsequent upload of valid
file.
- Numerous bug fixes.
- Numerous API documentation improvements.
- Additional performance improvements.
- Additional automated test coverage.
Drupal 7.53, 2016-12-07
-----------------------
- Fixed drag and drop support on newer Chrome/IE 11+ versions after 7.51 update
when jQuery is updated to 1.7-1.11.0.
Drupal 7.52, 2016-11-16
-----------------------
- Fixed security issues (multiple vulnerabilities). See SA-CORE-2016-005.
Drupal 7.51, 2016-10-05
-----------------------
- The Update module now also checks for updates to a disabled theme that is
used as an admin theme.
- Exceptions thrown in dblog_watchdog() are now caught and ignored.
- Clarified the warning that appears when modules are missing or have moved.
- Log messages are now XSS filtered on display.
- Draggable tables now work on touch screen devices.
- Added a setting for allowing double underscores in CSS identifiers
(https://www.drupal.org/node/2810369).
- If a user navigates away from a page while an Ajax request is running they
will no longer get an error message saying "An Ajax HTTP request terminated
abnormally".
- The system_region_list() API function now takes an optional third parameter
which allows region name translations to be skipped when they are not needed
(API addition: https://www.drupal.org/node/2810365).
- Numerous performance improvements.
- Numerous bug fixes.
- Numerous API documentation improvements.
- Additional automated test coverage.
Drupal 7.50, 2016-07-07
-----------------------
- Added a new "administer fields" permission for trusted users, which is
required in addition to other permissions to use the field UI
(https://www.drupal.org/node/2483307).
- Added clickjacking protection to Drupal core by setting the X-Frame-Options
header to SAMEORIGIN by default (https://www.drupal.org/node/2735873).
- Added support for full UTF-8 (emojis, Asian symbols, mathematical symbols) on
MySQL and other database drivers when the site and database are configured to
allow it (https://www.drupal.org/node/2761183).
- Improved performance by avoiding a re-scan of directories when a file is
missing; instead, trigger a PHP warning (minor API change:
https://www.drupal.org/node/2581445).
- Made it possible to use any PHP callable in Ajax form callbacks, form API
form-building functions, and form API wrapper callbacks (API addition:
https://www.drupal.org/node/2761169).
- Fixed that following a password reset link while logged in leaves users unable
to change their password (minor user interface change:
https://www.drupal.org/node/2759023).
- Implemented various fixes for automated test failures on PHP 5.4+ and PHP 7.
Drupal core automated tests now pass in these environments.
- Improved support for PHP 7 by fixing various problems.
- Fixed various bugs with PHP 5.5+ imagerotate(), including when incorrect
color indices are passed in.
- Fixed a regression introduced in Drupal 7.43 that allowed files uploaded by
anonymous users to be lost after form validation errors, and that also caused
regressions with certain contributed modules.
- Fixed a regression introduced in Drupal 7.36 which caused the default value
of hidden textarea fields to be ignored.
- Fixed robots.txt to allow search engines to access CSS, JavaScript and image
files.
- Changed wording on the Update Manager settings page to clarify that the
option to check for disabled module updates also applies to uninstalled
modules (administrative-facing translatable string change).
- Changed the help text when editing menu links and configuring URL redirect
actions so that it does not reference "Drupal" or the drupal.org website
(administrative-facing translatable string change).
- Fixed the locale safety check that is used to ensure that translations are
safe to allow for tokens in the href/src attributes of translated strings.
- Fixed that URL generation only works on port 80 when using domain based
language negotation.
- Made method="get" forms work inside the administrative overlay. The fix adds
a new hidden field to these forms when they appear inside the overlay (minor
data structure change).
- Increased maxlength of menu link title input fields in the node form and
menu link form from 128 to 255 characters.
- Removed meaningless post-check=0 and pre-check=0 cache control headers from
Drupal HTTP responses.
- Added a .editorconfig file to auto-configure editors that support it.
- Added --directory option to run-tests.sh for easier test discovery of all
tests within a project.
- Made run-tests.sh exit with a failure code when there are test fails or
problems running the script.
- Fixed that cookies from previous tests are still present when a new test
starts in DrupalWebTestCase.
- Improved performance of queries on the {authmap} database table.
- Fixed handling of missing files and functions inside the registry.
- Fixed Ajax handling for tableselect form elements that use checkboxes.
- Fixed a bug which caused ip_address() to return nothing when the client IP
address and proxy IP address are the same.
- Added a new option to format_xml_elements() to allow for already encoded
values.
- Changed the {history} table's node ID field to be an unsigned integer, to
match the same field in the {node} table and to prevent errors with very
large node IDs.
- Added an explicit page callback to the "admin/people/create" menu item in the
User module (minor data structure change). Previously this automatically
inherited the page callback from the parent "admin/people" menu item, which
broke contributed modules that override the "admin/people" page.
- Numerous small bug fixes.
- Numerous API documentation improvements.
- Additional automated test coverage.
Drupal 7.44, 2016-06-15
-----------------------
- Fixed security issues (privilege escalation). See SA-CORE-2016-002.
Drupal 7.43, 2016-02-24
-----------------------
- Fixed security issues (multiple vulnerabilities). See SA-CORE-2016-001.
Drupal 7.42, 2016-02-03
-----------------------
- Stopped invoking hook_flush_caches() on every cron run, since some modules
use that hook for expensive operations that are only needed on cache clears.
- Changed the default .htaccess and web.config to block Composer-related files.
- Added static caching to module_load_include() to improve performance.
- Fixed double-encoding bugs in select field widgets provided by the Options
module. The fix deprecates the 'strip_tags' property on option widgets and
replaces it with a new 'strip_tags_and_unescape' property (minor data
structure change).
- Improved MySQL 5.7 support by changing the MySQL database driver to stop
using the ANSI SQL mode alias, which has different meanings for different
MySQL versions.
- Fixed a regression introduced in Drupal 7.39 which prevented autocomplete
functionality from working on servers that are not configured to
automatically recognize index.php.
- Updated the Archive_Tar PEAR package to the latest 1.4.0 release, to fix bugs
with tar file handling on various operating systems.
- Fixed fatal errors on node preview when a field is displayed in the node
teaser but hidden in the full node view. The fix removes a
field_attach_prepare_view() call from the node_preview() function since it is
redundant with one in the node preview theme layer.
- Improved the description of the "Trimmed" format option on text fields
(translatable string change, and minor UI and data structure change).
- Numerous small bug fixes.
- Numerous API documentation improvements.
- Additional automated test coverage.
Drupal 7.41, 2015-10-21
-----------------------
- Fixed security issues (open redirect). See SA-CORE-2015-004.
Drupal 7.40, 2015-10-14
-----------------------
- Made Drupal's code for parsing .info files run much faster and use much less
memory.
- Prevented drupal_http_request() from returning an error when it receives a
201 through 206 HTTP status code.
- Added support for autoloading traits via the registry on sites running PHP
5.4 or higher.
- Allowed the user-picture.tpl.php theme template to have HTML classes besides
the default "user-picture" class printed in it (markup change).
- Fixed the URL text filter to convert e-mail addresses with plus signs into
mailto: links.
- Added alternate text to file icons displayed by the File module, to improve
accessibility (string change, and minor API addition to theme_file_icon()).
- Changed one-time login link failure messages to be displayed as errors or
warnings as appropriate, rather than as regular status messages (minor UI
change and data structure change).
- Changed the default settings.php configuration to exclude private files from
the "404_fast_paths" behavior.
- Changed the page that displays filter tips for a particular text format, for
example filter/tips/full_html, to return "page not found" or "access denied"
if the format does not exist or the user does not have access to it. This
change adds a new menu item to the Filter module's hook_menu() entry (minor
data structure change).
- Added a new hook, hook_block_cid_parts_alter(), to allow modules to alter the
cache keys used for caching a particular block.
- Made drupal_set_message() display and return messages when "0" is passed in
as the message to set.
- Fixed non-functional "Files displayed by default" setting on file fields.
- The "worker callback" provided in hook_cron_queue_info() and the "finished"
callback specified during batch processing can now be any PHP callable
instead of just functions.
- Prevented drupal_set_time_limit() from decreasing the time limit in the case
where the PHP maximum execution time is already unlimited.
- Changed the default thousand marker for numeric fields from a space ("1 000")
to nothing ("1000") (minor UI change: https://www.drupal.org/node/1388376).
- Prevented malformed theme .info files (without a "name" key) from causing
exceptions during menu rebuilds. If an .info file without a "name" key is
found in a module or theme directory, Drupal will now use the module or
theme's machine name as the display name instead.
- Made the format column in the {date_format_locale} database table
case-sensitive, to match the equivalent column in the {date_formats} table.
- Fixed a bug in the Statistics module that caused JavaScript files attached to
a node while it is being viewed to be omitted from the page.
- Added an optional 'project:' prefix that can be added to dependencies in a
module's .info file to indicate which project the dependency resides in (API
addition: https://www.drupal.org/node/2299747).
- Fixed various bugs that occurred after hooks were invoked early in the Drupal
bootstrap and that caused module_implements() and drupal_alter() to cache an
incomplete set of hook implementations for later use.
- Set the X-Content-Type-Options header to "nosniff" when possible, to prevent
certain web browsers from picking an unsafe MIME type.
- Prevented the database API from executing multiple queries at once on MySQL,
if the site's PHP version is new enough to do so. This is a secondary defense
against SQL injection (API change: https://www.drupal.org/node/2463973).
- Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused the upgrade
to fail when there were multiple file records pointing to the same file.
- Numerous small bug fixes.
- Numerous API documentation improvements.
- Additional automated test coverage.
Drupal 7.39, 2015-08-19
-----------------------
- Fixed security issues (multiple vulnerabilities). See SA-CORE-2015-003.
Drupal 7.38, 2015-06-17
-----------------------
- Fixed security issues (multiple vulnerabilities). See SA-CORE-2015-002.
Drupal 7.37, 2015-05-07
-----------------------
- Fixed a regression in Drupal 7.36 which caused certain kinds of content types
to become disabled if they were defined by a no-longer-enabled module.
- Removed a confusing description regarding automatic time zone detection from
the user account form (minor UI and data structure change).
- Allowed custom HTML tags with a dash in the name to pass through filter_xss()
when specified in the list of allowed tags.
- Allowed hook_field_schema() implementations to specify indexes for fields
based on a fixed-length column prefix (rather than the entire column), as was
already allowed in hook_schema() implementations.
- Fixed PDO exceptions on PostgreSQL when accessing invalid entity URLs.
- Added a sites/all/libraries folder to the codebase, with instructions for
using it.
- Added a description to the "Administer text formats and filters" permission
on the Permissions page (string change).
- Numerous small bug fixes.
- Numerous API documentation improvements.
- Additional automated test coverage.
Drupal 7.36, 2015-04-01
-----------------------
- Added a 'file_public_schema' variable which allows modules that define
publicly-accessible streams in hook_stream_wrappers() to bypass file download
access checks when processing managed file upload fields.
- Fixed a bug that caused database query tags not to be added to search-related
database queries under many circumstances, and which prevented the
corresponding hook_query_TAG_alter() implementations from being called.
- Fixed the "for" attribute on managed file upload field labels to improve
accessibility (minor markup change).
- Added a 'javascript_always_use_jquery' variable which can be set to FALSE by
sites that may not need jQuery loaded on all pages, and a 'requires_jquery'
option to drupal_add_js() which modules can set to FALSE when adding
JavaScript files that have no dependency on jQuery (API addition:
https://www.drupal.org/node/2462717).
- Fixed incorrect foreign keys in the User module's role_permission and
users_roles database tables.
- Changed permission descriptions throughout Drupal core to consistently link
to relevant administrative pages, regardless of whether the user viewing the
Permissions page can view the page being linked to (minor UI change).
- Fixed the drupal_add_region_content() function so that it actually adds
content to the page.
- Added an 'image_suppress_itok_output' variable to allow sites already using
the existing 'image_allow_insecure_derivatives' variable to also prevent
security tokens from appearing in image derivative URLs.
- Fixed double-escaping of theme names in the Block module administrative
interface (minor string change).
- Added basic support for Xdebug when running automated tests.
- Fixed a bug which caused previewing a node to remove elements from the node
being edited. With this fix, calling node_preview() will no longer modify the
passed-in node object (minor API change).
- Added a user_has_role() function to check whether a user has a particular
role (API addition: https://www.drupal.org/node/2462411).
- Fixed installation failures when an opcode cache is enabled.
- Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused private
files to be inaccessible.
- Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused user
pictures to be lost.
- Fixed missing language code in hook_field_attach_view_alter() when it is
invoked from field_view_field().
- Stopped sending ETag and Last-Modified headers for uncached page requests,
since they break caching for certain Varnish and Nginx configurations.
- Changed the Simpletest module to allow PSR-4 test classes to be used in
Drupal 7.
- Fixed a fatal error that occurred when using the Comment module's "Unpublish
comment containing keyword(s)" action.
- Changed the "lang" attribute on language links to "xml:lang" so it validates
as XHTML (minor markup change).
- Prevented the form API from allowing arrays to be submitted for various form
elements, such as textfields, textareas, and password fields (API change:
https://www.drupal.org/node/2462723).
- Fixed a bug in the Contact module which caused the global user object to have
the incorrect name and e-mail address during the remainder of the page
request after the contact form is submitted.
- Numerous small bug fixes.
- Numerous API documentation improvements.
- Additional automated test coverage.
Drupal 7.35, 2015-03-18
-----------------------
- Fixed security issues (multiple vulnerabilities). See SA-CORE-2015-001.
Drupal 7.34, 2014-11-19
-----------------------
- Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-006.
Drupal 7.33, 2014-11-07
-----------------------
- Began storing the file modification time of each module and theme in the
{system} database table so that contributed modules can use it to identify
recently changed modules and themes (minor data structure change to the
return value of system_get_info() and other related functions).
- Added a "Did you mean?" feature to the run-tests.sh script for running
automated tests from the command line, to help developers who are attempting
to run a particular test class or group.
- Changed the date format used in various HTTP headers output by Drupal core
from RFC 1123 format to RFC 7231 format.
- Added a "block_cache_bypass_node_grants" variable to allow sites which have
node access modules enabled to use the block cache if desired (API addition).
- Made image derivative generation HTTP requests return a 404 error (rather
than a 500 error) when the source image does not exist.
- Fixed a bug which caused user pictures to be removed from the user object
after saving, and resulted in data loss if the user account was subsequently
re-saved.
- Fixed a bug in which field_has_data() did not return TRUE for fields that
only had data in older entity revisions, leading to loss of the field's data
when the field configuration was edited.
- Fixed a bug which caused the Ajax progress throbber to appear misaligned in
many situatons (minor styling change).
- Prevented the Bartik theme from lower-casing the "Permalink" link on
comments, for improved multilingual support (minor UI change).
- Added a "preferred_menu_links" tag to the database query that is used by
menu_link_get_preferred() to find the preferred menu link for a given path,
to make it easier to alter.
- Increased the maximum allowed length of block titles to 255 characters
(database schema change to the {block} table).
- Removed the Field module's field_modules_uninstalled() function, since it did
not do anything when it was invoked.
- Added a "theme_hook_original" variable to templates and theme functions and
an optional sitewide theme debug mode, to provide contextual information in
the page's HTML to theme developers. The theme debug mode is based on the one
used with Twig in Drupal 8 and can be accessed by setting the "theme_debug"
variable to TRUE (API addition).
- Added an entity_view_mode_prepare() API function to allow entity-defining
modules to properly invoke hook_entity_view_mode_alter(), and used it
throughout Drupal core to fix bugs with the invocation of that hook (API
change: https://www.drupal.org/node/2369141).
- Security improvement: Made the database API's orderBy() method sanitize the
sort direction ("ASC" or "DESC") for queries built with db_select(), so that
calling code does not have to.
- Changed the RDF module to consistently output RDF metadata for nodes and
comments near where the node is rendered in the HTML (minor markup and data
structure change).
- Added an HTML class to RDFa metatags throughout Drupal to prevent them from
accidentally affecting the site appearance (minor markup change).
- Fixed a bug in the Unicode requirements check which prevented installing
Drupal on PHP 5.6.
- Fixed a bug which caused drupal_get_bootstrap_phase() to abort the bootstrap
when called early in the page request.
- Renamed the "Search result" view mode to "Search result highlighting input"
to better reflect how it is used (UI change).
- Improved database queries generated by EntityFieldQuery in the case where
delta or language condition groups are used, to reduce the number of INNER
JOINs (this is a minor data structure change affecting code which implements
hook_query_alter() on these queries).
- Removed special-case behavior for file uploads which allowed user #1 to
bypass maximum file size and user quota limits.
- Numerous small bug fixes.
- Numerous API documentation improvements.
- Additional automated test coverage.
Drupal 7.32, 2014-10-15
-----------------------
- Fixed security issues (SQL injection). See SA-CORE-2014-005.
Drupal 7.31, 2014-08-06
-----------------------
- Fixed security issues (denial of service). See SA-CORE-2014-004.
Drupal 7.30, 2014-07-24
-----------------------
- Fixed a regression introduced in Drupal 7.29 that caused files or images
attached to taxonomy terms to be deleted when the taxonomy term was edited
and resaved (and other related bugs with contributed and custom modules).
- Added a warning on the permissions page to recommend restricting access to
the "View site reports" permission to trusted administrators. See
DRUPAL-PSA-2014-002.
- Numerous API documentation improvements.
- Additional automated test coverage.
Drupal 7.29, 2014-07-16
-----------------------
- Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-003.
Drupal 7.28, 2014-05-08
-----------------------
- Fixed a regression introduced in Drupal 7.27 that caused JavaScript to break
on older browsers (such as Internet Explorer 8 and earlier) when Ajax was
used.
- Increased the timeout used by the Update Manager module when it fetches data
from drupal.org (from 5 seconds to 30 seconds), to work around a problem
which causes incomplete information about security updates to be presented to
site administrators. This fix may lead to a performance slowdown on the
Update Manager administration pages, when installing Drupal distributions,
and (for sites that use the automated cron feature) on occasional page loads
by site visitors.
- Fixed the behavior of the token system's "[node:summary]" token when the body
field does not have a manual summary.
- Changed the behavior of db_query_temporary() so that it works on SELECT
queries even when they have leading comments/whitespace. A side effect of
this fix is that db_query_temporary() will now fail with an error if it is
ever used on non-SELECT queries.
- Added a "node_admin_filter" tag to the database query used to build the list
of nodes on the content administration page, to make it easier to alter.
- Made the cron queue system log any exceptions that are thrown while an item
in the queue is being processed, rather than stopping the entire PHP request.
- Improved screen reader support by adding an aria-live HTML attribute to file
upload fields when there is an error uploading the file (minor markup
change).
- Made the pager on the Tracker module listing pages show the same number of
items as other pagers throughout Drupal core (minor UI change).
- Fixed a bug which caused caches not to be properly cleared when a file entity
was saved or deleted.
- Added several missing countries to the default list returned by
country_get_list() (string change).
- Replaced the term "weight" with "influence" in the content ranking settings
for search, and added help text for administrators (string change).
- Fixed untranslatable text strings in the administrative interface for the
"Crop" effect provided by the Image module (minor string change).
- Fixed a bug in the Taxonomy module update function introduced in Drupal 7.26
that caused memory and CPU problems on sites with very large numbers of
unpublished nodes.
- Numerous small bug fixes.
- Numerous API documentation improvements.
- Additional automated test coverage.
Drupal 7.27, 2014-04-16
-----------------------
- Fixed security issues (information disclosure). See SA-CORE-2014-002.
Drupal 7.26, 2014-01-15
-----------------------
- Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-001.
Drupal 7.25, 2014-01-02
-----------------------
- Fixed a bug in node_save() which prevented the saved node from being updated
in hook_node_insert() and other similar hooks.
- Added a meta tag to install.php to prevent it from being indexed by search
engines even when Drupal is installed in a subfolder (minor markup change).
- Fixed a bug in the database API that caused frequent deadlock errors when
running merge queries on some servers.
- Performance improvement: Prevented block rehashing from writing blocks to the
database on every cache clear and cron run when the blocks have not changed.
This fix results in an extra 'saved' key which is added and set to TRUE for
each block returned by _block_rehash() that actually is saved to the database
(data structure change).
- Added an optional 'skip on cron' parameter to hook_cron_queue_info() to allow
queues to avoid being automatically processed on cron runs (API addition).
- Fixed a bug which caused hook_block_view_MODULE_DELTA_alter() to never be
invoked if the block delta had a hyphen in it. To implement the hook when the
block delta has a hyphen, modules should now replace hyphens with underscores
when constructing the function name for the hook implementation.
- Fixed a bug which caused cached pages to sometimes be sent to the browser
with incorrect compression. The fix adds a new 'page_compressed' key to the
$cache->data array returned by drupal_page_get_cache() (minor data structure
change).
- Fixed broken tests on PHP 5.5.
- Made the File and Image modules more robust when saving entities that have
deleted files attached. The code in file_field_presave() will now remove the
record of the deleted file from the entity before saving (minor data
structure change).
- Standardized menu callback functions throughout Drupal core to return
MENU_NOT_FOUND and MENU_ACCESS_DENIED rather than printing their own "page
not found" or "access denied" pages (minor API change in the return value of
these functions under some circumstances).
- Fixed a bug in which caches were not properly cleared when a node was deleted
via the administrative interface.
- Changed the Bartik theme to render content contained in , and
similar tags in a larger font size, so it is easier to read.
- Fixed a bug in the Search module that caused exceptions to be thrown during
searches if the server was not configured to represent decimal points as a
period.
- Fixed a regression in the Image module that made image_style_url() not work
when a relative path (rather than a complete file URI) was passed to it.
- Added an optional feature to the Statistics module to allow node views to be
tracked by Ajax requests rather than during the server-side generation of the
page. This allows the node counter to work on sites that use external page
caches (string change and new administrative option:
https://drupal.org/node/2164069).
- Added a link to the drupal.org documentation page for cron to the Cron
settings page (string change).
- Added a 'drupal_anonymous_user_object' variable to allow the anonymous user
object returned by drupal_anonymous_user() to be overridden with a classed
object (API addition).
- Changed the database API to allow inserts based on a SELECT * query to work
correctly.
- Changed the database schema of the {file_managed} table to allow Drupal to
manage files larger than 4 GB.
- Changed the File module's hook_field_load() implementation to prevent file
entity properties which have the same name as file or image field properties
from overwriting the field properties (minor API change).
- Numerous small bug fixes.
- Numerous API documentation improvements.
- Additional automated test coverage.
Drupal 7.24, 2013-11-20
-----------------------
- Fixed security issues (multiple vulnerabilities), see SA-CORE-2013-003.
Drupal 7.23, 2013-08-07
-----------------------
- Fixed a fatal error on PostgreSQL databases when updating the Taxonomy module
from Drupal 6 to Drupal 7.
- Fixed the default ordering of CSS files for sites using right-to-left
languages, to consistently place the right-to-left override file immediately
after the CSS it is overriding (API change: https://drupal.org/node/2058463).
- Added a drupal_check_memory_limit() API function to allow the memory limit to
be checked consistently (API addition).
- Changed the default web.config file for IIS servers to allow favicon.ico
files which are present in the filesystem to be accessed.
- Fixed inconsistent support for the 'tel' protocol in Drupal's URL filtering
functions.
- Performance improvement: Allowed all hooks to be included in the
module_implements() cache, even those that are only invoked on HTTP POST
requests.
- Made the database system replace truncate queries with delete queries when
inside a transaction, to fix issues with PostgreSQL and other databases.
- Fixed a bug which caused nested contextual links to display improperly.
- Fixed a bug which prevented cached image derivatives from being flushed for
private files and other non-default file schemes.
- Fixed drupal_render() to always return an empty string when there is no
output, rather than sometimes returning NULL (minor API change).
- Added protection to cache_clear_all() to ensure that non-cache tables cannot
be truncated (API addition: a new isValidBin() method has been added to the
default database cache implementation).
- Changed the default .htaccess file to support HTTP authorization in CGI
environments.
- Changed the password reset form to pre-fill the username when requested via a
URL query parameter, and used this in the error message that appears after a
failed login attempt (minor data structure and behavior change).
- Fixed broken support for foreign keys in the field API.
- Fixed "No active batch" error when a user cancels their own account.
- Added a description to the "access content overview" permission on the
permissions page (string change).
- Added a drupal_array_diff_assoc_recursive() function to allow associative
arrays to be compared recursively (API addition).
- Added human-readable labels to image styles, in addition to the existing
machine-readable name (API change: https://drupal.org/node/2058503).
- Moved the drupal_get_hash_salt() function to bootstrap.inc and used it in
additional places in the code, for added security in the case where there is
no hash salt in settings.php.
- Fixed a regression in Drupal 7.22 that caused internal server errors for
sites running on very old Apache 1.x web servers.
- Numerous small bug fixes.
- Numerous API documentation improvements.
- Additional automated test coverage.
Drupal 7.22, 2013-04-03
-----------------------
- Allowed the drupal_http_request() function to be overridden so that
additional HTTP request capabilities can be added by contributed modules.
- Changed the Simpletest module to allow PSR-0 test classes to be used in
Drupal 7.
- Removed an unnecessary "Content-Disposition" header from private file
downloads; it prevented many private files from being viewed inline in a web
browser.
- Changed various field API functions to allow them to optionally act on a
single field within an entity (API addition: http://drupal.org/node/1825844).
- Fixed a bug which prevented Drupal's file transfer functionality from working
on some PHP 5.4 systems.
- Fixed incorrect log message when theme() is called for a theme hook that does
not exist (minor string change).
- Fixed Drupal's token-replacement system to allow spaces in the token value.
- Changed the default behavior after a user creates a node they do not have
access to view. The user will now be redirected to the front page rather than
an access denied page.
- Fixed a bug which prevented empty HTTP headers (such as "0") from being set.
(Minor behavior change: Callers of drupal_add_http_header() must now set
FALSE explicitly to prevent a header from being sent at all; this was already
indicated in the function's documentation.)
- Fixed OpenID errors when more than one module implements hook_openid(). The
behavior is now changed so that if more than one module tries to set the same
parameter, the last module's change takes effect.
- Fixed a serious documentation bug: The $name variable in the
taxonomy-term.tpl.php theme template was incorrectly documented as being
sanitized when in fact it is not.
- Fixed a bug which prevented Drupal 6 to Drupal 7 upgrades on sites which had
duplicate permission names in the User module's database tables.
- Added an empty "datatype" attribute to taxonomy term and username links to
make the RDFa markup upward compatible with RDFa 1.1 (minor markup addition).
- Fixed a bug which caused the denial-of-service protection added in Drupal
7.20 to break certain valid image URLs that had an extra slash in them.
- Fixed a bug with update queries in the SQLite database driver that prevented
Drupal from being installed with SQLite on PHP 5.4.
- Fixed enforced dependencies errors updating to recent versions of Drupal 7 on
certain non-MySQL databases.
- Refactored the Field module's caching behavior to obtain large improvements
in memory usage for sites with many fields and instances (API addition:
http://drupal.org/node/1915646).
- Fixed entity argument not being passed to implementations of
hook_file_download_access_alter(). The fix adds an additional context
parameter that can be passed when calling drupal_alter() for any hook (API
change: http://drupal.org/node/1882722).
- Fixed broken support for translatable comment fields (API change:
http://drupal.org/node/1874724).
- Added an assertThemeOutput() method to Simpletest to allow tests to check
that themed output matches an expected HTML string (API addition).
- Added a link to "Install another module" after a module has been successfully
downloaded via the Update Manager (UI change).
- Added an optional "exclusive" flag to installation profile .info files which
allows Drupal distributions to force a profile to be selected during
installation (API addition: http://drupal.org/node/1961012).
- Fixed a bug which caused the database API to not properly close database
connections.
- Added a link to the URL for running cron from outside the site to the Cron
settings page (UI change).
- Fixed a bug which prevented image styles from being reverted on PHP 5.4.
- Made the default .htaccess rules protocol sensitive to improve security for
sites which use HTTPS and redirect between "www" and non-"www" versions of
the page.
- Numerous small bug fixes.
- Numerous API documentation improvements.
- Additional automated test coverage.
Drupal 7.21, 2013-03-06
-----------------------
- Allowed sites using the 'image_allow_insecure_derivatives' variable to still
have partial protection from the security issues fixed in Drupal 7.20.
Drupal 7.20, 2013-02-20
-----------------------
- Fixed security issues (denial of service). See SA-CORE-2013-002.
Drupal 7.19, 2013-01-16
-----------------------
- Fixed security issues (multiple vulnerabilities). See SA-CORE-2013-001.
Drupal 7.18, 2012-12-19
-----------------------
- Fixed security issues (multiple vulnerabilities). See SA-CORE-2012-004.
Drupal 7.17, 2012-11-07
-----------------------
- Changed the default value of the '404_fast_html' variable to have a DOCTYPE
declaration.
- Made it possible to use associative arrays for the 'items' variable in
theme_item_list().
- Fixed a bug which prevented required form elements without a title from being
given an "error" class when the form fails validation.
- Prevented duplicate HTML IDs from appearing when two forms are displayed on
the same page and one of them is submitted with invalid data (minor markup
change).
- Fixed a bug which prevented Drupal 6 to Drupal 7 upgrades on sites which had
stale data in the Upload module's database tables.
- Fixed a bug in the States API which prevented certain types of form elements
from being disabled when requested.
- Allowed aggregator feed items with author names longer than 255 characters to
have a truncated version saved to the database (rather than causing a fatal
error).
- Allowed aggregator feed items to have URLs longer than 255 characters
(schema change which results in several columns in the Aggregator module's
database tables changing from VARCHAR to TEXT fields).
- Added hook_taxonomy_term_view() and standardized the process for rendering
taxonomy terms to invoke hook_entity_view() and otherwise make it consistent
with other entities (API change: http://drupal.org/node/1808870).
- Added hook_entity_view_mode_alter() to allow modules to change entity view
modes on display (API addition: http://drupal.org/node/1833086).
- Fixed a bug which made database queries running a "LIKE" query on blob fields
fail on PostgreSQL databases. This caused errors during the Drupal 6 to
Drupal 7 upgrade.
- Changed the hook_menu() entry for Drupal's rss.xml page to prevent extra path
components from being accidentally passed to the page callback function (data
structure change).
- Removed a non-standard "name" attribute from Drupal's default Content-Type
header for file downloads.
- Fixed the theme settings form to properly clean up submitted values in
$form_state['values'] when the form is submitted (data structure change).
- Fixed an inconsistency by removing the colon from the end of the label on
multi-valued form fields (minor string change).
- Added support for 'weight' in hook_field_widget_info() to allow modules to
control the order in which widgets are displayed in the Field UI.
- Updated various tables in the OpenID and Book modules to use the default
"empty table" text pattern (string change).
- Added proxy server support to drupal_http_request().
- Added "lang" attributes to language links, to better support screen readers.
- Fixed double occurrence of a "ul" HTML tag on secondary local tasks in the
Seven theme (markup change).
- Fixed bugs which caused taxonomy vocabulary and shortcut set titles to be
double-escaped. The fix replaces the taxonomy vocabulary overview page and
"Edit shortcuts" menu items' title callback entries in hook_menu() with new
functions that do not escape HTML characters (data structure change).
- Modified the Update manager module to allow drupal.org to collect usage
statistics for individual modules and themes, rather than only for entire
projects.
- Modified the node listing database query on Drupal's default front page to
add table aliases for better query altering (this is a data structure change
affecting code which implements hook_query_alter() on this query).
- Improved the translatability of the "Field type(s) in use" message on the
modules page (admin-facing string change).
- Fixed a regression which caused a "call to undefined function
drupal_find_base_themes()" fatal error under rare circumstances.
- Numerous API documentation improvements.
- Additional automated test coverage.
Drupal 7.16, 2012-10-17
-----------------------
- Fixed security issues (Arbitrary PHP code execution and information
disclosure). See SA-CORE-2012-003.
Drupal 7.15, 2012-08-01
-----------------------
- Introduced a 'user_password_reset_timeout' variable to allow the 24-hour
expiration for user password reset links to be adjusted (API addition).
- Fixed database errors due to ambiguous column names that occurred when
EntityFieldQuery was used in certain situations.
- Changed the drupal_array_get_nested_value() function to return a reference
(API addition).
- Changed the System module's hook_block_info() implementation to assign the
"Main page content" and "System help" blocks to appropriate regions by
default and prevent error messages on the block administration page (data
structure change).
- Fixed regression: Non-node entities couldn't be accessed with
EntityFieldQuery.
- Fixed regression: Optional radio buttons with an empty, non-NULL default
value led to an illegal choice error when none were selected.
- Reorganized the testing framework to split setUp() into specific sub-methods
and fix several regressions in the process.
- Fixed bug which made it impossible to search for strings that have not been
translated into a particular language.
- Renamed the "Field" column on the Manage Fields screen to "Field type", since
the former was confusing and inaccurate (UI change).
- Performance improvement: Removed needless call to system_rebuild_module_data()
in field_sync_field_status(), greatly speeding up bulk module enable/disable.
- Fixed bug which prevented notifications from being sent when core, module, and
theme updates are available.
- Fixed bug which prevented sub-themes from inheriting the default values of
theme settings defined by the base theme.
- Fixed bug which prevented the jQuery UI Datepicker from being localized.
- Made Ajax alert dialogs respect error reporting settings.
- Fixed bug which prevented image styles from being deleted on PHP 5.4.
- Fixed bug: Language detection by domain only worked on port 80.
- Fixed regression: The first plural index on a page was not calculated
correctly.
- Introduced generic entity language support. Entities may now declare their
language property in hook_entity_info(), and modules working with entities
may access the language using entity_language() (API change:
http://drupal.org/node/1626346).
- Added EntityFieldQuery support for taxonomy bundles.
- Fixed issue where field form structure was incomplete if field_access()
returned FALSE. Instead of being incomplete, the form structure now has
#access set to FALSE and field form validation is skipped (data structure
change: http://drupal.org/node/1663020).
- Fixed data loss issue due to field_has_data() returning inconsistent results.
The fix adds an optional DANGEROUS_ACCESS_CHECK_OPT_OUT tag to entity field
queries which field storage engines can respond to (API addition:
http://drupal.org/node/1597378).
- Fixed notice: Undefined index: default_image in image_field_prepare_view()
- Numerous API documentation improvements.
- Additional automated test coverage.
Drupal 7.14, 2012-05-02
-----------------------
- Fixed "integrity constraint" fatal errors when rebuilding registry.
- Fixed custom logo and favicon functionality referencing incorrect paths.
- Fixed DB Case Sensitivity: Allow BINARY attribute in MySQL.
- Split field_bundle_settings out per bundle.
- Improve UX for machine names for fields (UI change).
- Fixed User pictures are not removed properly.
- Fixed HTTPS sessions not working in all cases.
- Fixed Regression: Required radios throw illegal choice error when none
selected.
- Fixed allow autocompletion requests to include slashes.
- Eliminate $user->cache and {session}.cache in favor of
$_SESSION['cache_expiration'][$bin] (Performance).
- Fixed focus jumps to tab when pressing enter on a form element within tab.
- Fixed race condition in locale() - duplicates in {locales_source}.
- Fixed Missing "Default image" per field instance.
- Quit clobbering people's work when they click the filter tips link
- Form API #states: Fix conditionals to allow OR and XOR constructions.
- Fixed Focus jumps to tab when pressing enter on a form element within tab.
(Accessibility)
- Improved performance of node_access queries.
- Fixed Fieldsets inside vertical tabs have no title and can't be collapsed.
- Reduce size of cache_menu table (Performance).
- Fixed unnecessary aggregation of CSS/JS (Performance).
- Fixed taxonomy_autocomplete() produces SQL error for nonexistent field.
- Fixed HTML filter is not run first by default, despite default weight.
- Fixed Overlay does not work with prefixed URL paths.
- Better debug info for field errors (string change).
- Fixed Data corruption in comment IDs (results in broken threading on
PostgreSQL).
- Fixed machine name not editable if every character is replaced.
- Fixed user picture not appearing in comment preview (Markup change).
- Added optional vid argument for taxonomy_get_term_by_name().
- Fixed Invalid Unicode code range in PREG_CLASS_UNICODE_WORD_BOUNDARY fails
with PCRE 8.30.
- Fixed {trigger_assignments()}.hook has only 32 characters, is too short.
- Numerous fixes to run-tests.sh.
- Fixed Tests in profiles/[name]/modules cannot be run and cannot use a
different profile for running tests.
- Numerous JavaScript performance fixes.
- Numerous documentation fixes.
- Fixed All pager links have an 'active' CSS class.
- Numerous upgrade path fixes; notably:
- system_update_7061() fails on inserting files with same name but different
case.
- system_update_7061() converts filepaths too aggressively.
- Trigger upgrade path: Node triggers removed when upgrading to 7-x from 6.25.
Drupal 7.13, 2012-05-02
-----------------------
- Fixed security issues (Multiple vulnerabilities), see SA-CORE-2012-002.
Drupal 7.12, 2012-02-01
-----------------------
- Fixed bug preventing custom menus from receiving an active trail.
- Fixed hook_field_delete() no longer invoked during field_purge_data().
- Fixed bug causing entity info cache to not be cleared with the rest of caches.
- Fixed file_unmanaged_copy() fails with Drupal 7.7+ and safe_mode() or
open_basedir().
- Fixed Nested transactions throw exceptions when they got out of scope.
- Fixed bugs with the Return-Path when sending mail on both Windows and
non-Windows systems.
- Fixed bug with DrupalCacheArray property visibility preventing others from
extending it (API change: http://drupal.org/node/1422264).
- Fixed bug with handling of non-ASCII characters in file names (API change:
http://drupal.org/node/1424840).
- Reconciled field maximum length with database column size in image and
aggregator modules.
- Fixes to various core JavaScript files to allow for minification and
aggregation.
- Fixed Prevent tests from deleting main installation's tables when
parent::setUp() is not called.
- Fixed several Poll module bugs.
- Fixed several Shortcut module bugs.
- Added new hook_system_theme_info() to provide ability for contributed modules
to test theme functionality.
- Added ability to cancel mail sending from hook_mail_alter().
- Added support for configurable PDO connection options, enabling master-master
database replication.
- Numerous improvements to tests and test runner to pave the way for faster test
runs.
- Expanded test coverage.
- Numerous API documentation improvements.
- Numerous performance improvements, including token replacement and render
cache.
Drupal 7.11, 2012-02-01
-----------------------
- Fixed security issues (Multiple vulnerabilities), see SA-CORE-2012-001.
Drupal 7.10, 2011-12-05
-----------------------
- Fixed Content-Language HTTP header to not cause issues with Drush 5.x.
- Reduce memory usage of theme registry (performance).
- Fixed PECL upload progress bar for FileField
- Fixed running update.php doesn't always clear the cache.
- Fixed PDO exceptions on long titles.
- Fixed Overlay redirect does not include query string.
- Fixed D6 modules satisfy D7 module dependencies.
- Fixed the ordering of module hooks when using module_implements_alter().
- Fixed "floating" submit buttons during AJAX requests.
- Fixed timezone selected on install not propogating to admin account.
- Added msgctx context to JS translation functions, for feature parity with t().
- Profiles' .install files now available during hook_install_tasks().
- Added test coverage of 7.0 -> 7.x upgrade path.
- Numerous notice fixes.
- Numerous documentation improvements.
- Additional automated test coverage.
Drupal 7.9, 2011-10-26
----------------------
- Critical fixes to OpenID to spec violations that could allow for
impersonation in certain scenarios. Existing OpenID users should see
http://drupal.org/node/1120290#comment-5092796 for more information on
transitioning.
- Fixed files getting lost when adding multiple files to multiple file fields
at the same time.
- Improved usability of the clean URL test screens.
- Restored height/width attributes on images run through the theme system.
- Fixed usability bug with first password field being pre-filled by certain
browser plugins.
- Fixed file_usage_list() so that it can return more than one result.
- Fixed bug preventing preview of private images on node form.
- Fixed PDO error when inserting an aggregator title longer than 255 characters.
- Spelled out what TRADITIONAL means in MySQL sql_mode.
- Deprecated "!=" operator for DBTNG; should be "<>".
- Added two new API functions (menu_tree_set_path()/menu_tree_get_path()) were
added in order to enable setting the active menu trail for dynamically
generated menu paths.
- Added new "fast 404" capability in settings.php to bypass Drupal bootstrap
when serving 404 pages for certain file types.
- Added format_string() function which can perform string munging ala the t()
function without the overhead of the translation system.
- Numerous #states system fixes.
- Numerous EntityFieldQuery, DBTNG, and SQLite fixes.
- Numerous Shortcut module fixes.
- Numerous language system fixes.
- Numerous token fixes.
- Numerous CSS fixes.
- Numerous upgrade path fixes.
- Numerous minor string fixes.
- Numerous notice fixes.
Drupal 7.8, 2011-08-31
----------------------
- Fixed critical upgrade path issue with multilingual sites, leading to lost
content.
- Numerous fixes to upgrade path, preventing fatal errors due to incorrect
dependencies.
- Fixed issue with saving files on hosts with open_basedir restrictions.
- Fixed Update manger error when used with Overlay.
- Fixed RTL support in Seven administration theme and Overlay.
- Fixes to nested transaction support.
- Introduced performance pattern to reduce Drupal core's RAM usage.
- Added support for HTML 5 tags to filter_xss_admin().
- Added exception handling to cron.
- Added new hook hook_field_widget_form_alter() for contribtued modules.
- element_validate_*() functions now available to contrib.
- Added new maintainers for several subsystems.
- Numerous testing system improvements.
- Numerous markup and CSS fixes.
- Numerous poll module fixes.
- Numerous notice/warning fixes.
- Numerous documentation fixes.
- Numerous token fixes.
Drupal 7.7, 2011-07-27
----------------------
- Fixed VERSION string.
Drupal 7.6, 2011-07-27
----------------------
- Fixed support for remote streamwrappers.
- AJAX now binds to 'click' instead of 'mousedown'.
- 'Translatable' flag on fields created in UI now defaults to FALSE, to match those created via the API.
- Performance enhancement to permissions page on large numbers of permissions.
- More secure password generation.
- Fix for temporary directory on Windows servers.
- run-tests.sh now uses proc_open() instead of pcntl_fork() for better Windows support.
- Numerous upgrade path fixes.
- Numerous documentation fixes.
- Numerous notice fixes.
- Numerous fixes to improve PHP 5.4 support.
- Numerous RTL improvements.
Drupal 7.5, 2011-07-27
----------------------
- Fixed security issue (Access bypass), see SA-CORE-2011-003.
Drupal 7.4, 2011-06-29
----------------------
- Rolled back patch that caused fatal errors in CTools, Feeds, and other modules using the class registry.
- Fixed critical bug with saving default images.
- Fixed fatal errors when uninstalling some modules.
- Added workaround for MySQL transaction support breaking on DDL statments.
- Improved page caching with external caching systems.
- Fix to Batch API, which was terminating too early.
- Numerous upgrade path fixes.
- Performance fixes.
- Additional test coverage.
- Numerous documentation fixes.
Drupal 7.3, 2011-06-29
----------------------
- Fixed security issue (Access bypass), see SA-CORE-2011-002.
Drupal 7.2, 2011-05-25
----------------------
- Added a default .gitignore file.
- Improved PostgreSQL and SQLite support.
- Numerous critical performance improvements.
- Numerous critical fixes to the upgrade path.
- Numerous fixes to language and translation systems.
- Numerous fixes to AJAX and #states systems.
- Improvements to the locking system.
- Numerous documentation fixes.
- Numerous styling and theme system fixes.
- Numerous fixes for schema mis-matches between Drupal 6 and 7.
- Minor internal API clean-ups.
Drupal 7.1, 2011-05-25
----------------------
- Fixed security issues (Cross site scripting, File access bypass), see SA-CORE-2011-001.
Drupal 7.0, 2011-01-05
----------------------
- Database:
* Fully rewritten database layer utilizing PHP 5's PDO abstraction layer.
* Drupal now requires MySQL >= 5.0.15 or PostgreSQL >= 8.3.
* Added query builders for INSERT, UPDATE, DELETE, MERGE, and SELECT queries.
* Support for master/slave replication, transactions, multi-insert queries,
and other features.
* Added support for the SQLite database engine.
* Default to InnoDB engine, rather than MyISAM, on MySQL when available.
This offers increased scalability and data integrity.
- Security:
* Protected cron.php -- cron will only run if the proper key is provided.
* Implemented a pluggable password system and much stronger password hashes
that are compatible with the Portable PHP password hashing framework.
* Rate limited login attempts to prevent brute-force password guessing, and
improved the flood control API to allow variable time windows and
identifiers for limiting user access to resources.
* Transformed the "Update status" module into the "Update manager" which
can securely install or update modules and themes via a web interface.
- Usability:
* Added contextual links (a.k.a. local tasks) to page elements, such as
blocks, nodes, or comments, which allows to perform the most common tasks
with a single click only.
* Improved installer requirements check.
* Improved support for integration of WYSIWYG editors.
* Implemented drag-and-drop positioning for input format listings.
* Implemented drag-and-drop positioning for language listing.
* Implemented drag-and-drop positioning for poll options.
* Provided descriptions and human-readable names for user permissions.
* Removed comment controls for users.
* Removed display order settings for comment module. Comment display
order can now be customized using the Views module.
* Removed the 'related terms' feature from taxonomy module since this can
now be achieved with Field API.
* Added additional features to the default installation profile, and
implemented a "slimmed down" profile designed for developers.
* Added a built-in, automated cron run feature, which is triggered by site
visitors.
* Added an administrator role which is assigned all permissions for
installed modules automatically.
* Image toolkits are now provided by modules (rather than requiring a
manual file copy to the includes directory).
* Added an edit tab to taxonomy term pages.
* Redesigned password strength validator.
* Redesigned the add content type screen.
* Highlight duplicate URL aliases.
* Renamed "input formats" to "text formats".
* Moved text format permissions to the main permissions page.
* Added configurable ability for users to cancel their own accounts.
* Added "vertical tabs", a reusable interface component that features
automatic summaries and increases usability.
* Replaced fieldsets on node edit and add pages with vertical tabs.
- Performance:
* Improved performance on uncached page views by loading multiple core
objects in a single database query.
* Improved performance for logged-in users by reducing queries for path
alias lookups.
* Improved support for HTTP proxies (including reverse proxies), allowing
anonymous page views to be served entirely from the proxy.
- Documentation:
* Hook API documentation now included in Drupal core.
- News aggregator:
* Added OPML import functionality for RSS feeds.
* Optionally, RSS feeds may be configured to not automatically generate feed blocks.
- Search:
* Added support for language-aware searches.
- Aggregator:
* Introduced architecture that allows pluggable parsers and processors for
syndicating RSS and Atom feeds.
* Added options to suspend updating specific feeds and never discard feeds
items.
- Testing:
* Added test framework and tests.
- Improved time zone support:
* Drupal now uses PHP's time zone database when rendering dates in local
time. Site-wide and user-configured time zone offsets have been converted
to time zone names, e.g. Africa/Abidjan.
* In some cases the upgrade and install scripts do not choose the preferred
site default time zone. The automatically-selected time zone can be
corrected at admin/config/regional/settings.
* If your site is being upgraded from Drupal 6 and you do not have the
contributed date or event modules installed, user time zone settings will
fallback to the system time zone and will have to be reconfigured by each user.
* User-configured time zones now serve as the default time zone for PHP
date/time functions.
- Filter system:
* Revamped the filter API and text format storage.
* Added support for default text formats to be assigned on a per-role basis.
* Refactored the HTML corrector to take advantage of PHP 5 features.
- User system:
* Added clean API functions for creating, loading, updating, and deleting
user roles and permissions.
* Refactored the "access rules" component of user module: The user module
now provides a simple interface for blocking single IP addresses. The
previous functionality in the user module for restricting certain e-mail
addresses and usernames is now available as a contributed module. Further,
IP address range blocking is no longer supported and should be implemented
at the operating system level.
* Removed per-user themes: Contributed modules with similar functionality
are available.
- OpenID:
* Added support for Gmail and Google Apps for Domain identifiers. Users can
now login with their user@example.com identifier when example.com is powered
by Google.
* Made the OpenID module more pluggable.
- Added code registry:
* Using the registry, modules declare their includable files via their .info file,
allowing Drupal to lazy-load classes and interfaces as needed.
- Theme system:
* Removed the Bluemarine, Chameleon and Pushbutton themes. These themes live
on as contributed themes (http://drupal.org/project/bluemarine,
http://drupal.org/project/chameleon and http://drupal.org/project/pushbutton).
* Added Stark theme to make analyzing Drupal's default HTML and CSS easier.
* Added Seven as the default administration theme.
* Variable preprocessing of theme hooks prior to template rendering now goes
through two phases: a 'preprocess' phase and a new 'process' phase. See
http://api.drupal.org/api/function/theme/7 for details.
* Theme hooks implemented as functions (rather than as templates) can now
also have preprocess (and process) functions. See
http://api.drupal.org/api/function/theme/7 for details.
* Added Bartik as the default theme.
- File handling:
* Files are now first class Drupal objects with file_load(), file_save(),
and file_validate() functions and corresponding hooks.
* The file_move(), file_copy() and file_delete() functions now operate on
file objects and invoke file hooks so that modules are notified and can
respond to changes.
* For the occasions when only basic file manipulation are needed--such as
uploading a site logo--that don't require the overhead of databases and
hooks, the current unmanaged copy, move and delete operations have been
preserved but renamed to file_unmanaged_*().
* Rewrote file handling to use PHP stream wrappers to enable support for
both public and private files and to support pluggable storage mechanisms
and access to remote resources (e.g. S3 storage or Flickr photos).
* The mime_extension_mapping variable has been removed. Modules that need to
alter the default MIME type extension mappings should implement
hook_file_mimetype_mapping_alter().
* Added the hook_file_url_alter() hook, which makes it possible to serve
files from a CDN.
* Added a field specifically for uploading files, previously provided by
the contributed module FileField.
- Image handling:
* Improved image handling, including better support for add-on image
libraries.
* Added API and interface for creating advanced image thumbnails.
* Inclusion of additional effects such as rotate and desaturate.
* Added a field specifically for uploading images, previously provided by
the contributed module ImageField.
- Added aliased multi-site support:
* Added support for mapping domain names to sites directories.
- Added RDF support:
* Modules can declare RDF namespaces which are serialized in the tag
for RDFa support.
* Modules can specify how their data structure maps to RDF.
* Added support for RDFa export of nodes, comments, terms, users, etc. and
their fields.
- Search engine optimization and web linking:
* Added a rel="canonical" link on node and comment pages to prevent
duplicate content indexing by search engines.
* Added a default rel="shortlink" link on node and comment pages that
advertises a short link as an alternative URL to third-party services.
* Meta information is now alterable by all modules before rendering.
- Field API:
* Custom data fields may be attached to nodes, users, comments and taxonomy
terms.
* Node bodies and teasers are now Field API fields instead of
being a hard-coded property of node objects.
* In addition, any other object type may register with Field API
and allow custom data fields to be attached to itself.
* Provides most of the features of the former Content Construction
Kit (CCK) module.
* Taxonomy terms are now Field API fields that can be added to any fieldable
object.
- Installer:
* Refactored the installer into an API that allows Drupal to be installed
via a command line script.
- Page organization
* Made the help text area a full featured region with blocks.
* Site mission is replaced with the highlighted content block region and
separate RSS feed description settings.
* The footer message setting was removed in favor of custom blocks.
* Made the main page content a block which can be moved and ordered
with other blocks in the same region.
* Blocks can now return structured arrays for later rendering just
like page callbacks.
- Translation system
* The translation system now supports message context (msgctxt).
* Added support for translatable fields to Field API.
- JavaScript changes
* Upgraded the core JavaScript library to jQuery version 1.4.4.
* Upgraded the jQuery Forms library to 2.52.
* Added jQuery UI 1.8.7, which allows improvements to Drupal's user
experience.
- Better module version support
* Modules now can specify which version of another module they depend on.
- Removed modules from core
* The following modules have been removed from core, because contributed
modules with similar functionality are available:
* Blog API module
* Ping module
* Throttle module
- Improved node access control system.
* All modules may now influence the access to a node at runtime, not just
the module that defined a node.
* Users may now be allowed to bypass node access restrictions without giving
them complete access to the site.
* Access control affects both published and unpublished nodes.
* Numerous other improvements to the node access system.
- Actions system
* Simplified definitions of actions and triggers.
* Removed dependency on the combination of hooks and operations. Triggers
now directly map to module hooks.
- Task handling
* Added a queue API to process many or long-running tasks.
* Added queue API support to cron API.
* Added a locking framework to coordinate long-running operations across
requests.
Drupal 6.23-dev, xxxx-xx-xx (development release)
---------------------------
Drupal 6.22, 2011-05-25
-----------------------
- Made Drupal 6 work better with IIS and Internet Explorer.
- Fixed .po file imports to work better with custom textgroups.
- Improved code documentation at various places.
- Fixed a variety of other bugs.
Drupal 6.21, 2011-05-25
-----------------------
- Fixed security issues (Cross site scripting), see SA-CORE-2011-001.
Drupal 6.20, 2010-12-15
-----------------------
- Fixed a variety of small bugs, improved code documentation.
Drupal 6.19, 2010-08-11
-----------------------
- Fixed a variety of small bugs, improved code documentation.
Drupal 6.18, 2010-08-11
-----------------------
- Fixed security issues (OpenID authentication bypass, File download access
bypass, Comment unpublishing bypass, Actions cross site scripting),
see SA-CORE-2010-002.
Drupal 6.17, 2010-06-02
-----------------------
- Improved PostgreSQL compatibility
- Better PHP 5.3 and PHP 4 compatibility
- Better browser compatibility of CSS and JS aggregation
- Improved logging for login failures
- Fixed an incompatibility with some contributed modules and the locking system
- Fixed a variety of other bugs.
Drupal 6.16, 2010-03-03
-----------------------
- Fixed security issues (Installation cross site scripting, Open redirection,
Locale module cross site scripting, Blocked user session regeneration),
see SA-CORE-2010-001.
- Better support for updated jQuery versions.
- Reduced resource usage of update.module.
- Fixed several issues relating to support of installation profiles and
distributions.
- Added a locking framework to avoid data corruption on long operations.
- Fixed a variety of other bugs.
Drupal 6.15, 2009-12-16
-----------------------
- Fixed security issues (Cross site scripting), see SA-CORE-2009-009.
- Fixed a variety of other bugs.
Drupal 6.14, 2009-09-16
-----------------------
- Fixed security issues (OpenID association cross site request forgeries,
OpenID impersonation and File upload), see SA-CORE-2009-008.
- Changed the system modules page to not run all cache rebuilds; use the
button on the performance settings page to achieve the same effect.
- Added support for PHP 5.3.0 out of the box.
- Fixed a variety of small bugs.
Drupal 6.13, 2009-07-01
-----------------------
- Fixed security issues (Cross site scripting, Input format access bypass and
Password leakage in URL), see SA-CORE-2009-007.
- Fixed a variety of small bugs.
Drupal 6.12, 2009-05-13
-----------------------
- Fixed security issues (Cross site scripting), see SA-CORE-2009-006.
- Fixed a variety of small bugs.
Drupal 6.11, 2009-04-29
-----------------------
- Fixed security issues (Cross site scripting and limited information
disclosure), see SA-CORE-2009-005
- Fixed performance issues with the menu router cache, the update
status cache and improved cache invalidation
- Fixed a variety of small bugs.
Drupal 6.10, 2009-02-25
-----------------------
- Fixed a security issue, (Local file inclusion on Windows),
see SA-CORE-2009-003
- Fixed node_feed() so custom fields can show up in RSS feeds.
- Improved PostgreSQL compatibility.
- Fixed a variety of small bugs.
Drupal 6.9, 2009-01-14
----------------------
- Fixed security issues, (Access Bypass, Validation Bypass and Hardening
against SQL injection), see SA-CORE-2009-001
- Made HTTP request checking more robust and informative.
- Fixed HTTP_HOST checking to work again with HTTP 1.0 clients and
basic shell scripts.
- Removed t() calls from all schema documentation. Suggested best practice
changed for contributed modules, see http://drupal.org/node/322731.
- Fixed a variety of small bugs.
Drupal 6.8, 2008-12-11
----------------------
- Removed a previous change incompatible with PHP 5.1.x and lower.
Drupal 6.7, 2008-12-10
----------------------
- Fixed security issues, (Cross site request forgery and Cross site scripting), see SA-2008-073
- Updated robots.txt and .htaccess to match current file use.
- Fixed a variety of small bugs.
Drupal 6.6, 2008-10-22
----------------------
- Fixed security issues, (File inclusion, Cross site scripting), see SA-2008-067
- Fixed a variety of small bugs.
Drupal 6.5, 2008-10-08
----------------------
- Fixed security issues, (File upload access bypass, Access rules bypass,
BlogAPI access bypass), see SA-2008-060.
- Fixed a variety of small bugs.
Drupal 6.4, 2008-08-13
----------------------
- Fixed a security issue (Cross site scripting, Arbitrary file uploads via
BlogAPI, Cross site request forgeries and Various Upload module
vulnerabilities), see SA-2008-047.
- Improved error messages during installation.
- Fixed a bug that prevented AHAH handlers to be attached to radios widgets.
- Fixed a variety of small bugs.
Drupal 6.3, 2008-07-09
----------------------
- Fixed security issues, (Cross site scripting, cross site request forgery,
session fixation and SQL injection), see SA-2008-044.
- Slightly modified installation process to prevent file ownership issues on
shared hosts.
- Improved PostgreSQL compatibility (rewritten queries; custom blocks).
- Upgraded to jQuery 1.2.6.
- Performance improvements to search, menu handling and form API caches.
- Fixed Views compatibility issues (Views for Drupal 6 requires Drupal 6.3+).
- Fixed a variety of small bugs.
Drupal 6.2, 2008-04-09
----------------------
- Fixed a variety of small bugs.
- Fixed a security issue (Access bypasses), see SA-2008-026.
Drupal 6.1, 2008-02-27
----------------------
- Fixed a variety of small bugs.
- Fixed a security issue (Cross site scripting), see SA-2008-018.
Drupal 6.0, 2008-02-13
----------------------
- New, faster and better menu system.
- New watchdog as a hook functionality.
* New hook_watchdog that can be implemented by any module to route log
messages to various destinations.
* Expands the severity levels from 3 (Error, Warning, Notice) to the 8
levels defined in RFC 3164.
* The watchdog module is now called dblog, and is optional, but enabled by
default in the default installation profile.
* Extended the database log module so log messages can be filtered.
* Added syslog module: useful for monitoring large Drupal installations.
- Added optional e-mail notifications when users are approved, blocked, or
deleted.
- Drupal works with error reporting set to E_ALL.
- Added scripts/drupal.sh to execute Drupal code from the command line. Useful
to use Drupal as a framework to build command-line tools.
- Made signature support optional and made it possible to theme signatures.
- Made it possible to filter the URL aliases on the URL alias administration
screen.
- Language system improvements:
* Support for right to left languages.
* Language detection based on parts of the URL.
* Browser based language detection.
* Made it possible to specify a node's language.
* Support for translating posts on the site to different languages.
* Language dependent path aliases.
* Automatically import translations when adding a new language.
* JavaScript interface translation.
* Automatically import a module's translation upon enabling that module.
- Moved "PHP input filter" to a standalone module so it can be deleted for
security reasons.
- Usability:
* Improved handling of teasers in posts.
* Added sticky table headers.
* Check for clean URL support automatically with JavaScript.
* Removed default/settings.php. Instead the installer will create it from
default.settings.php.
* Made it possible to configure your own date formats.
* Remember anonymous comment posters.
* Only allow modules and themes to be enabled that have explicitly been
ported to the correct core API version.
* Can now specify the minimum PHP version required for a module within the
.info file.
* Drupal core no longer requires CREATE TEMPORARY TABLES or LOCK TABLES
database rights.
* Dynamically check password strength and confirmation.
* Refactored poll administration.
* Implemented drag-and-drop positioning for blocks, menu items, taxonomy
vocabularies and terms, forums, profile fields, and input format filters.
- Theme system:
* Added .info files to themes and made it easier to specify regions and
features.
* Added theme registry: modules can directly provide .tpl.php files for
their themes without having to create theme_ functions.
* Used the Garland theme for the installation and maintenance pages.
* Added theme preprocess functions for themes that are templates.
* Added support for themeable functions in JavaScript.
- Refactored update.php to a generic batch API to be able to run time-consuming
operations in multiple subsequent HTTP requests.
- Installer:
* Themed the installer with the Garland theme.
* Added form to provide initial site information during installation.
* Added ability to provide extra installation steps programmatically.
* Made it possible to import interface translations during installation.
- Added the HTML corrector filter:
* Fixes faulty and chopped off HTML in postings.
* Tags are now automatically closed at the end of the teaser.
- Performance:
* Made it easier to conditionally load .include files and split up many core
modules.
* Added a JavaScript aggregator.
* Added block-level caching, improving performance for both authenticated
and anonymous users.
* Made Drupal work correctly when running behind a reverse proxy like
Squid or Pound.
- File handling improvements:
* Entries in the files table are now keyed to a user instead of a node.
* Added reusable validation functions to check for uploaded file sizes,
extensions, and image resolution.
* Added ability to create and remove temporary files during a cron job.
- Forum improvements:
* Any node type may now be posted in a forum.
- Taxonomy improvements:
* Descriptions for terms are now shown on taxonomy/term pages as well
as RSS feeds.
* Added versioning support to categories by associating them with node
revisions.
- Added support for OpenID.
- Added support for triggering configurable actions.
- Added the Update status module to automatically check for available updates
and warn sites if they are missing security updates or newer versions.
Sites deploying from CVS should use http://drupal.org/project/cvs_deploy.
Advanced settings provided by http://drupal.org/project/update_advanced.
- Upgraded the core JavaScript library to jQuery version 1.2.3.
- Added a new Schema API, which provides built-in support for core and
contributed modules to work with databases other than MySQL.
- Removed drupal.module. The functionality lives on as the Site network
contributed module (http://drupal.org/project/site_network).
- Removed old system updates. Updates from Drupal versions prior to 5.x will
require upgrading to 5.x before upgrading to 6.x.
Drupal 5.23, 2010-08-11
-----------------------
- Fixed security issues (File download access bypass, Comment unpublishing
bypass), see SA-CORE-2010-002.
Drupal 5.22, 2010-03-03
-----------------------
- Fixed security issues (Open redirection, Locale module cross site scripting,
Blocked user session regeneration), see SA-CORE-2010-001.
Drupal 5.21, 2009-12-16
-----------------------
- Fixed a security issue (Cross site scripting), see SA-CORE-2009-009.
- Fixed a variety of small bugs.
Drupal 5.20, 2009-09-16
-----------------------
- Avoid security problems resulting from writing Drupal 6-style menu
declarations.
- Fixed security issues (session fixation), see SA-CORE-2009-008.
- Fixed a variety of small bugs.
Drupal 5.19, 2009-07-01
-----------------------
- Fixed security issues (Cross site scripting and Password leakage in URL), see
SA-CORE-2009-007.
- Fixed a variety of small bugs.
Drupal 5.18, 2009-05-13
-----------------------
- Fixed security issues (Cross site scripting), see SA-CORE-2009-006.
- Fixed a variety of small bugs.
Drupal 5.17, 2009-04-29
-----------------------
- Fixed security issues (Cross site scripting and limited information
disclosure) see SA-CORE-2009-005.
- Fixed a variety of small bugs.
Drupal 5.16, 2009-02-25
-----------------------
- Fixed a security issue, (Local file inclusion on Windows), see SA-CORE-2009-004.
- Fixed a variety of small bugs.
Drupal 5.15, 2009-01-14
-----------------------
- Fixed security issues, (Hardening against SQL injection), see
SA-CORE-2009-001
- Fixed HTTP_HOST checking to work again with HTTP 1.0 clients and basic shell
scripts.
- Fixed a variety of small bugs.
Drupal 5.14, 2008-12-11
-----------------------
- removed a previous change incompatible with PHP 5.1.x and lower.
Drupal 5.13, 2008-12-10
-----------------------
- fixed a variety of small bugs.
- fixed security issues, (Cross site request forgery and Cross site scripting), see SA-2008-073
- updated robots.txt and .htaccess to match current file use.
Drupal 5.12, 2008-10-22
-----------------------
- fixed security issues, (File inclusion), see SA-2008-067
Drupal 5.11, 2008-10-08
-----------------------
- fixed a variety of small bugs.
- fixed security issues, (File upload access bypass, Access rules bypass,
BlogAPI access bypass, Node validation bypass), see SA-2008-060
Drupal 5.10, 2008-08-13
-----------------------
- fixed a variety of small bugs.
- fixed security issues, (Cross site scripting, Arbitrary file uploads via
BlogAPI and Cross site request forgery), see SA-2008-047
Drupal 5.9, 2008-07-23
----------------------
- fixed a variety of small bugs.
- fixed security issues, (Session fixation), see SA-2008-046
Drupal 5.8, 2008-07-09
----------------------
- fixed a variety of small bugs.
- fixed security issues, (Cross site scripting, cross site request forgery, and
session fixation), see SA-2008-044
Drupal 5.7, 2008-01-28
----------------------
- fixed the input format configuration page.
- fixed a variety of small bugs.
Drupal 5.6, 2008-01-10
----------------------
- fixed a variety of small bugs.
- fixed a security issue (Cross site request forgery), see SA-2008-005
- fixed a security issue (Cross site scripting, UTF8), see SA-2008-006
- fixed a security issue (Cross site scripting, register_globals), see SA-2008-007
Drupal 5.5, 2007-12-06
----------------------
- fixed missing missing brackets in a query in the user module.
- fixed taxonomy feed bug introduced by SA-2007-031
Drupal 5.4, 2007-12-05
----------------------
- fixed a variety of small bugs.
- fixed a security issue (SQL injection), see SA-2007-031
Drupal 5.3, 2007-10-17
----------------------
- fixed a variety of small bugs.
- fixed a security issue (HTTP response splitting), see SA-2007-024
- fixed a security issue (Arbitrary code execution via installer), see SA-2007-025
- fixed a security issue (Cross site scripting via uploads), see SA-2007-026
- fixed a security issue (User deletion cross site request forgery), see SA-2007-029
- fixed a security issue (API handling of unpublished comment), see SA-2007-030
Drupal 5.2, 2007-07-26
----------------------
- changed hook_link() $teaser argument to match documentation.
- fixed a variety of small bugs.
- fixed a security issue (cross-site request forgery), see SA-2007-017
- fixed a security issue (cross-site scripting), see SA-2007-018
Drupal 5.1, 2007-01-29
----------------------
- fixed security issue (code execution), see SA-2007-005
- fixed a variety of small bugs.
Drupal 5.0, 2007-01-15
----------------------
- Completely retooled the administration page
* /Admin now contains an administration page which may be themed
* Reorganised administration menu items by task and by module
* Added a status report page with detailed PHP/MySQL/Drupal information
- Added web-based installer which can:
* Check installation and run-time requirements
* Automatically generate the database configuration file
* Install pre-made installation profiles or distributions
* Import the database structure with automatic table prefixing
* Be localized
- Added new default Garland theme
- Added color module to change some themes' color schemes
- Included the jQuery JavaScript library 1.0.4 and converted all core JavaScript to use it
- Introduced the ability to alter mail sent from system
- Module system:
* Added .info files for module meta-data
* Added support for module dependencies
* Improved module installation screen
* Moved core modules to their own directories
* Added support for module uninstalling
- Added support for different cache backends
- Added support for a generic "sites/all" directory.
- Usability:
* Added support for auto-complete forms (AJAX) to user profiles.
* Made it possible to instantly assign roles to newly created user accounts.
* Improved configurability of the contact forms.
* Reorganized the settings pages.
* Made it easy to investigate popular search terms.
* Added a 'select all' checkbox and a range select feature to administration tables.
* Simplified the 'break' tag to split teasers from body.
* Use proper capitalization for titles, menu items and operations.
- Integrated urlfilter.module into filter.module
- Block system:
* Extended the block visibility settings with a role specific setting.
* Made it possible to customize all block titles.
- Poll module:
* Optionally allow people to inspect all votes.
* Optionally allow people to cancel their vote.
- Distributed authentication:
* Added default server option.
- Added default robots.txt to control crawlers.
- Database API:
* Added db_table_exists().
- Blogapi module:
* 'Blogapi new' and 'blogapi edit' nodeapi operations.
- User module:
* Added hook_profile_alter().
* E-mail verification is made optional.
* Added mass editing and filtering on admin/user/user.
- PHP Template engine:
* Add the ability to look for a series of suggested templates.
* Look for page templates based upon the path.
* Look for block templates based upon the region, module, and delta.
- Content system:
* Made it easier for node access modules to work well with each other.
* Added configurable content types.
* Changed node rendering to work with structured arrays.
- Performance:
* Improved session handling: reduces database overhead.
* Improved access checking: reduces database overhead.
* Made it possible to do memcached based session management.
* Omit sidebars when serving a '404 - Page not found': saves CPU cycles and bandwidth.
* Added an 'aggressive' caching policy.
* Added a CSS aggregator and compressor (up to 40% faster page loads).
- Removed the archive module.
- Upgrade system:
* Created space for update branches.
- Form API:
* Made it possible to programmatically submit forms.
* Improved api for multistep forms.
- Theme system:
* Split up and removed drupal.css.
* Added nested lists generation.
* Added a self-clearing block class.
Drupal 4.7.11, 2008-01-10
-------------------------
- fixed a security issue (Cross site request forgery), see SA-2008-005
- fixed a security issue (Cross site scripting, UTF8), see SA-2008-006
- fixed a security issue (Cross site scripting, register_globals), see SA-2008-007
Drupal 4.7.10, 2007-12-06
-------------------------
- fixed taxonomy feed bug introduced by SA-2007-031
Drupal 4.7.9, 2007-12-05
------------------------
- fixed a security issue (SQL injection), see SA-2007-031
Drupal 4.7.8, 2007-10-17
------------------------
- fixed a security issue (HTTP response splitting), see SA-2007-024
- fixed a security issue (Cross site scripting via uploads), see SA-2007-026
- fixed a security issue (API handling of unpublished comment), see SA-2007-030
Drupal 4.7.7, 2007-07-26
------------------------
- fixed security issue (XSS), see SA-2007-018
Drupal 4.7.6, 2007-01-29
------------------------
- fixed security issue (code execution), see SA-2007-005
Drupal 4.7.5, 2007-01-05
------------------------
- Fixed security issue (XSS), see SA-2007-001
- Fixed security issue (DoS), see SA-2007-002
Drupal 4.7.4, 2006-10-18
------------------------
- Fixed security issue (XSS), see SA-2006-024
- Fixed security issue (CSRF), see SA-2006-025
- Fixed security issue (Form action attribute injection), see SA-2006-026
Drupal 4.7.3, 2006-08-02
------------------------
- Fixed security issue (XSS), see SA-2006-011
Drupal 4.7.2, 2006-06-01
------------------------
- Fixed critical upload issue, see SA-2006-007
- Fixed taxonomy XSS issue, see SA-2006-008
- Fixed a variety of small bugs.
Drupal 4.7.1, 2006-05-24
------------------------
- Fixed critical SQL issue, see SA-2006-005
- Fixed a serious upgrade related bug.
- Fixed a variety of small bugs.
Drupal 4.7.0, 2006-05-01
------------------------
- Added free tagging support.
- Added a site-wide contact form.
- Theme system:
* Added the PHPTemplate theme engine and removed the Xtemplate engine.
* Converted the bluemarine theme from XTemplate to PHPTemplate.
* Converted the pushbutton theme from XTemplate to PHPTemplate.
- Usability:
* Reworked the 'request new password' functionality.
* Reworked the node and comment edit forms.
* Made it easy to add nodes to the navigation menu.
* Added site 'offline for maintenance' feature.
* Added support for auto-complete forms (AJAX).
* Added support for collapsible page sections (JS).
* Added support for resizable text fields (JS).
* Improved file upload functionality (AJAX).
* Reorganized some settings pages.
* Added friendly database error screens.
* Improved styling of update.php.
- Refactored the forms API.
* Made it possible to alter, extend or theme forms.
- Comment system:
* Added support for "mass comment operations" to ease repetitive tasks.
* Comment moderation has been removed.
- Node system:
* Reworked the revision functionality.
* Removed the bookmarklet code. Third-party modules can now handle
This.
- Upgrade system:
* Allows contributed modules to plug into the upgrade system.
- Profiles:
* Added a block to display author information along with posts.
* Added support for private profile fields.
- Statistics module:
* Added the ability to track page generation times.
* Made it possible to block certain IPs/hostnames.
- Block system:
* Added support for theme-specific block regions.
- Syndication:
* Made the aggregator module parse Atom feeds.
* Made the aggregator generate RSS feeds.
* Added RSS feed settings.
- XML-RPC:
* Replaced the XML-RPC library by a better one.
- Performance:
* Added 'loose caching' option for high-traffic sites.
* Improved performance of path aliasing.
* Added the ability to track page generation times.
- Internationalization:
* Improved Unicode string handling API.
* Added support for PHP's multibyte string module.
- Added support for PHP5's 'mysqli' extension.
- Search module:
* Made indexer smarter and more robust.
* Added advanced search operators (e.g. phrase, node type, ...).
* Added customizable result ranking.
- PostgreSQL support:
* Removed dependency on PL/pgSQL procedural language.
- Menu system:
* Added support for external URLs.
- Queue module:
* Removed from core.
- HTTP handling:
* Added support for a tolerant Base URL.
* Output URIs relative to the root, without a base tag.
Drupal 4.6.11, 2007-01-05
-------------------------
- Fixed security issue (XSS), see SA-2007-001
- Fixed security issue (DoS), see SA-2007-002
Drupal 4.6.10, 2006-10-18
-------------------------
- Fixed security issue (XSS), see SA-2006-024
- Fixed security issue (CSRF), see SA-2006-025
- Fixed security issue (Form action attribute injection), see SA-2006-026
Drupal 4.6.9, 2006-08-02
------------------------
- Fixed security issue (XSS), see SA-2006-011
Drupal 4.6.8, 2006-06-01
------------------------
- Fixed critical upload issue, see SA-2006-007
- Fixed taxonomy XSS issue, see SA-2006-008
Drupal 4.6.7, 2006-05-24
------------------------
- Fixed critical SQL issue, see SA-2006-005
Drupal 4.6.6, 2006-03-13
------------------------
- Fixed bugs, including 4 security vulnerabilities.
Drupal 4.6.5, 2005-12-12
------------------------
- Fixed bugs: no critical bugs were identified.
Drupal 4.6.4, 2005-11-30
------------------------
- Fixed bugs, including 3 security vulnerabilities.
Drupal 4.6.3, 2005-08-15
------------------------
- Fixed bugs, including a critical "arbitrary PHP code execution" bug.
Drupal 4.6.2, 2005-06-29
------------------------
- Fixed bugs, including two critical "arbitrary PHP code execution" bugs.
Drupal 4.6.1, 2005-06-01
------------------------
- Fixed bugs, including a critical input validation bug.
Drupal 4.6.0, 2005-04-15
------------------------
- PHP5 compliance
- Search:
* Added UTF-8 support to make it work with all languages.
* Improved search indexing algorithm.
* Improved search output.
* Impose a throttle on indexing of large sites.
* Added search block.
- Syndication:
* Made the ping module ping pingomatic.com which, in turn, will ping all the major ping services.
* Made Drupal generate RSS 2.0 feeds.
* Made RSS feeds extensible.
* Added categories to RSS feeds.
* Added enclosures to RSS feeds.
- Flood control mechanism:
* Added a mechanism to throttle certain operations.
- Usability:
* Refactored the block configuration pages.
* Refactored the statistics pages.
* Refactored the watchdog pages.
* Refactored the throttle module configuration.
* Refactored the access rules page.
* Refactored the content administration page.
* Introduced forum configuration pages.
* Added a 'add child page' link to book pages.
- Contact module:
* Added a simple contact module that allows users to contact each other using e-mail.
- Multi-site configuration:
* Made it possible to run multiple sites from a single code base.
- Added an image API: enables better image handling.
- Block system:
* Extended the block visibility settings.
- Theme system:
* Added new theme functions.
- Database backend:
* The PEAR database backend is no longer supported.
- Performance:
* Improved performance of the forum topics block.
* Improved performance of the tracker module.
* Improved performance of the node pages.
- Documentation:
* Improved and extended PHPDoc/Doxygen comments.
Drupal 4.5.8, 2006-03-13
------------------------
- Fixed bugs, including 3 security vulnerabilities.
Drupal 4.5.7, 2005-12-12
------------------------
- Fixed bugs: no critical bugs were identified.
Drupal 4.5.6, 2005-11-30
------------------------
- Fixed bugs, including 3 security vulnerabilities.
Drupal 4.5.5, 2005-08-15
------------------------
- Fixed bugs, including a critical "arbitrary PHP code execution" bug.
Drupal 4.5.4, 2005-06-29
------------------------
- Fixed bugs, including two critical "arbitrary PHP code execution" bugs.
Drupal 4.5.3, 2005-06-01
------------------------
- Fixed bugs, including a critical input validation bug.
Drupal 4.5.2, 2005-01-15
------------------------
- Fixed bugs: a cross-site scripting (XSS) vulnerability has been fixed.
Drupal 4.5.1, 2004-12-01
------------------------
- Fixed bugs: no critical bugs were identified.
Drupal 4.5.0, 2004-10-18
------------------------
- Navigation:
* Made it possible to add, delete, rename and move menu items.
* Introduced tabs and subtabs for local tasks.
* Reorganized the navigation menus.
- User management:
* Added support for multiple roles per user.
* Made it possible to add custom profile fields.
* Made it possible to browse user profiles by field.
- Node system:
* Added support for node-level permissions.
- Comment module:
* Made it possible to leave contact information without having to register.
- Upload module:
* Added support for uploading documents (includes images).
- Forum module:
* Added support for sticky forum topics.
* Made it possible to track forum topics.
- Syndication:
* Added support for RSS ping-notifications of http://technorati.com/.
* Refactored the categorization of syndicated news items.
* Added an URL alias for 'rss.xml'.
* Improved date parsing.
- Database backend:
* Added support for multiple database connections.
* The PostgreSQL backend does no longer require PEAR.
- Theme system:
* Changed all GIFs to PNGs.
* Reorganised the handling of themes, template engines, templates and styles.
* Unified and extended the available theme settings.
* Added theme screenshots.
- Blocks:
* Added 'recent comments' block.
* Added 'categories' block.
- Blogger API:
* Added support for auto-discovery of blogger API via RSD.
- Performance:
* Added support for sending gzip compressed pages.
* Improved performance of the forum module.
- Accessibility:
* Improved the accessibility of the archive module's calendar.
* Improved form handling and error reporting.
* Added HTTP redirects to prevent submitting twice when refreshing right after a form submission.
- Refactored 403 (forbidden) handling and added support for custom 403 pages.
- Documentation:
* Added PHPDoc/Doxygen comments.
- Filter system:
* Added support for using multiple input formats on the site
* Expanded the embedded PHP-code feature so it can be used everywhere
* Added support for role-dependent filtering, through input formats
- UI translation:
* Managing translations is now completely done through the administration interface
* Added support for importing/exporting gettext .po files
Drupal 4.4.3, 2005-06-01
------------------------
- Fixed bugs, including a critical input validation bug.
Drupal 4.4.2, 2004-07-04
------------------------
- Fixed bugs: no critical bugs were identified.
Drupal 4.4.1, 2004-05-01
------------------------
- Fixed bugs: no critical bugs were identified.
Drupal 4.4.0, 2004-04-01
------------------------
- Added support for the MetaWeblog API and MovableType extensions.
- Added a file API: enables better document management.
- Improved the watchdog and search module to log search keys.
- News aggregator:
* Added support for conditional GET.
* Added OPML feed subscription list.
* Added support for , , , , and .
- Comment module:
* Made it possible to disable the "comment viewing controls".
- Performance:
* Improved module loading when serving cached pages.
* Made it possible to automatically disable modules when under heavy load.
* Made it possible to automatically disable blocks when under heavy load.
* Improved performance and memory footprint of the locale module.
- Theme system:
* Made all theme functions start with 'theme_'.
* Made all theme functions return their output.
* Migrated away from using the BaseTheme class.
* Added many new theme functions and refactored existing theme functions.
* Added avatar support to 'Xtemplate'.
* Replaced theme 'UnConeD' by 'Chameleon'.
* Replaced theme 'Marvin' by 'Pushbutton'.
- Usability:
* Added breadcrumb navigation to all pages.
* Made it possible to add context-sensitive help to all pages.
* Replaced drop-down menus by radio buttons where appropriate.
* Removed the 'magic_quotes_gpc = 0' requirement.
* Added a 'book navigation' block.
- Accessibility:
* Made themes degrade gracefully in absence of CSS.
* Grouped form elements using '' and '' tags.
* Added '' tags to form elements.
- Refactored 404 (file not found) handling and added support for custom 404 pages.
- Improved the filter system to prevent conflicts between filters:
* Made it possible to change the order in which filters are applied.
- Documentation:
* Added PHPDoc/Doxygen comments.
Drupal 4.3.2, 2004-01-01
------------------------
- Fixed bugs: no critical bugs were identified.
Drupal 4.3.1, 2003-12-01
------------------------
- Fixed bugs: no critical bugs were identified.
Drupal 4.3.0, 2003-11-01
------------------------
- Added support for configurable URLs.
- Added support for sortable table columns.
- Database backend:
* Added support for selective database table prefixing.
- Performance:
* Optimized many SQL queries for speed by converting left joins to inner joins.
- Comment module:
* Rewrote the comment housekeeping code to be much more efficient and scalable.
* Changed the comment module to use the standard pager.
- User module:
* Added support for multiple sessions per user.
* Added support for anonymous user sessions.
- Forum module:
* Improved the forum views and the themability thereof.
- Book module:
* Improved integration of non-book nodes in the book outline.
- Usability:
* Added support for "mass node operations" to ease repetitive tasks.
* Added support for breadcrumb navigation to several modules' user pages.
* Integrated the administration pages with the normal user pages.
Drupal 4.2.0, 2003-08-01
------------------------
- Added support for clean URLs.
- Added textarea hook and support for onload attributes: enables integration of WYSIWYG editors.
- Rewrote the RSS/RDF parser:
* It will now use PHP's built-in XML parser to parse news feeds.
- Rewrote the administration pages:
* Improved the navigational elements and added breadcrumb navigation.
* Improved the look and feel.
* Added context-sensitive help.
- Database backend:
* Fixed numerous SQL queries to make Drupal ANSI compliant.
* Added MSSQL database scheme.
- Search module:
* Changed the search module to use implicit AND'ing instead of implicit OR'ing.
- Node system:
* Replaced the "post content" permission by more fine-grained permissions.
* Improved content submission:
+ Improved teasers: teasers are now optional, teaser length can be configured, teaser and body are edited in a single textarea, users will no longer be bothered with teasers when the post is too short for one.
+ Added the ability to preview both the short and the full version of your posts.
* Extended the node API which allows for better integration.
* Added default node settings to control the behavior for promotion, moderation and other options.
- Themes:
* Replaced theme "Goofy" by "Xtemplate", a template driven theme.
- Removed the 'register_globals = on' requirement.
- Added better installation instructions.
Drupal 4.1.0, 2003-02-01
------------------------
- Collaboratively revised and expanded the Drupal documentation.
- Rewrote comment.module:
* Reintroduced comment rating/moderation.
* Added support for comment paging.
* Performance improvements: improved comment caching, faster SQL queries, etc.
- Rewrote block.module:
* Performance improvements: blocks are no longer rendered when not displayed.
- Rewrote forum.module:
* Added a lot of features one can find in stand-alone forum software including but not limited to support for topic paging, added support for icons, rewrote the statistics module, etc.
- Rewrote statistics.module:
* Collects access counts for each node, referrer logs, number of users/guests.
* Export blocks displaying top viewed nodes over last 24 hour period, top viewed nodes over all time, last nodes viewed, how many users/guest online.
- Added throttle.module:
* Auto-throttle congestion control mechanism: Drupal can adapt itself based on the server load.
- Added profile.module:
* Enables to extend the user and registration page.
- Added pager support to the main page.
- Replaced weblogs.module by ping.module:
* Added support for normal and RSS notifications of http://blo.gs/.
* Added support for RSS ping-notifications of http://weblogs.com/.
- Removed the rating module
- Themes:
* Removed a significant portion of hard-coded mark-up.
Drupal 4.0.0, 2002-06-15
------------------------
- Added tracker.module:
* Replaces the previous "your [site]" links (recent comments and nodes).
- Added weblogs.module:
* This will ping weblogs.com when new content is promoted.
- Added taxonomy module which replaces the meta module.
* Supports relations, hierarchies and synonyms.
- Added a caching system:
* Speeds up pages for anonymous users and reduces system load.
- Added support for external SMTP libraries.
- Added an archive extension to the calendar.
- Added support for the Blogger API.
- Themes:
* Cleaned up the theme system.
* Moved themes that are not maintained to contributions CVS repository.
- Database backend:
* Changed to PEAR database abstraction layer.
* Using ANSI SQL queries to be more portable.
- Rewrote the user system:
* Added support for Drupal authentication through XML-RPC and through a Jabber server.
* Added support for modules to add more user data.
* Users may delete their own account.
* Added who's new and who's online blocks.
- Changed block system:
* Various hard coded blocks are now dynamic.
* Blocks can now be enabled and/or be set by the user.
* Blocks can be set to only show up on some pages.
* Merged box module with block module.
- Node system:
* Blogs can be updated.
* Teasers (abstracts) on all node types.
* Improved error checking.
* Content versioning support.
* Usability improvements.
- Improved book module to support text, HTML and PHP pages.
- Improved comment module to mark new comments.
- Added a general outliner which will let any node type be linked to a book.
- Added an update script that lets you upgrade from previous releases or on a day to day basis when using the development tree.
- Search module:
* Improved the search system by making it context sensitive.
* Added indexing.
- Various updates:
* Changed output to valid XHTML.
* Improved multiple sites using the same Drupal database support.
* Added support for session IDs in URLs instead of cookies.
* Made the type of content on the front page configurable.
* Made each cloud site have its own settings.
* Modules and themes can now be enabled/disabled using the administration pages.
* Added URL abstraction for links.
* Usability changes (renamed links, better UI, etc).
- Collaboratively revised and expanded the Drupal documentation.
Drupal 3.0.1, 2001-10-15
------------------------
- Various updates:
* Added missing translations
* Updated the themes: tidied up some HTML code and added new Drupal logos.
Drupal 3.0.0, 2001-09-15
------------------------
- Major overhaul of the entire underlying design:
* Everything is based on nodes: nodes are a conceptual "black box" to couple and manage different types of content and that promotes reusing existing code, thus reducing the complexity and size of Drupal as well as improving long-term stability.
- Rewrote submission/moderation queue and renamed it to queue.module.
- Removed FAQ and documentation module and merged them into a "book module".
- Removed ban module and integrated it into account.module as "access control":
* Access control is based on much more powerful regular expressions (regex) now rather than on MySQL pattern matching.
- Rewrote watchdog and submission throttle:
* Improved watchdog messages and added watchdog filter.
- Rewrote headline code and renamed it to import.module and export.module:
* Added various improvements, including a better parser, bundles and better control over individual feeds.
- Rewrote section code and renamed it to meta.module:
* Supports unlimited amount of nested topics. Topics can be nested to create a multi-level hierarchy.
- Rewrote configuration file resolving:
* Drupal tries to locate a configuration file that matches your domain name or uses conf.php if the former failed. Note also that the configuration files got renamed from .conf to .php for security's sake on mal-configured Drupal sites.
- Added caching support which makes Drupal extremely scalable.
- Added access.module:
* Allows you to set up 'roles' (groups) and to bind a set of permissions to each group.
- Added blog.module.
- Added poll.module.
- Added system.module:
* Moved most of the configuration options from hostname.conf to the new administration section.
* Added support for custom "filters".
- Added statistics.module
- Added moderate.module:
* Allows to assign users editorial/moderator rights to certain nodes or topics.
- Added page.module:
* Allows creation of static (and dynamic) pages through the administration interface.
- Added help.module:
* Groups all available module documentation on a single page.
- Added forum.module:
* Added an integrated forum.
- Added cvs.module and cvs-to-sql.pl:
* Allows to display and mail CVS log messages as daily digests.
- Added book.module:
* Allows collaborative handbook writing: primary used for Drupal documentation.
- Removed cron.module and integrated it into conf.module.
- Removed module.module as it was no longer needed.
- Various updates:
* Added "auto-post new submissions" feature versus "moderate new submissions".
* Introduced links/Drupal tags: [[link]]
* Added preview functionality when submitting new content (such as a story) from the administration pages.
* Made the administration section only show those links a user has access to.
* Made all modules use specific form_* functions to guarantee a rock-solid forms and more consistent layout.
* Improved scheduler:
+ Content can be scheduled to be 'posted', 'queued' and 'hidden'.
* Improved account module:
+ Added "access control" to allow/deny certain usernames/e-mail addresses/hostnames.
* Improved locale module:
+ Added new overview to easy the translation process.
* Improved comment module:
+ Made it possible to permanently delete comments.
* Improved rating module
* Improved story module:
+ Added preview functionality for administrators.
+ Made it possible to permanently delete stories.
* Improved themes:
+ W3C validation on a best effort basis.
+ Removed $theme->control() from themes.
+ Added theme "goofy".
- Collaboratively revised and expanded the Drupal documentation.
Drupal 2.0.0, 2001-03-15
------------------------
- Rewrote the comment/discussion code:
* Comment navigation should be less confusing now.
* Additional/alternative display and order methods have been added.
* Modules can be extended with a "comment system": modules can embed the existing comment system without having to write their own, duplicate comment system.
- Added sections and section manager:
* Story sections can be maintained from the administration pages.
* Story sections make the open submission more adaptive in that you can set individual post, dump and expiration thresholds for each section according to the story type and urgency level: stories in certain sections do not "expire" and might stay interesting and active as time passes by, whereas news-related stories are only considered "hot" over a short period of time.
- Multiple vhosts + multiple directories:
* You can set up multiple Drupal sites on top of the same physical source tree either by using vhosts or sub-directories.
- Added "user ratings" similar to SlashCode's Karma or Scoop's Mojo:
* All rating logic is packed into a module to ease experimenting with different rating heuristics/algorithms.
- Added "search infrastructure":
* Improved search page and integrated search functionality in the administration pages.
- Added translation / localization / internationalization support:
* Because many people would love to see their website showing a lot less of English, and far more of their own language, Drupal provides a framework to set up a multi-lingual website or to overwrite the default English text in English.
- Added fine-grained user permission (or group) system:
* Users can be granted access to specific administration sections. Example: a FAQ maintainer can be given access to maintain the FAQ and translators can be given access to the translation pages.
- Added FAQ module
- Changed the "open submission queue" into a (optional) module.
- Various updates:
* Improved account module:
+ User accounts can be deleted.
+ Added fine-grained permission support.
* Improved block module
* Improved diary module:
+ Diary entries can be deleted
* Improved headline module:
+ Improved parser to support more "generic" RDF/RSS/XML backend.
* Improved module module
* Improved watchdog module
* Improved database abstraction layer
* Improved themes:
+ W3C validation on a best effort basis
+ Added theme "example" (alias "Stone Age")
* Added new scripts to directory "scripts"
* Added directory "misc"
* Added CREDITS file
- Revised documentation
Drupal 1.0.0, 2001-01-15
------------------------
- Initial release
All Drupal code is Copyright 2001 - 2013 by the original authors.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
along with this program as the file LICENSE.txt; if not, please see
http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
Drupal is a registered trademark of Dries Buytaert.
Drupal includes works under other copyright notices and distributed
according to the terms of the GNU General Public License or a compatible
license, including:
Javascript
Farbtastic - Copyright (c) 2010 Matt Farina
jQuery - Copyright (c) 2010 John Resig
jQuery BBQ - Copyright (c) 2010 "Cowboy" Ben Alman
jQuery Cookie - Copyright (c) 2006 Klaus Hartl
jQuery Form - Copyright (c) 2010 Mike Alsup
jQuery Once - Copyright (c) 2009 Konstantin K�fer
jQuery UI - Copyright (c) 2010 by the original authors
(http://jqueryui.com/about)
Sizzle.js - Copyright (c) 2010 The Dojo Foundation (http://sizzlejs.com/)
PHP
ArchiveTar - Copyright (c) 1997 - 2008 Vincent Blavet
CREATE THE MySQL DATABASE
--------------------------
This step is only necessary if you don't already have a database set up (e.g.,
by your host). In the following examples, 'username' is an example MySQL user
which has the CREATE and GRANT privileges. Use the appropriate user name for
your system.
First, you must create a new database for your Drupal site (here, 'databasename'
is the name of the new database):
mysqladmin -u username -p create databasename
MySQL will prompt for the 'username' database password and then create the
initial database files. Next you must log in and set the access database rights:
mysql -u username -p
Again, you will be asked for the 'username' database password. At the MySQL
prompt, enter the following command:
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER,
CREATE TEMPORARY TABLES ON databasename.*
TO 'username'@'localhost' IDENTIFIED BY 'password';
where:
'databasename' is the name of your database
'username' is the username of your MySQL account
'localhost' is the web server host where Drupal is installed
'password' is the password required for that username
Note: Unless the database user/host combination for your Drupal installation
has all of the privileges listed above (except possibly CREATE TEMPORARY TABLES,
which is currently only used by Drupal core automated tests and some
contributed modules), you will not be able to install or run Drupal.
If successful, MySQL will reply with:
Query OK, 0 rows affected
If the InnoDB storage engine is available, it will be used for all database
tables. InnoDB provides features over MyISAM such as transaction support,
row-level locks, and consistent non-locking reads.
CREATE THE PostgreSQL DATABASE
------------------------------
Note that the database must be created with UTF-8 (Unicode) encoding.
1. CREATE DATABASE USER
This step is only necessary if you don't already have a user set up (e.g., by
your host), or want to create a new user for use with Drupal only. The
following command creates a new user named 'username' and asks for a password
for that user:
createuser --pwprompt --encrypted --no-createrole --no-createdb username
If there are no errors, then the command was successful.
2. CREATE DRUPAL DATABASE
This step is only necessary if you don't already have a database set up
(e.g., by your host) or want to create a new database for use with Drupal
only. The following command creates a new database named 'databasename',
which is owned by the previously created 'username':
createdb --encoding=UTF8 --owner=username databasename
If there are no errors, then the command was successful.
3. CREATE SCHEMA OR SCHEMAS (Optional advanced step)
Drupal will run across different schemas within your database if you so wish.
By default, Drupal runs inside the 'public' schema but you can use $db_prefix
inside settings.php to define a schema for Drupal to run inside of, or
specify tables that are shared inside of a separate schema. Drupal will not
create schemas for you. In fact, the user that Drupal runs as should not be
allowed to do this. You'll need to execute the SQL below as a superuser,
replace 'username' with the username that Drupal uses to connect to
PostgreSQL, and replace 'schema_name' with a schema name you wish to use,
such as 'shared':
CREATE SCHEMA schema_name AUTHORIZATION username;
Do this for as many schemas as you need. See default.settings.php for
instructions on how to set which tables use which schemas.
SQLITE REQUIREMENTS
-------------------
To use SQLite with your Drupal installation, the following requirements must be
met: Server has PHP 5.3 or later with PDO, and the PDO SQLite driver must be
enabled.
SQLITE DATABASE CREATION
------------------------
The Drupal installer will create the SQLite database for you. The only
requirement is that the installer must have write permissions to the directory
where the database file resides. This directory (not just the database file) also
has to remain writeable by the web server going forward for SQLite to continue to
be able to operate.
On the "Database configuration" form in the "Database file" field, you must
supply the exact path to where you wish your database file to reside. It is
strongly suggested that you choose a path that is outside of the webroot, yet
ensure that the directory is writeable by the web server.
If you must place your database file in your webroot, you could try using the
following in your "Database file" field:
sites/default/files/.ht.sqlite
Note: The .ht in the name will tell Apache to prevent the database from being
downloaded. Please check that the file is, indeed, protected by your webserver.
If not, please consult the documentation of your webserver on how to protect a
file from downloading.
CONTENTS OF THIS FILE
---------------------
* Requirements and notes
* Optional server requirements
* Installation
* Building and customizing your site
* Multisite configuration
* More information
REQUIREMENTS AND NOTES
----------------------
Drupal requires:
- A web server. Apache (version 2.0 or greater) is recommended.
- PHP 5.3.3 (at least, PHP 7.x or greater recommended) (https://www.php.net/).
- One of the following databases:
- MySQL 5.5 (or greater) (https://www.mysql.com/) or equivalent versions of a
compatible database such as MariaDB or Percona.
- PostgreSQL 9.5 (or greater) (https://www.postgresql.org/).
- SQLite 3.27 (or greater) (https://www.sqlite.org/).
Note that version numbers above represent the minimum versions that Drupal 7 is
routinely tested with. For more detailed information about compatibility with
newer versions (that benefit from support from their maintainers), and
requirements including a list of PHP extensions and configurations that are
required, see "System requirements"
(https://www.drupal.org/docs/7/system-requirements) in the Drupal.org online
documentation.
For detailed information on how to configure a test server environment using a
variety of operating systems and web servers, see "Local server setup"
(http://drupal.org/node/157602) in the Drupal.org online documentation.
Note that all directories mentioned in this document are always relative to the
directory of your Drupal installation, and commands are meant to be run from
this directory (except for the initial commands that create that directory).
OPTIONAL SERVER REQUIREMENTS
----------------------------
- If you want to use Drupal's "Clean URLs" feature on an Apache web server, you
will need the mod_rewrite module and the ability to use local .htaccess
files. For Clean URLs support on IIS, see "Clean URLs with IIS"
(http://drupal.org/node/3854) in the Drupal.org online documentation.
- If you plan to use XML-based services such as RSS aggregation, you will need
PHP's XML extension. This extension is enabled by default on most PHP
installations.
- To serve gzip compressed CSS and JS files on an Apache web server, you will
need the mod_headers module and the ability to use local .htaccess files.
- Some Drupal functionality (e.g., checking whether Drupal and contributed
modules need updates, RSS aggregation, etc.) require that the web server be
able to go out to the web and download information. If you want to use this
functionality, you need to verify that your hosting provider or server
configuration allows the web server to initiate outbound connections. Most web
hosting setups allow this.
INSTALLATION
------------
1. Download and extract Drupal.
You can obtain the latest Drupal release from http://drupal.org -- the files
are available in .tar.gz and .zip formats and can be extracted using most
compression tools.
To download and extract the files, on a typical Unix/Linux command line, use
the following commands (assuming you want version x.y of Drupal in .tar.gz
format):
wget http://drupal.org/files/projects/drupal-x.y.tar.gz
tar -zxvf drupal-x.y.tar.gz
This will create a new directory drupal-x.y/ containing all Drupal files and
directories. Then, to move the contents of that directory into a directory
within your web server's document root or your public HTML directory,
continue with this command:
mv drupal-x.y/* drupal-x.y/.htaccess /path/to/your/installation
2. Optionally, download a translation.
By default, Drupal is installed in English, and further languages may be
installed later. If you prefer to install Drupal in another language
initially:
- Download a translation file for the correct Drupal version and language
from the translation server: http://localize.drupal.org/translate/downloads
- Place the file into your installation profile's translations directory.
For instance, if you are using the Standard installation profile,
move the .po file into the directory:
profiles/standard/translations/
For detailed instructions, visit http://drupal.org/localize
3. Create the Drupal database.
Because Drupal stores all site information in a database, you must create
this database in order to install Drupal, and grant Drupal certain database
privileges (such as the ability to create tables). For details, consult
INSTALL.mysql.txt, INSTALL.pgsql.txt, or INSTALL.sqlite.txt. You may also
need to consult your web hosting provider for instructions specific to your
web host.
Take note of the username, password, database name, and hostname as you
create the database. You will enter this information during the install.
4. Run the install script.
To run the install script, point your browser to the base URL of your
website (e.g., http://www.example.com).
You will be guided through several screens to set up the database, add the
site maintenance account (the first user, also known as user/1), and provide
basic web site settings.
During installation, several files and directories need to be created, which
the install script will try to do automatically. However, on some hosting
environments, manual steps are required, and the install script will tell
you that it cannot proceed until you fix certain issues. This is normal and
does not indicate a problem with your server.
The most common steps you may need to perform are:
a. Missing files directory.
The install script will attempt to create a file storage directory in
the default location at sites/default/files (the location of the files
directory may be changed after Drupal is installed).
If auto-creation fails, you can make it work by changing permissions on
the sites/default directory so that the web server can create the files
directory within it for you. (If you are creating a multisite
installation, substitute the correct sites directory for sites/default;
see the Multisite Configuration section of this file, below.)
For example, on a Unix/Linux command line, you can grant everyone
(including the web server) permission to write to the sites/default
directory with this command:
chmod a+w sites/default
Be sure to set the permissions back after the installation is finished!
Sample command:
chmod go-w sites/default
Alternatively, instead of allowing the web server to create the files
directory for you as described above, you can create it yourself. Sample
commands from a Unix/Linux command line:
mkdir sites/default/files
chmod a+w sites/default/files
b. Missing settings file.
Drupal will try to automatically create a settings.php configuration file,
which is normally in the directory sites/default (to avoid problems when
upgrading, Drupal is not packaged with this file). If auto-creation fails,
you will need to create this file yourself, using the file
sites/default/default.settings.php as a template.
For example, on a Unix/Linux command line, you can make a copy of the
default.settings.php file with the command:
cp sites/default/default.settings.php sites/default/settings.php
Next, grant write privileges to the file to everyone (including the web
server) with the command:
chmod a+w sites/default/settings.php
Be sure to set the permissions back after the installation is finished!
Sample command:
chmod go-w sites/default/settings.php
c. Write permissions after install.
The install script will attempt to write-protect the settings.php file and
the sites/default directory after saving your configuration. If this
fails, you will be notified, and you can do it manually. Sample commands
from a Unix/Linux command line:
chmod go-w sites/default/settings.php
chmod go-w sites/default
5. Verify that the site is working.
When the install script finishes, you will be logged in with the site
maintenance account on a "Welcome" page. If the default Drupal theme is not
displaying properly and links on the page result in "Page Not Found" errors,
you may be experiencing problems with clean URLs. Visit
http://drupal.org/getting-started/clean-urls to troubleshoot.
6. Change file system storage settings (optional).
The files directory created in step 4 is the default file system path used to
store all uploaded files, as well as some temporary files created by
Drupal. After installation, you can modify the file system path to store
uploaded files in a different location.
It is not necessary to modify this path, but you may wish to change it if:
- Your site runs multiple Drupal installations from a single codebase (modify
the file system path of each installation to a different directory so that
uploads do not overlap between installations).
- Your site runs on a number of web servers behind a load balancer or reverse
proxy (modify the file system path on each server to point to a shared file
repository).
- You want to restrict access to uploaded files.
To modify the file system path:
a. Ensure that the new location for the path exists and is writable by the
web server. For example, to create a new directory named uploads and grant
write permissions, use the following commands on a Unix/Linux command
line:
mkdir uploads
chmod a+w uploads
b. Navigate to Administration > Configuration > Media > File system, and
enter the desired path. Note that if you want to use private file storage,
you need to first enter the path for private files and save the
configuration, and then change the "Default download method" setting and
save again.
Changing the file system path after files have been uploaded may cause
unexpected problems on an existing site. If you modify the file system path
on an existing site, remember to copy all files from the original location
to the new location.
7. Revoke documentation file permissions (optional).
Some administrators suggest making the documentation files, especially
CHANGELOG.txt, non-readable so that the exact version of Drupal you are
running is slightly more difficult to determine. If you wish to implement
this optional security measure, from a Unix/Linux command line you can use
the following command:
chmod a-r CHANGELOG.txt
Note that the example only affects CHANGELOG.txt. To completely hide all
documentation files from public view, repeat this command for each of the
Drupal documentation files in the installation directory, substituting the
name of each file for CHANGELOG.txt in the example.
For more information on setting file permissions, see "Modifying Linux,
Unix, and Mac file permissions" (http://drupal.org/node/202483) or
"Modifying Windows file permissions" (http://drupal.org/node/202491) in the
Drupal.org online documentation.
8. Set up independent "cron" maintenance jobs.
Many Drupal modules have tasks that must be run periodically, including the
Search module (building and updating the index used for keyword searching),
the Aggregator module (retrieving feeds from other sites), and the System
module (performing routine maintenance and pruning of database tables). These
tasks are known as "cron maintenance tasks", named after the Unix/Linux
"cron" utility.
When you install Drupal, its built-in cron feature is enabled, which
automatically runs the cron tasks periodically, triggered by people visiting
pages of your site. You can configure the built-in cron feature by navigating
to Administration > Configuration > System > Cron.
It is also possible to run the cron tasks independent of site visits; this is
recommended for most sites. To do this, you will need to set up an automated
process to visit the page cron.php on your site, which executes the cron
tasks.
The URL of the cron.php page requires a "cron key" to protect against
unauthorized access. Your site's cron key is automatically generated during
installation and is specific to your site. The full URL of the page, with the
cron key, is available in the "Cron maintenance tasks" section of the Status
report page at Administration > Reports > Status report.
As an example for how to set up this automated process, you can use the
crontab utility on Unix/Linux systems. The following crontab line uses the
wget command to visit the cron.php page, and runs each hour, on the hour:
0 * * * * wget -O - -q -t 1 http://example.com/cron.php?cron_key=YOURKEY
Replace the text "http://example.com/cron.php?cron_key=YOURKEY" in the
example with the full URL displayed under "Cron maintenance tasks" on the
"Status report" page.
More information about cron maintenance tasks is available at
http://drupal.org/cron, and sample cron shell scripts can be found in the
scripts/ directory. (Note that these scripts must be customized like the
above example, to add your site-specific cron key and domain name.)
BUILDING AND CUSTOMIZING YOUR SITE
----------------------------------
A new installation of Drupal defaults to a very basic configuration. To extend
your site, you use "modules" and "themes". A module is a plugin that adds
functionality to Drupal, while a theme changes the look of your site. The core
of Drupal provides several optional modules and themes, and you can download
more at http://drupal.org/project/modules and http://drupal.org/project/themes
Do not mix downloaded or custom modules and themes with Drupal's core modules
and themes. Drupal's modules and themes are located in the top-level modules and
themes directories, while the modules and themes you add to Drupal are normally
placed in the sites/all/modules and sites/all/themes directories. If you run a
multisite installation, you can also place modules and themes in the
site-specific directories -- see the Multisite Configuration section, below.
Never edit Drupal's core modules and themes; instead, use the hooks available in
the Drupal API. To modify the behavior of Drupal, develop a module as described
at http://drupal.org/developing/modules. To modify the look of Drupal, create a
subtheme as described at http://drupal.org/node/225125, or a completely new
theme as described at http://drupal.org/documentation/theme
MULTISITE CONFIGURATION
-----------------------
A single Drupal installation can host several Drupal-powered sites, each with
its own individual configuration.
Additional site configurations are created in subdirectories within the 'sites'
directory. Each subdirectory must have a 'settings.php' file, which specifies
the configuration settings. The easiest way to create additional sites is to
copy the 'default' directory and modify the 'settings.php' file as appropriate.
The new directory name is constructed from the site's URL. The configuration for
www.example.com could be in 'sites/example.com/settings.php' (note that 'www.'
should be omitted if users can access your site at http://example.com/).
Sites do not have to have a different domain. You can also use subdomains and
subdirectories for Drupal sites. For example, example.com, sub.example.com, and
sub.example.com/site3 can all be defined as independent Drupal sites. The setup
for a configuration such as this would look like the following:
sites/default/settings.php
sites/example.com/settings.php
sites/sub.example.com/settings.php
sites/sub.example.com.site3/settings.php
When searching for a site configuration (for example www.sub.example.com/site3),
Drupal will search for configuration files in the following order, using the
first configuration it finds:
sites/www.sub.example.com.site3/settings.php
sites/sub.example.com.site3/settings.php
sites/example.com.site3/settings.php
sites/www.sub.example.com/settings.php
sites/sub.example.com/settings.php
sites/example.com/settings.php
sites/default/settings.php
If you are installing on a non-standard port, the port number is treated as the
deepest subdomain. For example: http://www.example.com:8080/ could be loaded
from sites/8080.www.example.com/. The port number will be removed according to
the pattern above if no port-specific configuration is found, just like a real
subdomain.
Each site configuration can have its own site-specific modules and themes in
addition to those installed in the standard 'modules' and 'themes' directories.
To use site-specific modules or themes, simply create a 'modules' or 'themes'
directory within the site configuration directory. For example, if
sub.example.com has a custom theme and a custom module that should not be
accessible to other sites, the setup would look like this:
sites/sub.example.com/
settings.php
themes/custom_theme
modules/custom_module
NOTE: for more information about multiple virtual hosts or the configuration
settings, consult http://drupal.org/getting-started/6/install/multi-site
For more information on configuring Drupal's file system path in a multisite
configuration, see step 6 above.
MORE INFORMATION
----------------
- See the Drupal.org online documentation:
http://drupal.org/documentation
- For a list of security announcements, see the "Security advisories" page at
http://drupal.org/security (available as an RSS feed). This page also
describes how to subscribe to these announcements via e-mail.
- For information about the Drupal security process, or to find out how to
report a potential security issue to the Drupal security team, see the
"Security team" page at http://drupal.org/security-team
- For information about the wide range of available support options, visit
http://drupal.org and click on Community and Support in the top or bottom
navigation.
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
Drupal core is built and maintained by the Drupal project community. Everyone is
encouraged to submit issues and changes (patches) to improve Drupal, and to
contribute in other ways -- see https://www.drupal.org/contribute to find out
how.
Branch maintainers
------------------
The Drupal Core branch maintainers oversee the development of Drupal as a whole.
The branch maintainers for Drupal 7 are:
- Dries Buytaert 'dries' https://www.drupal.org/u/dries
- Fabian Franz 'Fabianx' https://www.drupal.org/u/fabianx
- Drew Webber 'mcdruid' https://www.drupal.org/u/mcdruid
- (provisional) Juraj Nemec 'poker10' https://www.drupal.org/u/poker10
Component maintainers
---------------------
The Drupal Core component maintainers oversee the development of Drupal
subsystems. See https://www.drupal.org/contribute/core-maintainers for more
information on their responsibilities, and to find out how to become a component
maintainer. Current component maintainers for Drupal 7:
Ajax system
- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
- Earl Miles 'merlinofchaos' https://www.drupal.org/u/merlinofchaos
Base system
- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
Batch system
- Yves Chedemois 'yched' https://www.drupal.org/u/yched
Cache system
- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
Cron system
- Derek Wright 'dww' https://www.drupal.org/u/dww
Database system
- ?
- MySQL driver
- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
- PostgreSQL driver
- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
- Josh Waihi 'fiasco' https://www.drupal.org/u/josh-waihi
- Sqlite driver
- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
Database update system
- Ashok Modi 'BTMash' https://www.drupal.org/u/btmash
Entity system
- Wolfgang Ziegler 'fago' https://www.drupal.org/u/fago
- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
- Franz Heinzmann 'Frando' https://www.drupal.org/u/frando
File system
- Andrew Morton 'drewish' https://www.drupal.org/u/drewish
- Aaron Winborn 'aaron' https://www.drupal.org/u/aaron
Form system
- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
- Wolfgang Ziegler 'fago' https://www.drupal.org/u/fago
- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
- Franz Heinzmann 'Frando' https://www.drupal.org/u/frando
Image system
- Andrew Morton 'drewish' https://www.drupal.org/u/drewish
- Nathan Haug 'quicksketch' https://www.drupal.org/u/quicksketch
Install system
- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein
JavaScript
- Théodore Biadala 'nod_' https://www.drupal.org/u/nod_
- Steve De Jonghe 'seutje' https://www.drupal.org/u/seutje
Language system
- Francesco Placella 'plach' https://www.drupal.org/u/plach
- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
Lock system
- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
Mail system
- ?
Markup
- Jacine Luisi 'Jacine' https://www.drupal.org/u/jacine
- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
Menu system
- Peter Wolanin 'pwolanin' https://www.drupal.org/u/pwolanin
Path system
- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
Render system
- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
- Franz Heinzmann 'Frando' https://www.drupal.org/u/frando
Theme system
- Earl Miles 'merlinofchaos' https://www.drupal.org/u/merlinofchaos
- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
- Joon Park 'dvessel' https://www.drupal.org/u/dvessel
- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
Token system
- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
XML-RPC system
- Frederic G. Marand 'fgm' https://www.drupal.org/u/fgm
Topic coordinators
------------------
Accessibility
- Everett Zufelt 'Everett Zufelt' https://www.drupal.org/u/everett-zufelt
- Brandon Bowersox-Johnson 'bowersox' https://www.drupal.org/u/bowersox
Documentation
- Jennifer Hodgdon 'jhodgdon' https://www.drupal.org/u/jhodgdon
Translations
- Gerhard Killesreiter 'killes' https://www.drupal.org/u/gerhard-killesreiter
User experience and usability
- Roy Scholten 'yoroy' https://www.drupal.org/u/yoroy
- Bojhan Somers 'Bojhan' https://www.drupal.org/u/bojhan
Node Access
- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
- Ken Rickard 'agentrickard' https://www.drupal.org/u/agentrickard
Security team
-----------------
To report a security issue, see: https://www.drupal.org/security-team/report-issue
The Drupal security team provides Security Advisories for vulnerabilities,
assists developers in resolving security issues, and provides security
documentation. See https://www.drupal.org/security-team for more information.
The security team lead is:
- Michael Hess 'mlhess' https://www.drupal.org/u/mlhess
Module maintainers
------------------
Aggregator module
- ?
Block module
- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
Blog module
- ?
Book module
- Peter Wolanin 'pwolanin' https://www.drupal.org/u/pwolanin
Color module
- ?
Comment module
- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
Contact module
- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
Contextual module
- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
Dashboard module
- ?
Database logging module
- Khalid Baheyeldin 'kbahey' https://www.drupal.org/u/kbahey
Field module
- Yves Chedemois 'yched' https://www.drupal.org/u/yched
- Barry Jaspan 'bjaspan' https://www.drupal.org/u/bjaspan
Field UI module
- Yves Chedemois 'yched' https://www.drupal.org/u/yched
File module
- Aaron Winborn 'aaron' https://www.drupal.org/u/aaron
Filter module
- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
Forum module
- Lee Rowlands 'larowlan' https://www.drupal.org/u/larowlan
Help module
- ?
Image module
- Nathan Haug 'quicksketch' https://www.drupal.org/u/quicksketch
Locale module
- Gábor Hojtsy 'Gábor Hojtsy' https://www.drupal.org/u/gábor-hojtsy
Menu module
- ?
Node module
- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
OpenID module
- Vojtech Kusy 'wojtha' https://www.drupal.org/u/wojtha
- Christian Schmidt 'c960657' https://www.drupal.org/u/c960657
- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
Overlay module
- Katherine Senzee 'ksenzee' https://www.drupal.org/u/ksenzee
Path module
- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
PHP module
- ?
Poll module
- Andrei Mateescu 'amateescu' https://www.drupal.org/u/amateescu
Profile module
- ?
RDF module
- Stéphane Corlosquet 'scor' https://www.drupal.org/u/scor
Search module
- Doug Green 'douggreen' https://www.drupal.org/u/douggreen
Shortcut module
- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein
Simpletest module
- Jimmy Berry 'boombatower' https://www.drupal.org/u/boombatower
Statistics module
- Tim Millwood 'timmillwood' https://www.drupal.org/u/timmillwood
Syslog module
- Khalid Baheyeldin 'kbahey' https://www.drupal.org/u/kbahey
System module
- ?
Taxonomy module
- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
- Benjamin Doherty 'bangpound' https://www.drupal.org/u/bangpound
Toolbar module
- ?
Tracker module
- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
Translation module
- Francesco Placella 'plach' https://www.drupal.org/u/plach
Trigger module
- ?
Update module
- Derek Wright 'dww' https://www.drupal.org/u/dww
User module
- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
Theme maintainers
-----------------
Bartik theme
- Jen Simmons 'jensimmons' https://www.drupal.org/u/jensimmons
- Jeff Burns 'Jeff Burnz' https://www.drupal.org/u/jeff-burnz
Garland theme
- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
Seven theme
- Jeff Burns 'Jeff Burnz' https://www.drupal.org/u/jeff-burnz
Stark theme
- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
CONTENTS OF THIS FILE
---------------------
* About Drupal
* Configuration and features
* Installation profiles
* Appearance
* Developing for Drupal
ABOUT DRUPAL
------------
Drupal is an open source content management platform supporting a variety of
websites ranging from personal weblogs to large community-driven websites. For
more information, see the Drupal website at http://drupal.org/, and join the
Drupal community at http://drupal.org/community.
Legal information about Drupal:
* Know your rights when using Drupal:
See LICENSE.txt in the same directory as this document.
* Learn about the Drupal trademark and logo policy:
http://drupal.com/trademark
CONFIGURATION AND FEATURES
--------------------------
Drupal core (what you get when you download and extract a drupal-x.y.tar.gz or
drupal-x.y.zip file from http://drupal.org/project/drupal) has what you need to
get started with your website. It includes several modules (extensions that add
functionality) for common website features, such as managing content, user
accounts, image uploading, and search. Core comes with many options that allow
site-specific configuration. In addition to the core modules, there are
thousands of contributed modules (for functionality not included with Drupal
core) available for download.
More about configuration:
* Install, upgrade, and maintain Drupal:
See INSTALL.txt and UPGRADE.txt in the same directory as this document.
* Learn about how to use Drupal to create your site:
http://drupal.org/documentation
* Download contributed modules to sites/all/modules to extend Drupal's
functionality:
http://drupal.org/project/modules
* See also: "Developing for Drupal" for writing your own modules, below.
INSTALLATION PROFILES
---------------------
Installation profiles define additional steps (such as enabling modules,
defining content types, etc.) that run after the base installation provided
by core when Drupal is first installed. There are two basic installation
profiles provided with Drupal core.
Installation profiles from the Drupal community modify the installation process
to provide a website for a specific use case, such as a CMS for media
publishers, a web-based project tracking tool, or a full-fledged CRM for
non-profit organizations raising money and accepting donations. They can be
distributed as bare installation profiles or as "distributions". Distributions
include Drupal core, the installation profile, and all other required
extensions, such as contributed and custom modules, themes, and third-party
libraries. Bare installation profiles require you to download Drupal Core and
the required extensions separately; place the downloaded profile in the
/profiles directory before you start the installation process. Note that the
contents of this directory may be overwritten during updates of Drupal core;
it is advised to keep code backups or use a version control system.
Additionally, modules and themes may be placed inside subdirectories in a
specific installation profile such as profiles/your_site_profile/modules and
profiles/your_site_profile/themes respectively to restrict their usage to only
sites that were installed with that specific profile.
More about installation profiles and distributions:
* Read about the difference between installation profiles and distributions:
http://drupal.org/node/1089736
* Download contributed installation profiles and distributions:
http://drupal.org/project/distributions
* Develop your own installation profile or distribution:
http://drupal.org/developing/distributions
APPEARANCE
----------
In Drupal, the appearance of your site is set by the theme (themes are
extensions that set fonts, colors, and layout). Drupal core comes with several
themes. More themes are available for download, and you can also create your own
custom theme.
More about themes:
* Download contributed themes to sites/all/themes to modify Drupal's
appearance:
http://drupal.org/project/themes
* Develop your own theme:
http://drupal.org/documentation/theme
DEVELOPING FOR DRUPAL
---------------------
Drupal contains an extensive API that allows you to add to and modify the
functionality of your site. The API consists of "hooks", which allow modules to
react to system events and customize Drupal's behavior, and functions that
standardize common operations such as database queries and form generation. The
flexible hook architecture means that you should never need to directly modify
the files that come with Drupal core to achieve the functionality you want;
instead, functionality modifications take the form of modules.
When you need new functionality for your Drupal site, search for existing
contributed modules. If you find a module that matches except for a bug or an
additional needed feature, change the module and contribute your improvements
back to the project in the form of a "patch". Create new custom modules only
when nothing existing comes close to what you need.
More about developing:
* Search for existing contributed modules:
http://drupal.org/project/modules
* Contribute a patch:
http://drupal.org/patch/submit
* Develop your own module:
http://drupal.org/developing/modules
* Follow best practices:
http://drupal.org/best-practices
* Refer to the API documentation:
http://api.drupal.org/api/drupal/7
INTRODUCTION
------------
This document describes how to:
* Update your Drupal site from one minor 7.x version to another minor 7.x
version; for example, from 7.8 to 7.9, or from 7.6 to 7.10.
* Upgrade your Drupal site's major version from 6.x to 7.x.
First steps and definitions:
* If you are upgrading to Drupal version x.y, then x is known as the major
version number, and y is known as the minor version number. The download
file will be named drupal-x.y.tar.gz (or drupal-x.y.zip).
* All directories mentioned in this document are relative to the directory of
your Drupal installation.
* Make a full backup of all files, directories, and your database(s) before
starting, and save it outside your Drupal installation directory.
Instructions may be found at http://drupal.org/upgrade/backing-up-the-db
* It is wise to try an update or upgrade on a test copy of your site before
applying it to your live site. Even minor updates can cause your site's
behavior to change.
* Each new release of Drupal has release notes, which explain the changes made
since the previous version and any special instructions needed to update or
upgrade to the new version. You can find a link to the release notes for the
version you are upgrading or updating to on the Drupal project page
(http://drupal.org/project/drupal).
UPGRADE PROBLEMS
----------------
If you encounter errors during this process,
* Note any error messages you see.
* Restore your site to its previous state, using the file and database backups
you created before you started the upgrade process. Do not attempt to do
further upgrades on a site that had update problems.
* Consult one of the support options listed on http://drupal.org/support
More in-depth information on upgrading can be found at http://drupal.org/upgrade
MINOR VERSION UPDATES
---------------------
To update from one minor 7.x version of Drupal to any later 7.x version, after
following the instructions in the INTRODUCTION section at the top of this file:
1. Log in as a user with the permission "Administer software updates".
2. Go to Administration > Configuration > Development > Maintenance mode.
Enable the "Put site into maintenance mode" checkbox and save the
configuration.
3. Remove all old core files and directories, except for the 'sites' directory
and any custom files you added elsewhere.
If you made modifications to files like .htaccess or robots.txt, you will
need to re-apply them from your backup, after the new files are in place.
Sometimes an update includes changes to default.settings.php (this will be
noted in the release notes). If that's the case, follow these steps:
- Locate your settings.php file in the /sites/* directory. (Typically
sites/default.)
- Make a backup copy of your settings.php file, with a different file name.
- Make a copy of the new default.settings.php file, and name the copy
settings.php (overwriting your previous settings.php file).
- Copy the custom and site-specific entries from the backup you made into the
new settings.php file. You will definitely need the lines giving the
database information, and you will also want to copy in any other
customizations you have added.
You can find the release notes for your version at
https://www.drupal.org/project/drupal. At bottom of the project page under
"Downloads" use the link for your version of Drupal to view the release
notes. If your version is not listed, use the 'View all releases' link. From
this page you can scroll down or use the filter to find your version and its
release notes.
4. Download the latest Drupal 7.x release from http://drupal.org to a
directory outside of your web root. Extract the archive and copy the files
into your Drupal directory.
On a typical Unix/Linux command line, use the following commands to download
and extract:
wget http://drupal.org/files/projects/drupal-x.y.tar.gz
tar -zxvf drupal-x.y.tar.gz
This creates a new directory drupal-x.y/ containing all Drupal files and
directories. Copy the files into your Drupal installation directory:
cp -R drupal-x.y/* drupal-x.y/.htaccess /path/to/your/installation
If you do not have command line access to your server, download the archive
from http://drupal.org using your web browser, extract it, and then use an
FTP client to upload the files to your web root.
5. Re-apply any modifications to files such as .htaccess or robots.txt.
6. Run update.php by visiting http://www.example.com/update.php (replace
www.example.com with your domain name). This will update the core database
tables.
If you are unable to access update.php do the following:
- Open settings.php with a text editor.
- Find the line that says:
$update_free_access = FALSE;
- Change it into:
$update_free_access = TRUE;
- Once the upgrade is done, $update_free_access must be reverted to FALSE.
7. Go to Administration > Reports > Status report. Verify that everything is
working as expected.
8. Ensure that $update_free_access is FALSE in settings.php.
9. Go to Administration > Configuration > Development > Maintenance mode.
Disable the "Put site into maintenance mode" checkbox and save the
configuration.
MAJOR VERSION UPGRADE
---------------------
To upgrade from a previous major version of Drupal to Drupal 7.x, after
following the instructions in the INTRODUCTION section at the top of this file:
1. Check on the Drupal 7 status of your contributed and custom modules and
themes. See http://drupal.org/node/948216 for information on upgrading
contributed modules and themes. See http://drupal.org/node/895314 for a list
of modules that have been moved into core for Drupal 7, and instructions on
how to update them. See http://drupal.org/update/modules for information on
how to update your custom modules, and http://drupal.org/update/theme for
custom themes.
You may decide at this point that you cannot upgrade your site, because
needed modules or themes are not ready for Drupal 7.
2. Update to the latest available version of Drupal 6.x (if your current version
is Drupal 5.x, you have to upgrade to 6.x first). If you need to update,
download Drupal 6.x and follow the instructions in its UPGRADE.txt. This
document only applies for upgrades from 6.x to 7.x.
3. In addition to updating to the latest available version of Drupal 6.x core,
you must also upgrade all of your contributed modules for Drupal to their
latest Drupal 6.x versions.
4. Log in as user ID 1 (the site maintenance user).
5. Go to Administer > Site configuration > Site maintenance. Select
"Off-line" and save the configuration.
6. Go to Administer > Site building > Themes. Enable "Garland" and select it as
the default theme.
7. Go to Administer > Site building > Modules. Disable all modules that are not
listed under "Core - required" or "Core - optional". It is possible that some
modules cannot be disabled, because others depend on them. Repeat this step
until all non-core modules are disabled.
If you know that you will not re-enable some modules for Drupal 7.x and you
no longer need their data, then you can uninstall them under the Uninstall
tab after disabling them.
8. On the command line or in your FTP client, remove the file
sites/default/default.settings.php
9. Remove all old core files and directories, except for the 'sites' directory
and any custom files you added elsewhere.
If you made modifications to files like .htaccess or robots.txt, you will
need to re-apply them from your backup, after the new files are in place.
10. If you uninstalled any modules, remove them from the sites/all/modules and
other sites/*/modules directories. Leave other modules in place, even though
they are incompatible with Drupal 7.x.
11. Download the latest Drupal 7.x release from http://drupal.org to a
directory outside of your web root. Extract the archive and copy the files
into your Drupal directory.
On a typical Unix/Linux command line, use the following commands to download
and extract:
wget http://drupal.org/files/projects/drupal-x.y.tar.gz
tar -zxvf drupal-x.y.tar.gz
This creates a new directory drupal-x.y/ containing all Drupal files and
directories. Copy the files into your Drupal installation directory:
cp -R drupal-x.y/* drupal-x.y/.htaccess /path/to/your/installation
If you do not have command line access to your server, download the archive
from http://drupal.org using your web browser, extract it, and then use an
FTP client to upload the files to your web root.
12. Re-apply any modifications to files such as .htaccess or robots.txt.
13. Make your settings.php file writeable, so that the update process can
convert it to the format of Drupal 7.x. settings.php is usually located in
sites/default/settings.php
14. Run update.php by visiting http://www.example.com/update.php (replace
www.example.com with your domain name). This will update the core database
tables.
If you are unable to access update.php do the following:
- Open settings.php with a text editor.
- Find the line that says:
$update_free_access = FALSE;
- Change it into:
$update_free_access = TRUE;
- Once the upgrade is done, $update_free_access must be reverted to FALSE.
15. Backup your database after the core upgrade has run.
16. Replace and update your non-core modules and themes, following the
procedures at http://drupal.org/node/948216
17. Go to Administration > Reports > Status report. Verify that everything is
working as expected.
18. Ensure that $update_free_access is FALSE in settings.php.
19. Go to Administration > Configuration > Development > Maintenance mode.
Disable the "Put site into maintenance mode" checkbox and save the
configuration.
To get started with Drupal 7 administration, visit
http://drupal.org/getting-started/7/admin
$results['messages']));
$links = array();
if (is_array($results['tasks'])) {
$links += $results['tasks'];
}
else {
$links = array_merge($links, array(
l(t('Administration pages'), 'admin'),
l(t('Front page'), ''),
));
}
$output .= theme('item_list', array('items' => $links, 'title' => t('Next steps')));
}
// If a batch is running, let it run.
elseif (isset($_GET['batch'])) {
$output = _batch_page();
}
else {
if (empty($_SESSION['authorize_operation']) || empty($_SESSION['authorize_filetransfer_info'])) {
$output = t('It appears you have reached this page in error.');
}
elseif (!$batch = batch_get()) {
// We have a batch to process, show the filetransfer form.
$elements = drupal_get_form('authorize_filetransfer_form');
$output = drupal_render($elements);
}
}
// We defer the display of messages until all operations are done.
$show_messages = !(($batch = batch_get()) && isset($batch['running']));
}
else {
$output = authorize_access_denied_page();
}
if (!empty($output)) {
print theme('update_page', array('content' => $output, 'show_messages' => $show_messages));
}
variable_get('actions_max_stack', 35)) {
watchdog('actions', 'Stack overflow: too many calls to actions_do(). Aborting to prevent infinite recursion.', array(), WATCHDOG_ERROR);
return;
}
$actions = array();
$available_actions = actions_list();
$actions_result = array();
if (is_array($action_ids)) {
$conditions = array();
foreach ($action_ids as $action_id) {
if (is_numeric($action_id)) {
$conditions[] = $action_id;
}
elseif (isset($available_actions[$action_id])) {
$actions[$action_id] = $available_actions[$action_id];
}
}
// When we have action instances we must go to the database to retrieve
// instance data.
if (!empty($conditions)) {
$query = db_select('actions');
$query->addField('actions', 'aid');
$query->addField('actions', 'type');
$query->addField('actions', 'callback');
$query->addField('actions', 'parameters');
$query->condition('aid', $conditions, 'IN');
$result = $query->execute();
foreach ($result as $action) {
$actions[$action->aid] = $action->parameters ? unserialize($action->parameters) : array();
$actions[$action->aid]['callback'] = $action->callback;
$actions[$action->aid]['type'] = $action->type;
}
}
// Fire actions, in no particular order.
foreach ($actions as $action_id => $params) {
// Configurable actions need parameters.
if (is_numeric($action_id)) {
$function = $params['callback'];
if (function_exists($function)) {
$context = array_merge($context, $params);
$actions_result[$action_id] = $function($object, $context, $a1, $a2);
}
else {
$actions_result[$action_id] = FALSE;
}
}
// Singleton action; $action_id is the function name.
else {
$actions_result[$action_id] = $action_id($object, $context, $a1, $a2);
}
}
}
// Optimized execution of a single action.
else {
// If it's a configurable action, retrieve stored parameters.
if (is_numeric($action_ids)) {
$action = db_query("SELECT callback, parameters FROM {actions} WHERE aid = :aid", array(':aid' => $action_ids))->fetchObject();
$function = $action->callback;
if (function_exists($function)) {
$context = array_merge($context, unserialize($action->parameters));
$actions_result[$action_ids] = $function($object, $context, $a1, $a2);
}
else {
$actions_result[$action_ids] = FALSE;
}
}
// Singleton action; $action_ids is the function name.
else {
if (function_exists($action_ids)) {
$actions_result[$action_ids] = $action_ids($object, $context, $a1, $a2);
}
else {
// Set to avoid undefined index error messages later.
$actions_result[$action_ids] = FALSE;
}
}
}
$stack--;
return $actions_result;
}
/**
* Discovers all available actions by invoking hook_action_info().
*
* This function contrasts with actions_get_all_actions(); see the
* documentation of actions_get_all_actions() for an explanation.
*
* @param $reset
* Reset the action info static cache.
*
* @return
* An associative array keyed on action function name, with the same format
* as the return value of hook_action_info(), containing all
* modules' hook_action_info() return values as modified by any
* hook_action_info_alter() implementations.
*
* @see hook_action_info()
*/
function actions_list($reset = FALSE) {
$actions = &drupal_static(__FUNCTION__);
if (!isset($actions) || $reset) {
$actions = module_invoke_all('action_info');
drupal_alter('action_info', $actions);
}
// See module_implements() for an explanation of this cast.
return (array) $actions;
}
/**
* Retrieves all action instances from the database.
*
* This function differs from the actions_list() function, which gathers
* actions by invoking hook_action_info(). The actions returned by this
* function and the actions returned by actions_list() are partially
* synchronized. Non-configurable actions from hook_action_info()
* implementations are put into the database when actions_synchronize() is
* called, which happens when admin/config/system/actions is visited.
* Configurable actions are not added to the database until they are configured
* in the user interface, in which case a database row is created for each
* configuration of each action.
*
* @return
* Associative array keyed by numeric action ID. Each value is an associative
* array with keys 'callback', 'label', 'type' and 'configurable'.
*/
function actions_get_all_actions() {
$actions = db_query("SELECT aid, type, callback, parameters, label FROM {actions}")->fetchAllAssoc('aid', PDO::FETCH_ASSOC);
foreach ($actions as &$action) {
$action['configurable'] = (bool) $action['parameters'];
unset($action['parameters']);
unset($action['aid']);
}
return $actions;
}
/**
* Creates an associative array keyed by hashes of function names or IDs.
*
* Hashes are used to prevent actual function names from going out into HTML
* forms and coming back.
*
* @param $actions
* An associative array with function names or action IDs as keys
* and associative arrays with keys 'label', 'type', etc. as values.
* This is usually the output of actions_list() or actions_get_all_actions().
*
* @return
* An associative array whose keys are hashes of the input array keys, and
* whose corresponding values are associative arrays with components
* 'callback', 'label', 'type', and 'configurable' from the input array.
*/
function actions_actions_map($actions) {
$actions_map = array();
foreach ($actions as $callback => $array) {
$key = drupal_hash_base64($callback);
$actions_map[$key]['callback'] = isset($array['callback']) ? $array['callback'] : $callback;
$actions_map[$key]['label'] = $array['label'];
$actions_map[$key]['type'] = $array['type'];
$actions_map[$key]['configurable'] = $array['configurable'];
}
return $actions_map;
}
/**
* Returns an action array key (function or ID), given its hash.
*
* Faster than actions_actions_map() when you only need the function name or ID.
*
* @param $hash
* Hash of a function name or action ID array key. The array key
* is a key into the return value of actions_list() (array key is the action
* function name) or actions_get_all_actions() (array key is the action ID).
*
* @return
* The corresponding array key, or FALSE if no match is found.
*/
function actions_function_lookup($hash) {
// Check for a function name match.
$actions_list = actions_list();
foreach ($actions_list as $function => $array) {
if (drupal_hash_base64($function) == $hash) {
return $function;
}
}
$aid = FALSE;
// Must be a configurable action; check database.
$result = db_query("SELECT aid FROM {actions} WHERE parameters <> ''")->fetchAll(PDO::FETCH_ASSOC);
foreach ($result as $row) {
if (drupal_hash_base64($row['aid']) == $hash) {
$aid = $row['aid'];
break;
}
}
return $aid;
}
/**
* Synchronizes actions that are provided by modules in hook_action_info().
*
* Actions provided by modules in hook_action_info() implementations are
* synchronized with actions that are stored in the actions database table.
* This is necessary so that actions that do not require configuration can
* receive action IDs.
*
* @param $delete_orphans
* If TRUE, any actions that exist in the database but are no longer
* found in the code (for example, because the module that provides them has
* been disabled) will be deleted.
*/
function actions_synchronize($delete_orphans = FALSE) {
$actions_in_code = actions_list(TRUE);
$actions_in_db = db_query("SELECT aid, callback, label FROM {actions} WHERE parameters = ''")->fetchAllAssoc('callback', PDO::FETCH_ASSOC);
// Go through all the actions provided by modules.
foreach ($actions_in_code as $callback => $array) {
// Ignore configurable actions since their instances get put in when the
// user adds the action.
if (!$array['configurable']) {
// If we already have an action ID for this action, no need to assign aid.
if (isset($actions_in_db[$callback])) {
unset($actions_in_db[$callback]);
}
else {
// This is a new singleton that we don't have an aid for; assign one.
db_insert('actions')
->fields(array(
'aid' => $callback,
'type' => $array['type'],
'callback' => $callback,
'parameters' => '',
'label' => $array['label'],
))
->execute();
watchdog('actions', "Action '%action' added.", array('%action' => $array['label']));
}
}
}
// Any actions that we have left in $actions_in_db are orphaned.
if ($actions_in_db) {
$orphaned = array_keys($actions_in_db);
if ($delete_orphans) {
$actions = db_query('SELECT aid, label FROM {actions} WHERE callback IN (:orphaned)', array(':orphaned' => $orphaned))->fetchAll();
foreach ($actions as $action) {
actions_delete($action->aid);
watchdog('actions', "Removed orphaned action '%action' from database.", array('%action' => $action->label));
}
}
else {
$link = l(t('Remove orphaned actions'), 'admin/config/system/actions/orphan');
$count = count($actions_in_db);
$orphans = implode(', ', $orphaned);
watchdog('actions', '@count orphaned actions (%orphans) exist in the actions table. !link', array('@count' => $count, '%orphans' => $orphans, '!link' => $link), WATCHDOG_INFO);
}
}
}
/**
* Saves an action and its user-supplied parameter values to the database.
*
* @param $function
* The name of the function to be called when this action is performed.
* @param $type
* The type of action, to describe grouping and/or context, e.g., 'node',
* 'user', 'comment', or 'system'.
* @param $params
* An associative array with parameter names as keys and parameter values as
* values.
* @param $label
* A user-supplied label of this particular action, e.g., 'Send e-mail
* to Jim'.
* @param $aid
* The ID of this action. If omitted, a new action is created.
*
* @return
* The ID of the action.
*/
function actions_save($function, $type, $params, $label, $aid = NULL) {
// aid is the callback for singleton actions so we need to keep a separate
// table for numeric aids.
if (!$aid) {
$aid = db_next_id();
}
db_merge('actions')
->key(array('aid' => $aid))
->fields(array(
'callback' => $function,
'type' => $type,
'parameters' => serialize($params),
'label' => $label,
))
->execute();
watchdog('actions', 'Action %action saved.', array('%action' => $label));
return $aid;
}
/**
* Retrieves a single action from the database.
*
* @param $aid
* The ID of the action to retrieve.
*
* @return
* The appropriate action row from the database as an object.
*/
function actions_load($aid) {
return db_query("SELECT aid, type, callback, parameters, label FROM {actions} WHERE aid = :aid", array(':aid' => $aid))->fetchObject();
}
/**
* Deletes a single action from the database.
*
* @param $aid
* The ID of the action to delete.
*/
function actions_delete($aid) {
db_delete('actions')
->condition('aid', $aid)
->execute();
module_invoke_all('actions_delete', $aid);
}
'select',
* '#options' => array(
* 'one' => 'one',
* 'two' => 'two',
* 'three' => 'three',
* ),
* '#ajax' => array(
* 'callback' => 'ajax_example_simplest_callback',
* 'wrapper' => 'replace_textfield_div',
* ),
* );
* // This entire form element will be replaced with an updated value.
* $form['replace_textfield'] = array(
* '#type' => 'textfield',
* '#title' => t("The default value will be changed"),
* '#description' => t("Say something about why you chose") . "'" .
* (!empty($form_state['values']['changethis'])
* ? $form_state['values']['changethis'] : t("Not changed yet")) . "'",
* '#prefix' => '',
* '#suffix' => '
',
* );
* return $form;
* }
*
* function ajax_example_simplest_callback($form, $form_state) {
* // The form has already been submitted and updated. We can return the replaced
* // item as it is.
* return $form['replace_textfield'];
* }
* @endcode
*
* In the above example, the 'changethis' element is Ajax-enabled. The default
* #ajax['event'] is 'change', so when the 'changethis' element changes,
* an Ajax call is made. The form is submitted and reprocessed, and then the
* callback is called. In this case, the form has been automatically
* built changing $form['replace_textfield']['#description'], so the callback
* just returns that part of the form.
*
* To implement Ajax handling in a form, add '#ajax' to the form
* definition of a field. That field will trigger an Ajax event when it is
* clicked (or changed, depending on the kind of field). #ajax supports
* the following parameters (either 'path' or 'callback' is required at least):
* - #ajax['callback']: The callback to invoke to handle the server side of the
* Ajax event, which will receive a $form and $form_state as arguments, and
* returns a renderable array (most often a form or form fragment), an HTML
* string, or an array of Ajax commands. If returning a renderable array or
* a string, the value will replace the original element named in
* #ajax['wrapper'], and
* theme_status_messages()
* will be prepended to that
* element. (If the status messages are not wanted, return an array
* of Ajax commands instead.)
* #ajax['wrapper']. If an array of Ajax commands is returned, it will be
* executed by the calling code.
* - #ajax['path']: The menu path to use for the request. This is often omitted
* and the default is used. This path should map
* to a menu page callback that returns data using ajax_render(). Defaults to
* 'system/ajax', which invokes ajax_form_callback(), eventually calling
* the function named in #ajax['callback']. If you use a custom
* path, you must set up the menu entry and handle the entire callback in your
* own code.
* - #ajax['wrapper']: The CSS ID of the area to be replaced by the content
* returned by the #ajax['callback'] function. The content returned from
* the callback will replace the entire element named by #ajax['wrapper'].
* The wrapper is usually created using #prefix and #suffix properties in the
* form. Note that this is the wrapper ID, not a CSS selector. So to replace
* the element referred to by the CSS selector #some-selector on the page,
* use #ajax['wrapper'] = 'some-selector', not '#some-selector'.
* - #ajax['effect']: The jQuery effect to use when placing the new HTML.
* Defaults to no effect. Valid options are 'none', 'slide', or 'fade'.
* - #ajax['speed']: The effect speed to use. Defaults to 'slow'. May be
* 'slow', 'fast' or a number in milliseconds which represents the length
* of time the effect should run.
* - #ajax['event']: The JavaScript event to respond to. This is normally
* selected automatically for the type of form widget being used, and
* is only needed if you need to override the default behavior.
* - #ajax['prevent']: A JavaScript event to prevent when 'event' is triggered.
* Defaults to 'click' for #ajax on #type 'submit', 'button', and
* 'image_button'. Multiple events may be specified separated by spaces.
* For example, when binding #ajax behaviors to form buttons, pressing the
* ENTER key within a textfield triggers the 'click' event of the form's first
* submit button. Triggering Ajax in this situation leads to problems, like
* breaking autocomplete textfields. Because of that, Ajax behaviors are bound
* to the 'mousedown' event on form buttons by default. However, binding to
* 'mousedown' rather than 'click' means that it is possible to trigger a
* click by pressing the mouse, holding the mouse button down until the Ajax
* request is complete and the button is re-enabled, and then releasing the
* mouse button. For this case, 'prevent' can be set to 'click', so an
* additional event handler is bound to prevent such a click from triggering a
* non-Ajax form submission. This also prevents a textfield's ENTER press
* triggering a button's non-Ajax form submission behavior.
* - #ajax['method']: The jQuery method to use to place the new HTML.
* Defaults to 'replaceWith'. May be: 'replaceWith', 'append', 'prepend',
* 'before', 'after', or 'html'. See the
* @link http://api.jquery.com/category/manipulation/ jQuery manipulators documentation @endlink
* for more information on these methods.
* - #ajax['progress']: Choose either a throbber or progress bar that is
* displayed while awaiting a response from the callback, and add an optional
* message. Possible keys: 'type', 'message', 'url', 'interval'.
* More information is available in the
* @link forms_api_reference.html Form API Reference @endlink
*
* In addition to using Form API for doing in-form modification, Ajax may be
* enabled by adding classes to buttons and links. By adding the 'use-ajax'
* class to a link, the link will be loaded via an Ajax call. When using this
* method, the href of the link can contain '/nojs/' as part of the path. When
* the Ajax framework makes the request, it will convert this to '/ajax/'.
* The server is then able to easily tell if this request was made through an
* actual Ajax request or in a degraded state, and respond appropriately.
*
* Similarly, submit buttons can be given the class 'use-ajax-submit'. The
* form will then be submitted via Ajax to the path specified in the #action.
* Like the ajax-submit class above, this path will have '/nojs/' replaced with
* '/ajax/' so that the submit handler can tell if the form was submitted
* in a degraded state or not.
*
* When responding to Ajax requests, the server should do what it needs to do
* for that request, then create a commands array. This commands array will
* be converted to a JSON object and returned to the client, which will then
* iterate over the array and process it like a macro language.
*
* Each command item is an associative array which will be converted to a
* command object on the JavaScript side. $command_item['command'] is the type
* of command, e.g. 'alert' or 'replace', and will correspond to a method in the
* Drupal.ajax[command] space. The command array may contain any other data that
* the command needs to process, e.g. 'method', 'selector', 'settings', etc.
*
* Commands are usually created with a couple of helper functions, so they
* look like this:
* @code
* $commands = array();
* // Replace the content of '#object-1' on the page with 'some html here'.
* $commands[] = ajax_command_replace('#object-1', 'some html here');
* // Add a visual "changed" marker to the '#object-1' element.
* $commands[] = ajax_command_changed('#object-1');
* // Menu 'page callback' and #ajax['callback'] functions are supposed to
* // return render arrays. If returning an Ajax commands array, it must be
* // encapsulated in a render array structure.
* return array('#type' => 'ajax', '#commands' => $commands);
* @endcode
*
* When returning an Ajax command array, it is often useful to have
* status messages rendered along with other tasks in the command array.
* In that case the Ajax commands array may be constructed like this:
* @code
* $commands = array();
* $commands[] = ajax_command_replace(NULL, $output);
* $commands[] = ajax_command_prepend(NULL, theme('status_messages'));
* return array('#type' => 'ajax', '#commands' => $commands);
* @endcode
*
* See @link ajax_commands Ajax framework commands @endlink
*/
/**
* Renders a commands array into JSON.
*
* @param $commands
* A list of macro commands generated by the use of ajax_command_*()
* functions.
*/
function ajax_render($commands = array()) {
// Although ajax_deliver() does this, some contributed and custom modules
// render Ajax responses without using that delivery callback.
ajax_set_verification_header();
// Ajax responses aren't rendered with html.tpl.php, so we have to call
// drupal_get_css() and drupal_get_js() here, in order to have new files added
// during this request to be loaded by the page. We only want to send back
// files that the page hasn't already loaded, so we implement simple diffing
// logic using array_diff_key().
foreach (array('css', 'js') as $type) {
// It is highly suspicious if $_POST['ajax_page_state'][$type] is empty,
// since the base page ought to have at least one JS file and one CSS file
// loaded. It probably indicates an error, and rather than making the page
// reload all of the files, instead we return no new files.
if (empty($_POST['ajax_page_state'][$type])) {
$items[$type] = array();
}
else {
$function = 'drupal_add_' . $type;
$items[$type] = $function();
drupal_alter($type, $items[$type]);
// @todo Inline CSS and JS items are indexed numerically. These can't be
// reliably diffed with array_diff_key(), since the number can change
// due to factors unrelated to the inline content, so for now, we strip
// the inline items from Ajax responses, and can add support for them
// when drupal_add_css() and drupal_add_js() are changed to use a hash
// of the inline content as the array key.
foreach ($items[$type] as $key => $item) {
if (is_numeric($key)) {
unset($items[$type][$key]);
}
}
// Ensure that the page doesn't reload what it already has.
$items[$type] = array_diff_key($items[$type], $_POST['ajax_page_state'][$type]);
}
}
// Render the HTML to load these files, and add AJAX commands to insert this
// HTML in the page. We pass TRUE as the $skip_alter argument to prevent the
// data from being altered again, as we already altered it above. Settings are
// handled separately, afterwards.
if (isset($items['js']['settings'])) {
unset($items['js']['settings']);
}
$styles = drupal_get_css($items['css'], TRUE);
$scripts_footer = drupal_get_js('footer', $items['js'], TRUE);
$scripts_header = drupal_get_js('header', $items['js'], TRUE);
$extra_commands = array();
if (!empty($styles)) {
$extra_commands[] = ajax_command_add_css($styles);
}
if (!empty($scripts_header)) {
$extra_commands[] = ajax_command_prepend('head', $scripts_header);
}
if (!empty($scripts_footer)) {
$extra_commands[] = ajax_command_append('body', $scripts_footer);
}
if (!empty($extra_commands)) {
$commands = array_merge($extra_commands, $commands);
}
// Now add a command to merge changes and additions to Drupal.settings.
$scripts = drupal_add_js();
drupal_alter('js', $scripts);
if (!empty($scripts['settings'])) {
$settings = $scripts['settings'];
array_unshift($commands, ajax_command_settings(drupal_array_merge_deep_array($settings['data']), TRUE));
}
// Allow modules to alter any Ajax response.
drupal_alter('ajax_render', $commands);
return drupal_json_encode($commands);
}
/**
* Gets a form submitted via #ajax during an Ajax callback.
*
* This will load a form from the form cache used during Ajax operations. It
* pulls the form info from $_POST.
*
* @return
* An array containing the $form, $form_state, $form_id, $form_build_id and an
* initial list of Ajax $commands. Use the list() function to break these
* apart:
* @code
* list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form();
* @endcode
*/
function ajax_get_form() {
$form_state = form_state_defaults();
$form_build_id = $_POST['form_build_id'];
// Get the form from the cache.
$form = form_get_cache($form_build_id, $form_state);
if (!$form) {
// If $form cannot be loaded from the cache, the form_build_id in $_POST
// must be invalid, which means that someone performed a POST request onto
// system/ajax without actually viewing the concerned form in the browser.
// This is likely a hacking attempt as it never happens under normal
// circumstances, so we just do nothing.
watchdog('ajax', 'Invalid form POST data.', array(), WATCHDOG_WARNING);
drupal_exit();
}
// When a page level cache is enabled, the form-build id might have been
// replaced from within form_get_cache. If this is the case, it is also
// necessary to update it in the browser by issuing an appropriate Ajax
// command.
$commands = array();
if (isset($form['#build_id_old']) && $form['#build_id_old'] != $form['#build_id']) {
// If the form build ID has changed, issue an Ajax command to update it.
$commands[] = ajax_command_update_build_id($form);
$form_build_id = $form['#build_id'];
}
// Since some of the submit handlers are run, redirects need to be disabled.
$form_state['no_redirect'] = TRUE;
// When a form is rebuilt after Ajax processing, its #build_id and #action
// should not change.
// @see drupal_rebuild_form()
$form_state['rebuild_info']['copy']['#build_id'] = TRUE;
$form_state['rebuild_info']['copy']['#action'] = TRUE;
// The form needs to be processed; prepare for that by setting a few internal
// variables.
$form_state['input'] = $_POST;
$form_id = $form['#form_id'];
return array($form, $form_state, $form_id, $form_build_id, $commands);
}
/**
* Menu callback; handles Ajax requests for the #ajax Form API property.
*
* This rebuilds the form from cache and invokes the defined #ajax['callback']
* to return an Ajax command structure for JavaScript. In case no 'callback' has
* been defined, nothing will happen.
*
* The Form API #ajax property can be set both for buttons and other input
* elements.
*
* This function is also the canonical example of how to implement
* #ajax['path']. If processing is required that cannot be accomplished with
* a callback, re-implement this function and set #ajax['path'] to the
* enhanced function.
*
* @see system_menu()
*/
function ajax_form_callback() {
list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form();
drupal_process_form($form['#form_id'], $form, $form_state);
// We need to return the part of the form (or some other content) that needs
// to be re-rendered so the browser can update the page with changed content.
// Since this is the generic menu callback used by many Ajax elements, it is
// up to the #ajax['callback'] function of the element (may or may not be a
// button) that triggered the Ajax request to determine what needs to be
// rendered.
if (!empty($form_state['triggering_element'])) {
$callback = $form_state['triggering_element']['#ajax']['callback'];
}
if (!empty($callback) && is_callable($callback)) {
$result = $callback($form, $form_state);
if (!(is_array($result) && isset($result['#type']) && $result['#type'] == 'ajax')) {
// Turn the response into a #type=ajax array if it isn't one already.
$result = array(
'#type' => 'ajax',
'#commands' => ajax_prepare_response($result),
);
}
$result['#commands'] = array_merge($commands, $result['#commands']);
return $result;
}
}
/**
* Theme callback for Ajax requests.
*
* Many different pages can invoke an Ajax request to system/ajax or another
* generic Ajax path. It is almost always desired for an Ajax response to be
* rendered using the same theme as the base page, because most themes are built
* with the assumption that they control the entire page, so if the CSS for two
* themes are both loaded for a given page, they may conflict with each other.
* For example, Bartik is Drupal's default theme, and Seven is Drupal's default
* administration theme. Depending on whether the "Use the administration theme
* when editing or creating content" checkbox is checked, the node edit form may
* be displayed in either theme, but the Ajax response to the Field module's
* "Add another item" button should be rendered using the same theme as the rest
* of the page. Therefore, system_menu() sets the 'theme callback' for
* 'system/ajax' to this function, and it is recommended that modules
* implementing other generic Ajax paths do the same.
*
* @see system_menu()
* @see file_menu()
*/
function ajax_base_page_theme() {
if (!empty($_POST['ajax_page_state']['theme']) && !empty($_POST['ajax_page_state']['theme_token'])) {
$theme = $_POST['ajax_page_state']['theme'];
$token = $_POST['ajax_page_state']['theme_token'];
// Prevent a request forgery from giving a person access to a theme they
// shouldn't be otherwise allowed to see. However, since everyone is allowed
// to see the default theme, token validation isn't required for that, and
// bypassing it allows most use-cases to work even when accessed from the
// page cache.
if ($theme === variable_get('theme_default', 'bartik') || drupal_valid_token($token, $theme)) {
return $theme;
}
}
}
/**
* Packages and sends the result of a page callback as an Ajax response.
*
* This function is the equivalent of drupal_deliver_html_page(), but for Ajax
* requests. Like that function, it:
* - Adds needed HTTP headers.
* - Prints rendered output.
* - Performs end-of-request tasks.
*
* @param $page_callback_result
* The result of a page callback. Can be one of:
* - NULL: to indicate no content.
* - An integer menu status constant: to indicate an error condition.
* - A string of HTML content.
* - A renderable array of content.
*
* @see drupal_deliver_html_page()
*/
function ajax_deliver($page_callback_result) {
// Browsers do not allow JavaScript to read the contents of a user's local
// files. To work around that, the jQuery Form plugin submits forms containing
// a file input element to an IFRAME, instead of using XHR. Browsers do not
// normally expect JSON strings as content within an IFRAME, so the response
// must be customized accordingly.
// @see http://malsup.com/jquery/form/#file-upload
// @see Drupal.ajax.prototype.beforeSend()
$iframe_upload = !empty($_POST['ajax_iframe_upload']);
// Emit a Content-Type HTTP header if none has been added by the page callback
// or by a wrapping delivery callback.
if (is_null(drupal_get_http_header('Content-Type'))) {
if (!$iframe_upload) {
// Standard JSON can be returned to a browser's XHR object, and to
// non-browser user agents.
// @see http://www.ietf.org/rfc/rfc4627.txt?number=4627
drupal_add_http_header('Content-Type', 'application/json; charset=utf-8');
}
else {
// Browser IFRAMEs expect HTML. With most other content types, Internet
// Explorer presents the user with a download prompt.
drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
}
}
// Let ajax.js know that this response is safe to process.
ajax_set_verification_header();
// Print the response.
$commands = ajax_prepare_response($page_callback_result);
$json = ajax_render($commands);
if (!$iframe_upload) {
// Standard JSON can be returned to a browser's XHR object, and to
// non-browser user agents.
print $json;
}
else {
// Browser IFRAMEs expect HTML. Browser extensions, such as Linkification
// and Skype's Browser Highlighter, convert URLs, phone numbers, etc. into
// links. This corrupts the JSON response. Protect the integrity of the
// JSON data by making it the value of a textarea.
// @see http://malsup.com/jquery/form/#file-upload
// @see http://drupal.org/node/1009382
print '';
}
// Perform end-of-request tasks.
ajax_footer();
}
/**
* Converts the return value of a page callback into an Ajax commands array.
*
* @param $page_callback_result
* The result of a page callback. Can be one of:
* - NULL: to indicate no content.
* - An integer menu status constant: to indicate an error condition.
* - A string of HTML content.
* - A renderable array of content.
*
* @return
* An Ajax commands array that can be passed to ajax_render().
*/
function ajax_prepare_response($page_callback_result) {
$commands = array();
if (!isset($page_callback_result)) {
// Simply delivering an empty commands array is sufficient. This results
// in the Ajax request being completed, but nothing being done to the page.
}
elseif (is_int($page_callback_result)) {
switch ($page_callback_result) {
case MENU_NOT_FOUND:
$commands[] = ajax_command_alert(t('The requested page could not be found.'));
break;
case MENU_ACCESS_DENIED:
$commands[] = ajax_command_alert(t('You are not authorized to access this page.'));
break;
case MENU_SITE_OFFLINE:
$commands[] = ajax_command_alert(filter_xss_admin(variable_get('maintenance_mode_message',
t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal'))))));
break;
}
}
elseif (is_array($page_callback_result) && isset($page_callback_result['#type']) && ($page_callback_result['#type'] == 'ajax')) {
// Complex Ajax callbacks can return a result that contains an error message
// or a specific set of commands to send to the browser.
$page_callback_result += element_info('ajax');
$error = $page_callback_result['#error'];
if (isset($error) && $error !== FALSE) {
if ((empty($error) || $error === TRUE)) {
$error = t('An error occurred while handling the request: The server received invalid input.');
}
$commands[] = ajax_command_alert($error);
}
else {
$commands = $page_callback_result['#commands'];
}
}
else {
// Like normal page callbacks, simple Ajax callbacks can return HTML
// content, as a string or render array. This HTML is inserted in some
// relationship to #ajax['wrapper'], as determined by which jQuery DOM
// manipulation method is used. The method used is specified by
// #ajax['method']. The default method is 'replaceWith', which completely
// replaces the old wrapper element and its content with the new HTML.
$html = is_string($page_callback_result) ? $page_callback_result : drupal_render($page_callback_result);
$commands[] = ajax_command_insert(NULL, $html);
// Add the status messages inside the new content's wrapper element, so that
// on subsequent Ajax requests, it is treated as old content.
$commands[] = ajax_command_prepend(NULL, theme('status_messages'));
}
return $commands;
}
/**
* Sets a response header for ajax.js to trust the response body.
*
* It is not safe to invoke Ajax commands within user-uploaded files, so this
* header protects against those being invoked.
*
* @see Drupal.ajax.options.success()
*/
function ajax_set_verification_header() {
$added = &drupal_static(__FUNCTION__);
// User-uploaded files cannot set any response headers, so a custom header is
// used to indicate to ajax.js that this response is safe. Note that most
// Ajax requests bound using the Form API will be protected by having the URL
// flagged as trusted in Drupal.settings, so this header is used only for
// things like custom markup that gets Ajax behaviors attached.
if (empty($added)) {
drupal_add_http_header('X-Drupal-Ajax-Token', '1');
// Avoid sending the header twice.
$added = TRUE;
}
}
/**
* Performs end-of-Ajax-request tasks.
*
* This function is the equivalent of drupal_page_footer(), but for Ajax
* requests.
*
* @see drupal_page_footer()
*/
function ajax_footer() {
// Even for Ajax requests, invoke hook_exit() implementations. There may be
// modules that need very fast Ajax responses, and therefore, run Ajax
// requests with an early bootstrap.
if (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update')) {
module_invoke_all('exit');
}
// Commit the user session. See above comment about the possibility of this
// function running without session.inc loaded.
if (function_exists('drupal_session_commit')) {
drupal_session_commit();
}
}
/**
* Form element processing handler for the #ajax form property.
*
* @param $element
* An associative array containing the properties of the element.
*
* @return
* The processed element.
*
* @see ajax_pre_render_element()
*/
function ajax_process_form($element, &$form_state) {
$element = ajax_pre_render_element($element);
if (!empty($element['#ajax_processed'])) {
$form_state['cache'] = TRUE;
}
return $element;
}
/**
* Adds Ajax information about an element to communicate with JavaScript.
*
* If #ajax['path'] is set on an element, this additional JavaScript is added
* to the page header to attach the Ajax behaviors. See ajax.js for more
* information.
*
* @param $element
* An associative array containing the properties of the element.
* Properties used:
* - #ajax['event']
* - #ajax['prevent']
* - #ajax['path']
* - #ajax['options']
* - #ajax['wrapper']
* - #ajax['parameters']
* - #ajax['effect']
*
* @return
* The processed element with the necessary JavaScript attached to it.
*/
function ajax_pre_render_element($element) {
// Skip already processed elements.
if (isset($element['#ajax_processed'])) {
return $element;
}
// Initialize #ajax_processed, so we do not process this element again.
$element['#ajax_processed'] = FALSE;
// Nothing to do if there is neither a callback nor a path.
if (!(isset($element['#ajax']['callback']) || isset($element['#ajax']['path']))) {
return $element;
}
// Add a reasonable default event handler if none was specified.
if (isset($element['#ajax']) && !isset($element['#ajax']['event'])) {
switch ($element['#type']) {
case 'submit':
case 'button':
case 'image_button':
// Pressing the ENTER key within a textfield triggers the click event of
// the form's first submit button. Triggering Ajax in this situation
// leads to problems, like breaking autocomplete textfields, so we bind
// to mousedown instead of click.
// @see http://drupal.org/node/216059
$element['#ajax']['event'] = 'mousedown';
// Retain keyboard accessibility by setting 'keypress'. This causes
// ajax.js to trigger 'event' when SPACE or ENTER are pressed while the
// button has focus.
$element['#ajax']['keypress'] = TRUE;
// Binding to mousedown rather than click means that it is possible to
// trigger a click by pressing the mouse, holding the mouse button down
// until the Ajax request is complete and the button is re-enabled, and
// then releasing the mouse button. Set 'prevent' so that ajax.js binds
// an additional handler to prevent such a click from triggering a
// non-Ajax form submission. This also prevents a textfield's ENTER
// press triggering this button's non-Ajax form submission behavior.
if (!isset($element['#ajax']['prevent'])) {
$element['#ajax']['prevent'] = 'click';
}
break;
case 'password':
case 'textfield':
case 'textarea':
$element['#ajax']['event'] = 'blur';
break;
case 'radio':
case 'checkbox':
case 'select':
$element['#ajax']['event'] = 'change';
break;
case 'link':
$element['#ajax']['event'] = 'click';
break;
default:
return $element;
}
}
// Attach JavaScript settings to the element.
if (isset($element['#ajax']['event'])) {
$element['#attached']['library'][] = array('system', 'jquery.form');
$element['#attached']['library'][] = array('system', 'drupal.ajax');
$settings = $element['#ajax'];
// Assign default settings.
$settings += array(
'path' => 'system/ajax',
'options' => array(),
);
// @todo Legacy support. Remove in Drupal 8.
if (isset($settings['method']) && $settings['method'] == 'replace') {
$settings['method'] = 'replaceWith';
}
// Change path to URL.
$settings['url'] = url($settings['path'], $settings['options']);
unset($settings['path'], $settings['options']);
// Add special data to $settings['submit'] so that when this element
// triggers an Ajax submission, Drupal's form processing can determine which
// element triggered it.
// @see _form_element_triggered_scripted_submission()
if (isset($settings['trigger_as'])) {
// An element can add a 'trigger_as' key within #ajax to make the element
// submit as though another one (for example, a non-button can use this
// to submit the form as though a button were clicked). When using this,
// the 'name' key is always required to identify the element to trigger
// as. The 'value' key is optional, and only needed when multiple elements
// share the same name, which is commonly the case for buttons.
$settings['submit']['_triggering_element_name'] = $settings['trigger_as']['name'];
if (isset($settings['trigger_as']['value'])) {
$settings['submit']['_triggering_element_value'] = $settings['trigger_as']['value'];
}
unset($settings['trigger_as']);
}
elseif (isset($element['#name'])) {
// Most of the time, elements can submit as themselves, in which case the
// 'trigger_as' key isn't needed, and the element's name is used.
$settings['submit']['_triggering_element_name'] = $element['#name'];
// If the element is a (non-image) button, its name may not identify it
// uniquely, in which case a match on value is also needed.
// @see _form_button_was_clicked()
if (isset($element['#button_type']) && empty($element['#has_garbage_value'])) {
$settings['submit']['_triggering_element_value'] = $element['#value'];
}
}
// Convert a simple #ajax['progress'] string into an array.
if (isset($settings['progress']) && is_string($settings['progress'])) {
$settings['progress'] = array('type' => $settings['progress']);
}
// Change progress path to a full URL.
if (isset($settings['progress']['path'])) {
$settings['progress']['url'] = url($settings['progress']['path']);
unset($settings['progress']['path']);
}
$element['#attached']['js'][] = array(
'type' => 'setting',
'data' => array(
'ajax' => array($element['#id'] => $settings),
'urlIsAjaxTrusted' => array(
$settings['url'] => TRUE,
),
),
);
// Indicate that Ajax processing was successful.
$element['#ajax_processed'] = TRUE;
}
return $element;
}
/**
* @} End of "defgroup ajax".
*/
/**
* @defgroup ajax_commands Ajax framework commands
* @{
* Functions to create various Ajax commands.
*
* These functions can be used to create arrays for use with the
* ajax_render() function.
*/
/**
* Creates a Drupal Ajax 'alert' command.
*
* The 'alert' command instructs the client to display a JavaScript alert
* dialog box.
*
* This command is implemented by Drupal.ajax.prototype.commands.alert()
* defined in misc/ajax.js.
*
* @param $text
* The message string to display to the user.
*
* @return
* An array suitable for use with the ajax_render() function.
*/
function ajax_command_alert($text) {
return array(
'command' => 'alert',
'text' => $text,
);
}
/**
* Creates a Drupal Ajax 'insert' command using the method in #ajax['method'].
*
* This command instructs the client to insert the given HTML using whichever
* jQuery DOM manipulation method has been specified in the #ajax['method']
* variable of the element that triggered the request.
*
* This command is implemented by Drupal.ajax.prototype.commands.insert()
* defined in misc/ajax.js.
*
* @param $selector
* A jQuery selector string. If the command is a response to a request from
* an #ajax form element then this value can be NULL.
* @param $html
* The data to use with the jQuery method.
* @param $settings
* An optional array of settings that will be used for this command only.
*
* @return
* An array suitable for use with the ajax_render() function.
*/
function ajax_command_insert($selector, $html, $settings = NULL) {
return array(
'command' => 'insert',
'method' => NULL,
'selector' => $selector,
'data' => $html,
'settings' => $settings,
);
}
/**
* Creates a Drupal Ajax 'insert/replaceWith' command.
*
* The 'insert/replaceWith' command instructs the client to use jQuery's
* replaceWith() method to replace each element matched matched by the given
* selector with the given HTML.
*
* This command is implemented by Drupal.ajax.prototype.commands.insert()
* defined in misc/ajax.js.
*
* @param $selector
* A jQuery selector string. If the command is a response to a request from
* an #ajax form element then this value can be NULL.
* @param $html
* The data to use with the jQuery replaceWith() method.
* @param $settings
* An optional array of settings that will be used for this command only.
*
* @return
* An array suitable for use with the ajax_render() function.
*
* See
* @link http://docs.jquery.com/Manipulation/replaceWith#content jQuery replaceWith command @endlink
*/
function ajax_command_replace($selector, $html, $settings = NULL) {
return array(
'command' => 'insert',
'method' => 'replaceWith',
'selector' => $selector,
'data' => $html,
'settings' => $settings,
);
}
/**
* Creates a Drupal Ajax 'insert/html' command.
*
* The 'insert/html' command instructs the client to use jQuery's html()
* method to set the HTML content of each element matched by the given
* selector while leaving the outer tags intact.
*
* This command is implemented by Drupal.ajax.prototype.commands.insert()
* defined in misc/ajax.js.
*
* @param $selector
* A jQuery selector string. If the command is a response to a request from
* an #ajax form element then this value can be NULL.
* @param $html
* The data to use with the jQuery html() method.
* @param $settings
* An optional array of settings that will be used for this command only.
*
* @return
* An array suitable for use with the ajax_render() function.
*
* @see http://docs.jquery.com/Attributes/html#val
*/
function ajax_command_html($selector, $html, $settings = NULL) {
return array(
'command' => 'insert',
'method' => 'html',
'selector' => $selector,
'data' => $html,
'settings' => $settings,
);
}
/**
* Creates a Drupal Ajax 'insert/prepend' command.
*
* The 'insert/prepend' command instructs the client to use jQuery's prepend()
* method to prepend the given HTML content to the inside each element matched
* by the given selector.
*
* This command is implemented by Drupal.ajax.prototype.commands.insert()
* defined in misc/ajax.js.
*
* @param $selector
* A jQuery selector string. If the command is a response to a request from
* an #ajax form element then this value can be NULL.
* @param $html
* The data to use with the jQuery prepend() method.
* @param $settings
* An optional array of settings that will be used for this command only.
*
* @return
* An array suitable for use with the ajax_render() function.
*
* @see http://docs.jquery.com/Manipulation/prepend#content
*/
function ajax_command_prepend($selector, $html, $settings = NULL) {
return array(
'command' => 'insert',
'method' => 'prepend',
'selector' => $selector,
'data' => $html,
'settings' => $settings,
);
}
/**
* Creates a Drupal Ajax 'insert/append' command.
*
* The 'insert/append' command instructs the client to use jQuery's append()
* method to append the given HTML content to the inside of each element matched
* by the given selector.
*
* This command is implemented by Drupal.ajax.prototype.commands.insert()
* defined in misc/ajax.js.
*
* @param $selector
* A jQuery selector string. If the command is a response to a request from
* an #ajax form element then this value can be NULL.
* @param $html
* The data to use with the jQuery append() method.
* @param $settings
* An optional array of settings that will be used for this command only.
*
* @return
* An array suitable for use with the ajax_render() function.
*
* @see http://docs.jquery.com/Manipulation/append#content
*/
function ajax_command_append($selector, $html, $settings = NULL) {
return array(
'command' => 'insert',
'method' => 'append',
'selector' => $selector,
'data' => $html,
'settings' => $settings,
);
}
/**
* Creates a Drupal Ajax 'insert/after' command.
*
* The 'insert/after' command instructs the client to use jQuery's after()
* method to insert the given HTML content after each element matched by
* the given selector.
*
* This command is implemented by Drupal.ajax.prototype.commands.insert()
* defined in misc/ajax.js.
*
* @param $selector
* A jQuery selector string. If the command is a response to a request from
* an #ajax form element then this value can be NULL.
* @param $html
* The data to use with the jQuery after() method.
* @param $settings
* An optional array of settings that will be used for this command only.
*
* @return
* An array suitable for use with the ajax_render() function.
*
* @see http://docs.jquery.com/Manipulation/after#content
*/
function ajax_command_after($selector, $html, $settings = NULL) {
return array(
'command' => 'insert',
'method' => 'after',
'selector' => $selector,
'data' => $html,
'settings' => $settings,
);
}
/**
* Creates a Drupal Ajax 'insert/before' command.
*
* The 'insert/before' command instructs the client to use jQuery's before()
* method to insert the given HTML content before each of elements matched by
* the given selector.
*
* This command is implemented by Drupal.ajax.prototype.commands.insert()
* defined in misc/ajax.js.
*
* @param $selector
* A jQuery selector string. If the command is a response to a request from
* an #ajax form element then this value can be NULL.
* @param $html
* The data to use with the jQuery before() method.
* @param $settings
* An optional array of settings that will be used for this command only.
*
* @return
* An array suitable for use with the ajax_render() function.
*
* @see http://docs.jquery.com/Manipulation/before#content
*/
function ajax_command_before($selector, $html, $settings = NULL) {
return array(
'command' => 'insert',
'method' => 'before',
'selector' => $selector,
'data' => $html,
'settings' => $settings,
);
}
/**
* Creates a Drupal Ajax 'remove' command.
*
* The 'remove' command instructs the client to use jQuery's remove() method
* to remove each of elements matched by the given selector, and everything
* within them.
*
* This command is implemented by Drupal.ajax.prototype.commands.remove()
* defined in misc/ajax.js.
*
* @param $selector
* A jQuery selector string. If the command is a response to a request from
* an #ajax form element then this value can be NULL.
*
* @return
* An array suitable for use with the ajax_render() function.
*
* @see http://docs.jquery.com/Manipulation/remove#expr
*/
function ajax_command_remove($selector) {
return array(
'command' => 'remove',
'selector' => $selector,
);
}
/**
* Creates a Drupal Ajax 'changed' command.
*
* This command instructs the client to mark each of the elements matched by the
* given selector as 'ajax-changed'.
*
* This command is implemented by Drupal.ajax.prototype.commands.changed()
* defined in misc/ajax.js.
*
* @param $selector
* A jQuery selector string. If the command is a response to a request from
* an #ajax form element then this value can be NULL.
* @param $asterisk
* An optional CSS selector which must be inside $selector. If specified,
* an asterisk will be appended to the HTML inside the $asterisk selector.
*
* @return
* An array suitable for use with the ajax_render() function.
*/
function ajax_command_changed($selector, $asterisk = '') {
return array(
'command' => 'changed',
'selector' => $selector,
'asterisk' => $asterisk,
);
}
/**
* Creates a Drupal Ajax 'css' command.
*
* The 'css' command will instruct the client to use the jQuery css() method
* to apply the CSS arguments to elements matched by the given selector.
*
* This command is implemented by Drupal.ajax.prototype.commands.css()
* defined in misc/ajax.js.
*
* @param $selector
* A jQuery selector string. If the command is a response to a request from
* an #ajax form element then this value can be NULL.
* @param $argument
* An array of key/value pairs to set in the CSS for the selector.
*
* @return
* An array suitable for use with the ajax_render() function.
*
* @see http://docs.jquery.com/CSS/css#properties
*/
function ajax_command_css($selector, $argument) {
return array(
'command' => 'css',
'selector' => $selector,
'argument' => $argument,
);
}
/**
* Creates a Drupal Ajax 'settings' command.
*
* The 'settings' command instructs the client either to use the given array as
* the settings for ajax-loaded content or to extend Drupal.settings with the
* given array, depending on the value of the $merge parameter.
*
* This command is implemented by Drupal.ajax.prototype.commands.settings()
* defined in misc/ajax.js.
*
* @param $argument
* An array of key/value pairs to add to the settings. This will be utilized
* for all commands after this if they do not include their own settings
* array.
* @param $merge
* Whether or not the passed settings in $argument should be merged into the
* global Drupal.settings on the page. By default (FALSE), the settings that
* are passed to Drupal.attachBehaviors will not include the global
* Drupal.settings.
*
* @return
* An array suitable for use with the ajax_render() function.
*/
function ajax_command_settings($argument, $merge = FALSE) {
return array(
'command' => 'settings',
'settings' => $argument,
'merge' => $merge,
);
}
/**
* Creates a Drupal Ajax 'data' command.
*
* The 'data' command instructs the client to attach the name=value pair of
* data to the selector via jQuery's data cache.
*
* This command is implemented by Drupal.ajax.prototype.commands.data()
* defined in misc/ajax.js.
*
* @param $selector
* A jQuery selector string. If the command is a response to a request from
* an #ajax form element then this value can be NULL.
* @param $name
* The name or key (in the key value pair) of the data attached to this
* selector.
* @param $value
* The value of the data. Not just limited to strings can be any format.
*
* @return
* An array suitable for use with the ajax_render() function.
*
* @see http://docs.jquery.com/Core/data#namevalue
*/
function ajax_command_data($selector, $name, $value) {
return array(
'command' => 'data',
'selector' => $selector,
'name' => $name,
'value' => $value,
);
}
/**
* Creates a Drupal Ajax 'invoke' command.
*
* The 'invoke' command will instruct the client to invoke the given jQuery
* method with the supplied arguments on the elements matched by the given
* selector. Intended for simple jQuery commands, such as attr(), addClass(),
* removeClass(), toggleClass(), etc.
*
* This command is implemented by Drupal.ajax.prototype.commands.invoke()
* defined in misc/ajax.js.
*
* @param $selector
* A jQuery selector string. If the command is a response to a request from
* an #ajax form element then this value can be NULL.
* @param $method
* The jQuery method to invoke.
* @param $arguments
* (optional) A list of arguments to the jQuery $method, if any.
*
* @return
* An array suitable for use with the ajax_render() function.
*/
function ajax_command_invoke($selector, $method, array $arguments = array()) {
return array(
'command' => 'invoke',
'selector' => $selector,
'method' => $method,
'arguments' => $arguments,
);
}
/**
* Creates a Drupal Ajax 'restripe' command.
*
* The 'restripe' command instructs the client to restripe a table. This is
* usually used after a table has been modified by a replace or append command.
*
* This command is implemented by Drupal.ajax.prototype.commands.restripe()
* defined in misc/ajax.js.
*
* @param $selector
* A jQuery selector string.
*
* @return
* An array suitable for use with the ajax_render() function.
*/
function ajax_command_restripe($selector) {
return array(
'command' => 'restripe',
'selector' => $selector,
);
}
/**
* Creates a Drupal Ajax 'update_build_id' command.
*
* This command updates the value of a hidden form_build_id input element on a
* form. It requires the form passed in to have keys for both the old build ID
* in #build_id_old and the new build ID in #build_id.
*
* The primary use case for this Ajax command is to serve a new build ID to a
* form served from the cache to an anonymous user, preventing one anonymous
* user from accessing the form state of another anonymous users on Ajax enabled
* forms.
*
* @param $form
* The form array representing the form whose build ID should be updated.
*/
function ajax_command_update_build_id($form) {
return array(
'command' => 'updateBuildId',
'old' => $form['#build_id_old'],
'new' => $form['#build_id'],
);
}
/**
* Creates a Drupal Ajax 'add_css' command.
*
* This method will add css via ajax in a cross-browser compatible way.
*
* This command is implemented by Drupal.ajax.prototype.commands.add_css()
* defined in misc/ajax.js.
*
* @param $styles
* A string that contains the styles to be added.
*
* @return
* An array suitable for use with the ajax_render() function.
*
* @see misc/ajax.js
*/
function ajax_command_add_css($styles) {
return array(
'command' => 'add_css',
'data' => $styles,
);
}
'',
'#markup' => t('WARNING: You are not using an encrypted connection, so your password will be sent in plain text.
Learn more .', array('@https-link' => 'http://drupal.org/https-information')),
'#suffix' => '
',
);
}
// Decide on a default backend.
if (isset($form_state['values']['connection_settings']['authorize_filetransfer_default'])) {
$authorize_filetransfer_default = $form_state['values']['connection_settings']['authorize_filetransfer_default'];
}
elseif ($authorize_filetransfer_default = variable_get('authorize_filetransfer_default', NULL));
else {
$authorize_filetransfer_default = key($available_backends);
}
$form['information']['main_header'] = array(
'#prefix' => '',
'#markup' => t('To continue, provide your server connection details'),
'#suffix' => ' ',
);
$form['connection_settings']['#tree'] = TRUE;
$form['connection_settings']['authorize_filetransfer_default'] = array(
'#type' => 'select',
'#title' => t('Connection method'),
'#default_value' => $authorize_filetransfer_default,
'#weight' => -10,
);
/*
* Here we create two submit buttons. For a JS enabled client, they will
* only ever see submit_process. However, if a client doesn't have JS
* enabled, they will see submit_connection on the first form (when picking
* what filetransfer type to use, and submit_process on the second one (which
* leads to the actual operation).
*/
$form['submit_connection'] = array(
'#prefix' => " ",
'#name' => 'enter_connection_settings',
'#type' => 'submit',
'#value' => t('Enter connection settings'),
'#weight' => 100,
);
$form['submit_process'] = array(
'#name' => 'process_updates',
'#type' => 'submit',
'#value' => t('Continue'),
'#weight' => 100,
'#attributes' => array('style' => 'display:none'),
);
// Build a container for each connection type.
foreach ($available_backends as $name => $backend) {
$form['connection_settings']['authorize_filetransfer_default']['#options'][$name] = $backend['title'];
$form['connection_settings'][$name] = array(
'#type' => 'container',
'#attributes' => array('class' => array("filetransfer-$name", 'filetransfer')),
);
// We can't use #prefix on the container itself since then the header won't
// be hidden and shown when the containers are being manipulated via JS.
$form['connection_settings'][$name]['header'] = array(
'#markup' => '' . t('@backend connection settings', array('@backend' => $backend['title'])) . ' ',
);
$form['connection_settings'][$name] += _authorize_filetransfer_connection_settings($name);
// Start non-JS code.
if (isset($form_state['values']['connection_settings']['authorize_filetransfer_default']) && $form_state['values']['connection_settings']['authorize_filetransfer_default'] == $name) {
// Change the submit button to the submit_process one.
$form['submit_process']['#attributes'] = array();
unset($form['submit_connection']);
// Activate the proper filetransfer settings form.
$form['connection_settings'][$name]['#attributes']['style'] = 'display:block';
// Disable the select box.
$form['connection_settings']['authorize_filetransfer_default']['#disabled'] = TRUE;
// Create a button for changing the type of connection.
$form['connection_settings']['change_connection_type'] = array(
'#name' => 'change_connection_type',
'#type' => 'submit',
'#value' => t('Change connection type'),
'#weight' => -5,
'#attributes' => array('class' => array('filetransfer-change-connection-type')),
);
}
// End non-JS code.
}
return $form;
}
/**
* Generates the Form API array for a given connection backend's settings.
*
* @param $backend
* The name of the backend (e.g. 'ftp', 'ssh', etc).
*
* @return
* Form API array of connection settings for the given backend.
*
* @see hook_filetransfer_backends()
*/
function _authorize_filetransfer_connection_settings($backend) {
$defaults = variable_get('authorize_filetransfer_connection_settings_' . $backend, array());
$form = array();
// Create an instance of the file transfer class to get its settings form.
$filetransfer = authorize_get_filetransfer($backend);
if ($filetransfer) {
$form = $filetransfer->getSettingsForm();
}
// Fill in the defaults based on the saved settings, if any.
_authorize_filetransfer_connection_settings_set_defaults($form, NULL, $defaults);
return $form;
}
/**
* Sets the default settings on a file transfer connection form recursively.
*
* The default settings for the file transfer connection forms are saved in
* the database. The settings are stored as a nested array in the case of a
* settings form that has fieldsets or otherwise uses a nested structure.
* Therefore, to properly add defaults, we need to walk through all the
* children form elements and process those defaults recursively.
*
* @param $element
* Reference to the Form API form element we're operating on.
* @param $key
* The key for our current form element, if any.
* @param array $defaults
* The default settings for the file transfer backend we're operating on.
*/
function _authorize_filetransfer_connection_settings_set_defaults(&$element, $key, array $defaults) {
// If we're operating on a form element which isn't a fieldset, and we have
// a default setting saved, stash it in #default_value.
if (!empty($key) && isset($defaults[$key]) && isset($element['#type']) && $element['#type'] != 'fieldset') {
$element['#default_value'] = $defaults[$key];
}
// Now, we walk through all the child elements, and recursively invoke
// ourself on each one. Since the $defaults settings array can be nested
// (because of #tree, any values inside fieldsets will be nested), if
// there's a subarray of settings for the form key we're currently
// processing, pass in that subarray to the recursive call. Otherwise, just
// pass on the whole $defaults array.
foreach (element_children($element) as $child_key) {
_authorize_filetransfer_connection_settings_set_defaults($element[$child_key], $child_key, ((isset($defaults[$key]) && is_array($defaults[$key])) ? $defaults[$key] : $defaults));
}
}
/**
* Form validation handler for authorize_filetransfer_form().
*
* @see authorize_filetransfer_form()
* @see authorize_filetransfer_submit()
*/
function authorize_filetransfer_form_validate($form, &$form_state) {
// Only validate the form if we have collected all of the user input and are
// ready to proceed with updating or installing.
if ($form_state['triggering_element']['#name'] != 'process_updates') {
return;
}
if (isset($form_state['values']['connection_settings'])) {
$backend = $form_state['values']['connection_settings']['authorize_filetransfer_default'];
$filetransfer = authorize_get_filetransfer($backend, $form_state['values']['connection_settings'][$backend]);
try {
if (!$filetransfer) {
throw new Exception(t('Error, this type of connection protocol (%backend) does not exist.', array('%backend' => $backend)));
}
$filetransfer->connect();
}
catch (Exception $e) {
// The format of this error message is similar to that used on the
// database connection form in the installer.
form_set_error('connection_settings', t('Failed to connect to the server. The server reports the following message: !message For more help installing or updating code on your server, see the handbook .', array(
'!message' => '' . $e->getMessage() . '
',
'@handbook_url' => 'http://drupal.org/documentation/install/modules-themes',
)));
}
}
}
/**
* Form submission handler for authorize_filetransfer_form().
*
* @see authorize_filetransfer_form()
* @see authorize_filetransfer_validate()
*/
function authorize_filetransfer_form_submit($form, &$form_state) {
global $base_url;
switch ($form_state['triggering_element']['#name']) {
case 'process_updates':
// Save the connection settings to the DB.
$filetransfer_backend = $form_state['values']['connection_settings']['authorize_filetransfer_default'];
// If the database is available then try to save our settings. We have
// to make sure it is available since this code could potentially (will
// likely) be called during the installation process, before the
// database is set up.
try {
$connection_settings = array();
foreach ($form_state['values']['connection_settings'][$filetransfer_backend] as $key => $value) {
// We do *not* want to store passwords in the database, unless the
// backend explicitly says so via the magic #filetransfer_save form
// property. Otherwise, we store everything that's not explicitly
// marked with #filetransfer_save set to FALSE.
if (!isset($form['connection_settings'][$filetransfer_backend][$key]['#filetransfer_save'])) {
if ($form['connection_settings'][$filetransfer_backend][$key]['#type'] != 'password') {
$connection_settings[$key] = $value;
}
}
// The attribute is defined, so only save if set to TRUE.
elseif ($form['connection_settings'][$filetransfer_backend][$key]['#filetransfer_save']) {
$connection_settings[$key] = $value;
}
}
// Set this one as the default authorize method.
variable_set('authorize_filetransfer_default', $filetransfer_backend);
// Save the connection settings minus the password.
variable_set('authorize_filetransfer_connection_settings_' . $filetransfer_backend, $connection_settings);
$filetransfer = authorize_get_filetransfer($filetransfer_backend, $form_state['values']['connection_settings'][$filetransfer_backend]);
// Now run the operation.
authorize_run_operation($filetransfer);
}
catch (Exception $e) {
// If there is no database available, we don't care and just skip
// this part entirely.
}
break;
case 'enter_connection_settings':
$form_state['rebuild'] = TRUE;
break;
case 'change_connection_type':
$form_state['rebuild'] = TRUE;
unset($form_state['values']['connection_settings']['authorize_filetransfer_default']);
break;
}
}
/**
* Runs the operation specified in $_SESSION['authorize_operation'].
*
* @param $filetransfer
* The FileTransfer object to use for running the operation.
*/
function authorize_run_operation($filetransfer) {
$operation = $_SESSION['authorize_operation'];
unset($_SESSION['authorize_operation']);
if (!empty($operation['page_title'])) {
drupal_set_title($operation['page_title']);
}
require_once DRUPAL_ROOT . '/' . $operation['file'];
call_user_func_array($operation['callback'], array_merge(array($filetransfer), $operation['arguments']));
}
/**
* Gets a FileTransfer class for a specific transfer method and settings.
*
* @param $backend
* The FileTransfer backend to get the class for.
* @param $settings
* Array of settings for the FileTransfer.
*
* @return
* An instantiated FileTransfer object for the requested method and settings,
* or FALSE if there was an error finding or instantiating it.
*/
function authorize_get_filetransfer($backend, $settings = array()) {
$filetransfer = FALSE;
if (!empty($_SESSION['authorize_filetransfer_info'][$backend])) {
$backend_info = $_SESSION['authorize_filetransfer_info'][$backend];
if (!empty($backend_info['file'])) {
$file = $backend_info['file path'] . '/' . $backend_info['file'];
require_once $file;
}
if (class_exists($backend_info['class'])) {
// PHP 5.2 doesn't support $class::factory() syntax, so we have to
// use call_user_func_array() until we can require PHP 5.3.
$filetransfer = call_user_func_array(array($backend_info['class'], 'factory'), array(DRUPAL_ROOT, $settings));
}
}
return $filetransfer;
}
$id,
':token' => drupal_get_token($id),
))->fetchField();
if ($batch) {
return unserialize($batch);
}
return FALSE;
}
/**
* Renders the batch processing page based on the current state of the batch.
*
* @see _batch_shutdown()
*/
function _batch_page() {
$batch = &batch_get();
if (!isset($_REQUEST['id'])) {
return FALSE;
}
// Retrieve the current state of the batch.
if (!$batch) {
$batch = batch_load($_REQUEST['id']);
if (!$batch) {
drupal_set_message(t('No active batch.'), 'error');
drupal_goto();
}
}
// Register database update for the end of processing.
drupal_register_shutdown_function('_batch_shutdown');
// Add batch-specific CSS.
foreach ($batch['sets'] as $batch_set) {
if (isset($batch_set['css'])) {
foreach ($batch_set['css'] as $css) {
drupal_add_css($css);
}
}
}
$op = isset($_REQUEST['op']) ? $_REQUEST['op'] : '';
$output = NULL;
switch ($op) {
case 'start':
// Display the full progress page on startup and on each additional
// non-JavaScript iteration.
$output = _batch_progress_page();
break;
case 'do':
// JavaScript-based progress page callback.
_batch_do();
break;
case 'do_nojs':
// Non-JavaScript-based progress page.
$output = _batch_progress_page();
break;
case 'finished':
$output = _batch_finished();
break;
}
return $output;
}
/**
* Does one execution pass with JavaScript and returns progress to the browser.
*
* @see _batch_process()
*/
function _batch_do() {
// Perform actual processing.
list($percentage, $message) = _batch_process();
drupal_json_output(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message));
}
/**
* Outputs a batch processing page.
*
* @see _batch_process()
*/
function _batch_progress_page() {
$batch = &batch_get();
$current_set = _batch_current_set();
drupal_set_title($current_set['title'], PASS_THROUGH);
$new_op = 'do_nojs';
if (!isset($batch['running'])) {
// This is the first page so we return some output immediately.
$percentage = 0;
$message = $current_set['init_message'];
$batch['running'] = TRUE;
}
else {
// This is one of the later requests; do some processing first.
// Error handling: if PHP dies due to a fatal error (e.g. a nonexistent
// function), it will output whatever is in the output buffer, followed by
// the error message.
ob_start();
$fallback = $current_set['error_message'] . ' ' . $batch['error_message'];
$fallback = theme('maintenance_page', array('content' => $fallback, 'show_messages' => FALSE));
// We strip the end of the page using a marker in the template, so any
// additional HTML output by PHP shows up inside the page rather than below
// it. While this causes invalid HTML, the same would be true if we didn't,
// as content is not allowed to appear after anyway.
list($fallback) = explode('', $fallback);
print $fallback;
// Perform actual processing.
list($percentage, $message) = _batch_process($batch);
if ($percentage == 100) {
$new_op = 'finished';
}
// PHP did not die; remove the fallback output.
ob_end_clean();
}
// Merge required query parameters for batch processing into those provided by
// batch_set() or hook_batch_alter().
$batch['url_options']['query']['id'] = $batch['id'];
$batch['url_options']['query']['op'] = $new_op;
$url = url($batch['url'], $batch['url_options']);
$element = array(
// Redirect through a 'Refresh' meta tag if JavaScript is disabled.
'#prefix' => '',
'#suffix' => ' ',
'#tag' => 'meta',
'#attributes' => array(
'http-equiv' => 'Refresh',
'content' => '0; URL=' . $url,
),
);
drupal_add_html_head($element, 'batch_progress_meta_refresh');
// Adds JavaScript code and settings for clients where JavaScript is enabled.
$js_setting = array(
'batch' => array(
'errorMessage' => $current_set['error_message'] . ' ' . $batch['error_message'],
'initMessage' => $current_set['init_message'],
'uri' => $url,
),
);
drupal_add_js($js_setting, 'setting');
drupal_add_library('system', 'drupal.batch');
return theme('progress_bar', array('percent' => $percentage, 'message' => $message));
}
/**
* Processes sets in a batch.
*
* If the batch was marked for progressive execution (default), this executes as
* many operations in batch sets until an execution time of 1 second has been
* exceeded. It will continue with the next operation of the same batch set in
* the next request.
*
* @return
* An array containing a completion value (in percent) and a status message.
*/
function _batch_process() {
$batch = &batch_get();
$current_set = &_batch_current_set();
// Indicate that this batch set needs to be initialized.
$set_changed = TRUE;
// If this batch was marked for progressive execution (e.g. forms submitted by
// drupal_form_submit()), initialize a timer to determine whether we need to
// proceed with the same batch phase when a processing time of 1 second has
// been exceeded.
if ($batch['progressive']) {
timer_start('batch_processing');
}
if (empty($current_set['start'])) {
$current_set['start'] = microtime(TRUE);
}
$queue = _batch_queue($current_set);
while (!$current_set['success']) {
// If this is the first time we iterate this batch set in the current
// request, we check if it requires an additional file for functions
// definitions.
if ($set_changed && isset($current_set['file']) && is_file($current_set['file'])) {
include_once DRUPAL_ROOT . '/' . $current_set['file'];
}
$task_message = '';
// Assume a single pass operation and set the completion level to 1 by
// default.
$finished = 1;
if ($item = $queue->claimItem()) {
list($function, $args) = $item->data;
// Build the 'context' array and execute the function call.
$batch_context = array(
'sandbox' => &$current_set['sandbox'],
'results' => &$current_set['results'],
'finished' => &$finished,
'message' => &$task_message,
);
call_user_func_array($function, array_merge($args, array(&$batch_context)));
if ($finished >= 1) {
// Make sure this step is not counted twice when computing $current.
$finished = 0;
// Remove the processed operation and clear the sandbox.
$queue->deleteItem($item);
$current_set['count']--;
$current_set['sandbox'] = array();
}
}
// When all operations in the current batch set are completed, browse
// through the remaining sets, marking them 'successfully processed'
// along the way, until we find a set that contains operations.
// _batch_next_set() executes form submit handlers stored in 'control'
// sets (see form_execute_handlers()), which can in turn add new sets to
// the batch.
$set_changed = FALSE;
$old_set = $current_set;
while (empty($current_set['count']) && ($current_set['success'] = TRUE) && _batch_next_set()) {
$current_set = &_batch_current_set();
$current_set['start'] = microtime(TRUE);
$set_changed = TRUE;
}
// At this point, either $current_set contains operations that need to be
// processed or all sets have been completed.
$queue = _batch_queue($current_set);
// If we are in progressive mode, break processing after 1 second.
if ($batch['progressive'] && timer_read('batch_processing') > 1000) {
// Record elapsed wall clock time.
$current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
break;
}
}
if ($batch['progressive']) {
// Gather progress information.
// Reporting 100% progress will cause the whole batch to be considered
// processed. If processing was paused right after moving to a new set,
// we have to use the info from the new (unprocessed) set.
if ($set_changed && isset($current_set['queue'])) {
// Processing will continue with a fresh batch set.
$remaining = $current_set['count'];
$total = $current_set['total'];
$progress_message = $current_set['init_message'];
$task_message = '';
}
else {
// Processing will continue with the current batch set.
$remaining = $old_set['count'];
$total = $old_set['total'];
$progress_message = $old_set['progress_message'];
}
// Total progress is the number of operations that have fully run plus the
// completion level of the current operation.
$current = $total - $remaining + $finished;
$percentage = _batch_api_percentage($total, $current);
$elapsed = isset($current_set['elapsed']) ? $current_set['elapsed'] : 0;
$values = array(
'@remaining' => $remaining,
'@total' => $total,
'@current' => floor($current),
'@percentage' => $percentage,
'@elapsed' => format_interval($elapsed / 1000),
// If possible, estimate remaining processing time.
'@estimate' => ($current > 0) ? format_interval(($elapsed * ($total - $current) / $current) / 1000) : '-',
);
$message = strtr($progress_message, $values);
if (!empty($message)) {
$message .= ' ';
}
if (!empty($task_message)) {
$message .= $task_message;
}
return array($percentage, $message);
}
else {
// If we are not in progressive mode, the entire batch has been processed.
return _batch_finished();
}
}
/**
* Formats the percent completion for a batch set.
*
* @param $total
* The total number of operations.
* @param $current
* The number of the current operation. This may be a floating point number
* rather than an integer in the case of a multi-step operation that is not
* yet complete; in that case, the fractional part of $current represents the
* fraction of the operation that has been completed.
*
* @return
* The properly formatted percentage, as a string. We output percentages
* using the correct number of decimal places so that we never print "100%"
* until we are finished, but we also never print more decimal places than
* are meaningful.
*
* @see _batch_process()
*/
function _batch_api_percentage($total, $current) {
if (!$total || $total == $current) {
// If $total doesn't evaluate as true or is equal to the current set, then
// we're finished, and we can return "100".
$percentage = "100";
}
else {
// We add a new digit at 200, 2000, etc. (since, for example, 199/200
// would round up to 100% if we didn't).
$decimal_places = max(0, floor(log10($total / 2.0)) - 1);
do {
// Calculate the percentage to the specified number of decimal places.
$percentage = sprintf('%01.' . $decimal_places . 'f', round($current / $total * 100, $decimal_places));
// When $current is an integer, the above calculation will always be
// correct. However, if $current is a floating point number (in the case
// of a multi-step batch operation that is not yet complete), $percentage
// may be erroneously rounded up to 100%. To prevent that, we add one
// more decimal place and try again.
$decimal_places++;
} while ($percentage == '100');
}
return $percentage;
}
/**
* Returns the batch set being currently processed.
*/
function &_batch_current_set() {
$batch = &batch_get();
return $batch['sets'][$batch['current_set']];
}
/**
* Retrieves the next set in a batch.
*
* If there is a subsequent set in this batch, assign it as the new set to
* process and execute its form submit handler (if defined), which may add
* further sets to this batch.
*
* @return
* TRUE if a subsequent set was found in the batch.
*/
function _batch_next_set() {
$batch = &batch_get();
if (isset($batch['sets'][$batch['current_set'] + 1])) {
$batch['current_set']++;
$current_set = &_batch_current_set();
if (isset($current_set['form_submit']) && ($function = $current_set['form_submit']) && function_exists($function)) {
// We use our stored copies of $form and $form_state to account for
// possible alterations by previous form submit handlers.
$function($batch['form_state']['complete form'], $batch['form_state']);
}
return TRUE;
}
}
/**
* Ends the batch processing.
*
* Call the 'finished' callback of each batch set to allow custom handling of
* the results and resolve page redirection.
*/
function _batch_finished() {
$batch = &batch_get();
// Execute the 'finished' callbacks for each batch set, if defined.
foreach ($batch['sets'] as $batch_set) {
if (isset($batch_set['finished'])) {
// Check if the set requires an additional file for function definitions.
if (isset($batch_set['file']) && is_file($batch_set['file'])) {
include_once DRUPAL_ROOT . '/' . $batch_set['file'];
}
if (is_callable($batch_set['finished'])) {
$queue = _batch_queue($batch_set);
$operations = $queue->getAllItems();
call_user_func($batch_set['finished'], $batch_set['success'], $batch_set['results'], $operations, format_interval($batch_set['elapsed'] / 1000));
}
}
}
// Clean up the batch table and unset the static $batch variable.
if ($batch['progressive']) {
db_delete('batch')
->condition('bid', $batch['id'])
->execute();
foreach ($batch['sets'] as $batch_set) {
if ($queue = _batch_queue($batch_set)) {
$queue->deleteQueue();
}
}
// Clean-up the session. Not needed for CLI updates.
if (isset($_SESSION)) {
unset($_SESSION['batches'][$batch['id']]);
if (empty($_SESSION['batches'])) {
unset($_SESSION['batches']);
}
}
}
$_batch = $batch;
$batch = NULL;
// Redirect if needed.
if ($_batch['progressive']) {
// Revert the 'destination' that was saved in batch_process().
if (isset($_batch['destination'])) {
$_GET['destination'] = $_batch['destination'];
}
// Determine the target path to redirect to.
if (!isset($_batch['form_state']['redirect'])) {
if (isset($_batch['redirect'])) {
$_batch['form_state']['redirect'] = $_batch['redirect'];
}
else {
$_batch['form_state']['redirect'] = $_batch['source_url'];
}
}
// Use drupal_redirect_form() to handle the redirection logic.
drupal_redirect_form($_batch['form_state']);
// If no redirection happened, redirect to the originating page. In case the
// form needs to be rebuilt, save the final $form_state for
// drupal_build_form().
if (!empty($_batch['form_state']['rebuild'])) {
$_SESSION['batch_form_state'] = $_batch['form_state'];
}
$function = $_batch['redirect_callback'];
if (function_exists($function)) {
$function($_batch['source_url'], array('query' => array('op' => 'finish', 'id' => $_batch['id'])));
}
}
}
/**
* Shutdown function: Stores the current batch data for the next request.
*
* @see _batch_page()
* @see drupal_register_shutdown_function()
*/
function _batch_shutdown() {
if ($batch = batch_get()) {
db_update('batch')
->fields(array('batch' => serialize($batch)))
->condition('bid', $batch['id'])
->execute();
}
}
$this->name))->fetchObject();
if ($item) {
$item->data = unserialize($item->data);
return $item;
}
return FALSE;
}
/**
* Retrieves all remaining items in the queue.
*
* This is specific to Batch API and is not part of the DrupalQueueInterface.
*/
public function getAllItems() {
$result = array();
$items = db_query('SELECT data FROM {queue} q WHERE name = :name ORDER BY item_id ASC', array(':name' => $this->name))->fetchAll();
foreach ($items as $item) {
$result[] = unserialize($item->data);
}
return $result;
}
}
/**
* Defines a batch queue for non-progressive batches.
*/
class BatchMemoryQueue extends MemoryQueue {
/**
* Overrides MemoryQueue::claimItem().
*
* Unlike MemoryQueue::claimItem(), this method provides a default lease
* time of 0 (no expiration) instead of 30. This allows the item to be
* claimed repeatedly until it is deleted.
*/
public function claimItem($lease_time = 0) {
if (!empty($this->queue)) {
reset($this->queue);
return current($this->queue);
}
return FALSE;
}
/**
* Retrieves all remaining items in the queue.
*
* This is specific to Batch API and is not part of the DrupalQueueInterface.
*/
public function getAllItems() {
$result = array();
foreach ($this->queue as $item) {
$result[] = $item->data;
}
return $result;
}
}
direction.
*/
define('LANGUAGE_LTR', 0);
/**
* Language written right to left. Possible value of $language->direction.
*/
define('LANGUAGE_RTL', 1);
/**
* Time of the current request in seconds elapsed since the Unix Epoch.
*
* This differs from $_SERVER['REQUEST_TIME'], which is stored as a float
* since PHP 5.4.0. Float timestamps confuse most PHP functions
* (including date_create()).
*
* @see http://php.net/manual/reserved.variables.server.php
* @see http://php.net/manual/function.time.php
*/
define('REQUEST_TIME', (int) $_SERVER['REQUEST_TIME']);
/**
* Flag used to indicate that text is not sanitized, so run check_plain().
*
* @see drupal_set_title()
*/
define('CHECK_PLAIN', 0);
/**
* Flag used to indicate that text has already been sanitized.
*
* @see drupal_set_title()
*/
define('PASS_THROUGH', -1);
/**
* Signals that the registry lookup cache should be reset.
*/
define('REGISTRY_RESET_LOOKUP_CACHE', 1);
/**
* Signals that the registry lookup cache should be written to storage.
*/
define('REGISTRY_WRITE_LOOKUP_CACHE', 2);
/**
* Regular expression to match PHP function names.
*
* @see http://php.net/manual/language.functions.php
*/
define('DRUPAL_PHP_FUNCTION_PATTERN', '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*');
/**
* A RFC7231 Compliant date.
*
* http://tools.ietf.org/html/rfc7231#section-7.1.1.1
*
* Example: Sun, 06 Nov 1994 08:49:37 GMT
*
* This constant was introduced in PHP 7.0.19 and PHP 7.1.5 but needs to be
* defined by Drupal for earlier PHP versions.
*/
if (!defined('DATE_RFC7231')) {
define('DATE_RFC7231', 'D, d M Y H:i:s \G\M\T');
}
/**
* Provides a caching wrapper to be used in place of large array structures.
*
* This class should be extended by systems that need to cache large amounts
* of data and have it represented as an array to calling functions. These
* arrays can become very large, so ArrayAccess is used to allow different
* strategies to be used for caching internally (lazy loading, building caches
* over time etc.). This can dramatically reduce the amount of data that needs
* to be loaded from cache backends on each request, and memory usage from
* static caches of that same data.
*
* Note that array_* functions do not work with ArrayAccess. Systems using
* DrupalCacheArray should use this only internally. If providing API functions
* that return the full array, this can be cached separately or returned
* directly. However since DrupalCacheArray holds partial content by design, it
* should be a normal PHP array or otherwise contain the full structure.
*
* Note also that due to limitations in PHP prior to 5.3.4, it is impossible to
* write directly to the contents of nested arrays contained in this object.
* Only writes to the top-level array elements are possible. So if you
* previously had set $object['foo'] = array(1, 2, 'bar' => 'baz'), but later
* want to change the value of 'bar' from 'baz' to 'foobar', you cannot do so
* a targeted write like $object['foo']['bar'] = 'foobar'. Instead, you must
* overwrite the entire top-level 'foo' array with the entire set of new
* values: $object['foo'] = array(1, 2, 'bar' => 'foobar'). Due to this same
* limitation, attempts to create references to any contained data, nested or
* otherwise, will fail silently. So $var = &$object['foo'] will not throw an
* error, and $var will be populated with the contents of $object['foo'], but
* that data will be passed by value, not reference. For more information on
* the PHP limitation, see the note in the official PHP documentation at·
* http://php.net/manual/arrayaccess.offsetget.php on
* ArrayAccess::offsetGet().
*
* By default, the class accounts for caches where calling functions might
* request keys in the array that won't exist even after a cache rebuild. This
* prevents situations where a cache rebuild would be triggered over and over
* due to a 'missing' item. These cases are stored internally as a value of
* NULL. This means that the offsetGet() and offsetExists() methods
* must be overridden if caching an array where the top level values can
* legitimately be NULL, and where $object->offsetExists() needs to correctly
* return (equivalent to array_key_exists() vs. isset()). This should not
* be necessary in the majority of cases.
*
* Classes extending this class must override at least the
* resolveCacheMiss() method to have a working implementation.
*
* offsetSet() is not overridden by this class by default. In practice this
* means that assigning an offset via arrayAccess will only apply while the
* object is in scope and will not be written back to the persistent cache.
* This follows a similar pattern to static vs. persistent caching in
* procedural code. Extending classes may wish to alter this behavior, for
* example by overriding offsetSet() and adding an automatic call to persist().
*
* @see SchemaCache
*/
abstract class DrupalCacheArray implements ArrayAccess {
/**
* A cid to pass to cache_set() and cache_get().
*/
protected $cid;
/**
* A bin to pass to cache_set() and cache_get().
*/
protected $bin;
/**
* An array of keys to add to the cache at the end of the request.
*/
protected $keysToPersist = array();
/**
* Storage for the data itself.
*/
protected $storage = array();
/**
* Constructs a DrupalCacheArray object.
*
* @param $cid
* The cid for the array being cached.
* @param $bin
* The bin to cache the array.
*/
public function __construct($cid, $bin) {
$this->cid = $cid;
$this->bin = $bin;
if ($cached = cache_get($this->cid, $this->bin)) {
$this->storage = $cached->data;
}
}
/**
* Implements ArrayAccess::offsetExists().
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset) {
return $this->offsetGet($offset) !== NULL;
}
/**
* Implements ArrayAccess::offsetGet().
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset) {
if (isset($this->storage[$offset]) || array_key_exists($offset, $this->storage)) {
return $this->storage[$offset];
}
else {
return $this->resolveCacheMiss($offset);
}
}
/**
* Implements ArrayAccess::offsetSet().
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value) {
$this->storage[$offset] = $value;
}
/**
* Implements ArrayAccess::offsetUnset().
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset) {
unset($this->storage[$offset]);
}
/**
* Flags an offset value to be written to the persistent cache.
*
* If a value is assigned to a cache object with offsetSet(), by default it
* will not be written to the persistent cache unless it is flagged with this
* method. This allows items to be cached for the duration of a request,
* without necessarily writing back to the persistent cache at the end.
*
* @param $offset
* The array offset that was requested.
* @param $persist
* Optional boolean to specify whether the offset should be persisted or
* not, defaults to TRUE. When called with $persist = FALSE the offset will
* be unflagged so that it will not be written at the end of the request.
*/
protected function persist($offset, $persist = TRUE) {
$this->keysToPersist[$offset] = $persist;
}
/**
* Resolves a cache miss.
*
* When an offset is not found in the object, this is treated as a cache
* miss. This method allows classes implementing the interface to look up
* the actual value and allow it to be cached.
*
* @param $offset
* The offset that was requested.
*
* @return
* The value of the offset, or NULL if no value was found.
*/
abstract protected function resolveCacheMiss($offset);
/**
* Writes a value to the persistent cache immediately.
*
* @param $data
* The data to write to the persistent cache.
* @param $lock
* Whether to acquire a lock before writing to cache.
*/
protected function set($data, $lock = TRUE) {
// Lock cache writes to help avoid stampedes.
// To implement locking for cache misses, override __construct().
$lock_name = $this->cid . ':' . $this->bin;
if (!$lock || lock_acquire($lock_name)) {
if ($cached = cache_get($this->cid, $this->bin)) {
$data = $cached->data + $data;
}
cache_set($this->cid, $data, $this->bin);
if ($lock) {
lock_release($lock_name);
}
}
}
/**
* Destructs the DrupalCacheArray object.
*/
public function __destruct() {
$data = array();
foreach ($this->keysToPersist as $offset => $persist) {
if ($persist) {
$data[$offset] = $this->storage[$offset];
}
}
if (!empty($data)) {
$this->set($data);
}
}
}
/**
* Starts the timer with the specified name.
*
* If you start and stop the same timer multiple times, the measured intervals
* will be accumulated.
*
* @param $name
* The name of the timer.
*/
function timer_start($name) {
global $timers;
$timers[$name]['start'] = microtime(TRUE);
$timers[$name]['count'] = isset($timers[$name]['count']) ? ++$timers[$name]['count'] : 1;
}
/**
* Reads the current timer value without stopping the timer.
*
* @param $name
* The name of the timer.
*
* @return
* The current timer value in ms.
*/
function timer_read($name) {
global $timers;
if (isset($timers[$name]['start'])) {
$stop = microtime(TRUE);
$diff = round(($stop - $timers[$name]['start']) * 1000, 2);
if (isset($timers[$name]['time'])) {
$diff += $timers[$name]['time'];
}
return $diff;
}
return $timers[$name]['time'];
}
/**
* Stops the timer with the specified name.
*
* @param $name
* The name of the timer.
*
* @return
* A timer array. The array contains the number of times the timer has been
* started and stopped (count) and the accumulated timer value in ms (time).
*/
function timer_stop($name) {
global $timers;
if (isset($timers[$name]['start'])) {
$stop = microtime(TRUE);
$diff = round(($stop - $timers[$name]['start']) * 1000, 2);
if (isset($timers[$name]['time'])) {
$timers[$name]['time'] += $diff;
}
else {
$timers[$name]['time'] = $diff;
}
unset($timers[$name]['start']);
}
return $timers[$name];
}
/**
* Returns the appropriate configuration directory.
*
* Returns the configuration path based on the site's hostname, port, and
* pathname. See default.settings.php for examples on how the URL is converted
* to a directory.
*
* @param bool $require_settings
* Only configuration directories with an existing settings.php file
* will be recognized. Defaults to TRUE. During initial installation,
* this is set to FALSE so that Drupal can detect a matching directory,
* then create a new settings.php file in it.
* @param bool $reset
* Force a full search for matching directories even if one had been
* found previously. Defaults to FALSE.
*
* @return
* The path of the matching directory.
*
* @see default.settings.php
*/
function conf_path($require_settings = TRUE, $reset = FALSE) {
$conf = &drupal_static(__FUNCTION__, '');
if ($conf && !$reset) {
return $conf;
}
$confdir = 'sites';
$sites = array();
if (file_exists(DRUPAL_ROOT . '/' . $confdir . '/sites.php')) {
// This will overwrite $sites with the desired mappings.
include(DRUPAL_ROOT . '/' . $confdir . '/sites.php');
}
$uri = explode('/', $_SERVER['SCRIPT_NAME'] ? $_SERVER['SCRIPT_NAME'] : $_SERVER['SCRIPT_FILENAME']);
$server = explode('.', implode('.', array_reverse(explode(':', rtrim($_SERVER['HTTP_HOST'], '.')))));
for ($i = count($uri) - 1; $i > 0; $i--) {
for ($j = count($server); $j > 0; $j--) {
$dir = implode('.', array_slice($server, -$j)) . implode('.', array_slice($uri, 0, $i));
if (isset($sites[$dir]) && file_exists(DRUPAL_ROOT . '/' . $confdir . '/' . $sites[$dir])) {
$dir = $sites[$dir];
}
if (file_exists(DRUPAL_ROOT . '/' . $confdir . '/' . $dir . '/settings.php') || (!$require_settings && file_exists(DRUPAL_ROOT . '/' . $confdir . '/' . $dir))) {
$conf = "$confdir/$dir";
return $conf;
}
}
}
$conf = "$confdir/default";
return $conf;
}
/**
* Sets appropriate server variables needed for command line scripts to work.
*
* This function can be called by command line scripts before bootstrapping
* Drupal, to ensure that the page loads with the desired server parameters.
* This is because many parts of Drupal assume that they are running in a web
* browser and therefore use information from the global PHP $_SERVER variable
* that does not get set when Drupal is run from the command line.
*
* In many cases, the default way in which this function populates the $_SERVER
* variable is sufficient, and it can therefore be called without passing in
* any input. However, command line scripts running on a multisite installation
* (or on any installation that has settings.php stored somewhere other than
* the sites/default folder) need to pass in the URL of the site to allow
* Drupal to detect the correct location of the settings.php file. Passing in
* the 'url' parameter is also required for functions like request_uri() to
* return the expected values.
*
* Most other parameters do not need to be passed in, but may be necessary in
* some cases; for example, if Drupal's ip_address() function needs to return
* anything but the standard localhost value ('127.0.0.1'), the command line
* script should pass in the desired value via the 'REMOTE_ADDR' key.
*
* @param $variables
* (optional) An associative array of variables within $_SERVER that should
* be replaced. If the special element 'url' is provided in this array, it
* will be used to populate some of the server defaults; it should be set to
* the URL of the current page request, excluding any $_GET request but
* including the script name (e.g., http://www.example.com/mysite/index.php).
*
* @see conf_path()
* @see request_uri()
* @see ip_address()
*/
function drupal_override_server_variables($variables = array()) {
// Allow the provided URL to override any existing values in $_SERVER.
if (isset($variables['url'])) {
$url = parse_url($variables['url']);
if (isset($url['host'])) {
$_SERVER['HTTP_HOST'] = $url['host'];
}
if (isset($url['path'])) {
$_SERVER['SCRIPT_NAME'] = $url['path'];
}
unset($variables['url']);
}
// Define default values for $_SERVER keys. These will be used if $_SERVER
// does not already define them and no other values are passed in to this
// function.
$defaults = array(
'HTTP_HOST' => 'localhost',
'SCRIPT_NAME' => NULL,
'REMOTE_ADDR' => '127.0.0.1',
'REQUEST_METHOD' => 'GET',
'SERVER_NAME' => NULL,
'SERVER_SOFTWARE' => NULL,
'HTTP_USER_AGENT' => NULL,
);
// Replace elements of the $_SERVER array, as appropriate.
$_SERVER = $variables + $_SERVER + $defaults;
}
/**
* Initializes the PHP environment.
*/
function drupal_environment_initialize() {
if (!isset($_SERVER['HTTP_REFERER'])) {
$_SERVER['HTTP_REFERER'] = '';
}
if (!isset($_SERVER['SERVER_PROTOCOL']) || ($_SERVER['SERVER_PROTOCOL'] != 'HTTP/1.0' && $_SERVER['SERVER_PROTOCOL'] != 'HTTP/1.1')) {
$_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.0';
}
if (isset($_SERVER['HTTP_HOST'])) {
// As HTTP_HOST is user input, ensure it only contains characters allowed
// in hostnames. See RFC 952 (and RFC 2181).
// $_SERVER['HTTP_HOST'] is lowercased here per specifications.
$_SERVER['HTTP_HOST'] = strtolower($_SERVER['HTTP_HOST']);
if (!drupal_valid_http_host($_SERVER['HTTP_HOST'])) {
// HTTP_HOST is invalid, e.g. if containing slashes it may be an attack.
header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
exit;
}
}
else {
// Some pre-HTTP/1.1 clients will not send a Host header. Ensure the key is
// defined for E_ALL compliance.
$_SERVER['HTTP_HOST'] = '';
}
// When clean URLs are enabled, emulate ?q=foo/bar using REQUEST_URI. It is
// not possible to append the query string using mod_rewrite without the B
// flag (this was added in Apache 2.2.8), because mod_rewrite unescapes the
// path before passing it on to PHP. This is a problem when the path contains
// e.g. "&" or "%" that have special meanings in URLs and must be encoded.
$_GET['q'] = request_path();
// Enforce E_ALL, but allow users to set levels not part of E_ALL.
error_reporting(E_ALL | error_reporting());
// Override PHP settings required for Drupal to work properly.
// sites/default/default.settings.php contains more runtime settings.
// The .htaccess file contains settings that cannot be changed at runtime.
// Don't escape quotes when reading files from the database, disk, etc.
ini_set('magic_quotes_runtime', '0');
// Use session cookies, not transparent sessions that puts the session id in
// the query string.
// ini_set('session.use_cookies', '1');
// ini_set('session.use_only_cookies', '1');
// ini_set('session.use_trans_sid', '0');
// Don't send HTTP headers using PHP's session handler.
// An empty string is used here to disable the cache limiter.
// ini_set('session.cache_limiter', '');
// Use httponly session cookies.
// ini_set('session.cookie_httponly', '1');
// Set sane locale settings, to ensure consistent string, dates, times and
// numbers handling.
setlocale(LC_ALL, 'C');
// PHP's built-in phar:// stream wrapper is not sufficiently secure. Override
// it with a more secure one, which requires PHP 5.3.3. For lower versions,
// unregister the built-in one without replacing it. Sites needing phar
// support for lower PHP versions must implement hook_stream_wrappers() to
// register their desired implementation.
if (in_array('phar', stream_get_wrappers(), TRUE)) {
stream_wrapper_unregister('phar');
if (version_compare(PHP_VERSION, '5.3.3', '>=')) {
include_once DRUPAL_ROOT . '/includes/file.phar.inc';
file_register_phar_wrapper();
}
}
}
/**
* Validates that a hostname (for example $_SERVER['HTTP_HOST']) is safe.
*
* @return
* TRUE if only containing valid characters, or FALSE otherwise.
*/
function drupal_valid_http_host($host) {
// Limit the length of the host name to 1000 bytes to prevent DoS attacks with
// long host names.
return strlen($host) <= 1000
// Limit the number of subdomains and port separators to prevent DoS attacks
// in conf_path().
&& substr_count($host, '.') <= 100
&& substr_count($host, ':') <= 100
&& preg_match('/^\[?(?:[a-zA-Z0-9-:\]_]+\.?)+$/', $host);
}
/**
* Checks whether an HTTPS request is being served.
*
* @return bool
* TRUE if the request is HTTPS, FALSE otherwise.
*/
function drupal_is_https() {
return isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on';
}
/**
* Sets the base URL, cookie domain, and session name from configuration.
*/
function drupal_settings_initialize() {
global $base_url, $base_path, $base_root;
// Export these settings.php variables to the global namespace.
global $databases, $cookie_domain, $conf, $installed_profile, $update_free_access, $db_url, $db_prefix, $drupal_hash_salt, $is_https, $base_secure_url, $base_insecure_url;
$conf = array();
if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) {
include_once DRUPAL_ROOT . '/' . conf_path() . '/settings.php';
}
$is_https = drupal_is_https();
if (isset($base_url)) {
// Parse fixed base URL from settings.php.
$parts = parse_url($base_url);
if (!isset($parts['path'])) {
$parts['path'] = '';
}
$base_path = $parts['path'] . '/';
// Build $base_root (everything until first slash after "scheme://").
$base_root = substr($base_url, 0, strlen($base_url) - strlen($parts['path']));
}
else {
// Create base URL.
$http_protocol = $is_https ? 'https' : 'http';
$base_root = $http_protocol . '://' . $_SERVER['HTTP_HOST'];
$base_url = $base_root;
// $_SERVER['SCRIPT_NAME'] can, in contrast to $_SERVER['PHP_SELF'], not
// be modified by a visitor.
if ($dir = rtrim(dirname($_SERVER['SCRIPT_NAME']), '\/')) {
$base_path = $dir;
$base_url .= $base_path;
$base_path .= '/';
}
else {
$base_path = '/';
}
}
$base_secure_url = str_replace('http://', 'https://', $base_url);
$base_insecure_url = str_replace('https://', 'http://', $base_url);
if ($cookie_domain) {
// If the user specifies the cookie domain, also use it for session name.
$session_name = $cookie_domain;
}
else {
// // Otherwise use $base_url as session name, without the protocol
// // to use the same session identifiers across HTTP and HTTPS.
// list( , $session_name) = explode('://', $base_url, 2);
// // HTTP_HOST can be modified by a visitor, but we already sanitized it
// // in drupal_settings_initialize().
// if (!empty($_SERVER['HTTP_HOST'])) {
// $cookie_domain = _drupal_get_cookie_domain($_SERVER['HTTP_HOST']);
// }
// Drupal 7.83 included a security improvement whereby www. is no longer
// stripped from the cookie domain. However, this can cause problems with
// existing session cookies where some users are left unable to login. In
// order to avoid that, prepend a leading dot to the session_name that was
// derived from the base_url when a www. subdomain is in use.
// @see https://www.drupal.org/project/drupal/issues/2522002
if (strpos($session_name, 'www.') === 0) {
$session_name = '.' . $session_name;
}
}
// Per RFC 2109, cookie domains must contain at least one dot other than the
// first. For hosts such as 'localhost' or IP Addresses we don't set a cookie domain.
if (count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
ini_set('session.cookie_domain', $cookie_domain);
}
// To prevent session cookies from being hijacked, a user can configure the
// SSL version of their website to only transfer session cookies via SSL by
// using PHP's session.cookie_secure setting. The browser will then use two
// separate session cookies for the HTTPS and HTTP versions of the site. So we
// must use different session identifiers for HTTPS and HTTP to prevent a
// cookie collision.
if ($is_https) {
ini_set('session.cookie_secure', TRUE);
}
$prefix = ini_get('session.cookie_secure') ? 'SSESS' : 'SESS';
session_name($prefix . substr(hash('sha256', $session_name), 0, 32));
}
/**
* Derive the cookie domain to use for session cookies.
*
* @param $host
* The value of the HTTP host name.
*
* @return
* The string to use as a cookie domain.
*/
function _drupal_get_cookie_domain($host) {
$cookie_domain = $host;
// Strip leading periods and port numbers from cookie domain.
$cookie_domain = ltrim($cookie_domain, '.');
$cookie_domain = explode(':', $cookie_domain);
$cookie_domain = '.' . $cookie_domain[0];
return $cookie_domain;
}
/**
* Returns and optionally sets the filename for a system resource.
*
* The filename, whether provided, cached, or retrieved from the database, is
* only returned if the file exists.
*
* This function plays a key role in allowing Drupal's resources (modules
* and themes) to be located in different places depending on a site's
* configuration. For example, a module 'foo' may legally be located
* in any of these three places:
*
* modules/foo/foo.module
* sites/all/modules/foo/foo.module
* sites/example.com/modules/foo/foo.module
*
* Calling drupal_get_filename('module', 'foo') will give you one of
* the above, depending on where the module is located.
*
* @param $type
* The type of the item (theme, theme_engine, module, profile).
* @param $name
* The name of the item for which the filename is requested.
* @param $filename
* The filename of the item if it is to be set explicitly rather
* than by consulting the database.
* @param bool $trigger_error
* Whether to trigger an error when a file is missing or has unexpectedly
* moved. This defaults to TRUE, but can be set to FALSE by calling code that
* merely wants to check whether an item exists in the filesystem.
*
* @return
* The filename of the requested item or NULL if the item is not found.
*/
function drupal_get_filename($type, $name, $filename = NULL, $trigger_error = TRUE) {
// The $files static variable will hold the locations of all requested files.
// We can be sure that any file listed in this static variable actually
// exists as all additions have gone through a file_exists() check.
// The location of files will not change during the request, so do not use
// drupal_static().
static $files = array();
// Profiles are a special case: they have a fixed location and naming.
if ($type == 'profile') {
$profile_filename = "profiles/$name/$name.profile";
$files[$type][$name] = file_exists($profile_filename) ? $profile_filename : FALSE;
}
if (!isset($files[$type])) {
$files[$type] = array();
}
if (!empty($filename) && file_exists($filename)) {
// Prime the static cache with the provided filename.
$files[$type][$name] = $filename;
}
elseif (isset($files[$type][$name])) {
// This item had already been found earlier in the request, either through
// priming of the static cache (for example, in system_list()), through a
// lookup in the {system} table, or through a file scan (cached or not). Do
// nothing.
}
else {
// Look for the filename listed in the {system} table. Verify that we have
// an active database connection before doing so, since this function is
// called both before we have a database connection (i.e. during
// installation) and when a database connection fails.
$database_unavailable = TRUE;
try {
if (function_exists('db_query')) {
$file = db_query("SELECT filename FROM {system} WHERE name = :name AND type = :type", array(':name' => $name, ':type' => $type))->fetchField();
if ($file !== FALSE && file_exists(DRUPAL_ROOT . '/' . $file)) {
$files[$type][$name] = $file;
}
$database_unavailable = FALSE;
}
}
catch (Exception $e) {
// The database table may not exist because Drupal is not yet installed,
// the database might be down, or we may have done a non-database cache
// flush while $conf['page_cache_without_database'] = TRUE and
// $conf['page_cache_invoke_hooks'] = TRUE. We have a fallback for these
// cases so we hide the error completely.
}
// Fall back to searching the filesystem if the database could not find the
// file or the file does not exist at the path returned by the database.
if (!isset($files[$type][$name])) {
$files[$type][$name] = _drupal_get_filename_fallback($type, $name, $trigger_error, $database_unavailable);
}
}
if (isset($files[$type][$name])) {
return $files[$type][$name];
}
}
/**
* Performs a cached file system scan as a fallback when searching for a file.
*
* This function looks for the requested file by triggering a file scan,
* caching the new location if the file has moved and caching the miss
* if the file is missing. If a file had been marked as missing in a previous
* file scan, or if it has been marked as moved and is still in the last known
* location, no new file scan will be performed.
*
* @param string $type
* The type of the item (theme, theme_engine, module, profile).
* @param string $name
* The name of the item for which the filename is requested.
* @param bool $trigger_error
* Whether to trigger an error when a file is missing or has unexpectedly
* moved.
* @param bool $database_unavailable
* Whether this function is being called because the Drupal database could
* not be queried for the file's location.
*
* @return
* The filename of the requested item or NULL if the item is not found.
*
* @see drupal_get_filename()
*/
function _drupal_get_filename_fallback($type, $name, $trigger_error, $database_unavailable) {
$file_scans = &_drupal_file_scan_cache();
$filename = NULL;
// If the cache indicates that the item is missing, or we can verify that the
// item exists in the location the cache says it exists in, use that.
if (isset($file_scans[$type][$name]) && ($file_scans[$type][$name] === FALSE || file_exists($file_scans[$type][$name]))) {
$filename = $file_scans[$type][$name];
}
// Otherwise, perform a new file scan to find the item.
else {
$filename = _drupal_get_filename_perform_file_scan($type, $name);
// Update the static cache, and mark the persistent cache for updating at
// the end of the page request. See drupal_file_scan_write_cache().
$file_scans[$type][$name] = $filename;
$file_scans['#write_cache'] = TRUE;
}
// If requested, trigger a user-level warning about the missing or
// unexpectedly moved file. If the database was unavailable, do not trigger a
// warning in the latter case, though, since if the {system} table could not
// be queried there is no way to know if the location found here was
// "unexpected" or not.
if ($trigger_error) {
$error_type = $filename === FALSE ? 'missing' : 'moved';
if ($error_type == 'missing' || !$database_unavailable) {
_drupal_get_filename_fallback_trigger_error($type, $name, $error_type);
}
}
// The cache stores FALSE for files that aren't found (to be able to
// distinguish them from files that have not yet been searched for), but
// drupal_get_filename() expects NULL for these instead, so convert to NULL
// before returning.
if ($filename === FALSE) {
$filename = NULL;
}
return $filename;
}
/**
* Returns the current list of cached file system scan results.
*
* @return
* An associative array tracking the most recent file scan results for all
* files that have had scans performed. The keys are the type and name of the
* item that was searched for, and the values can be either:
* - Boolean FALSE if the item was not found in the file system.
* - A string pointing to the location where the item was found.
*/
function &_drupal_file_scan_cache() {
$file_scans = &drupal_static(__FUNCTION__, array());
// The file scan results are stored in a persistent cache (in addition to the
// static cache) but because this function can be called before the
// persistent cache is available, we must merge any items that were found
// earlier in the page request into the results from the persistent cache.
if (!isset($file_scans['#cache_merge_done'])) {
try {
if (function_exists('cache_get')) {
$cache = cache_get('_drupal_file_scan_cache', 'cache_bootstrap');
if (!empty($cache->data)) {
// File scan results from the current request should take precedence
// over the results from the persistent cache, since they are newer.
$file_scans = drupal_array_merge_deep($cache->data, $file_scans);
}
// Set a flag to indicate that the persistent cache does not need to be
// merged again.
$file_scans['#cache_merge_done'] = TRUE;
}
}
catch (Exception $e) {
// Hide the error.
}
}
return $file_scans;
}
/**
* Performs a file system scan to search for a system resource.
*
* @param $type
* The type of the item (theme, theme_engine, module, profile).
* @param $name
* The name of the item for which the filename is requested.
*
* @return
* The filename of the requested item or FALSE if the item is not found.
*
* @see drupal_get_filename()
* @see _drupal_get_filename_fallback()
*/
function _drupal_get_filename_perform_file_scan($type, $name) {
// The location of files will not change during the request, so do not use
// drupal_static().
static $dirs = array(), $files = array();
// We have a consistent directory naming: modules, themes...
$dir = $type . 's';
if ($type == 'theme_engine') {
$dir = 'themes/engines';
$extension = 'engine';
}
elseif ($type == 'theme') {
$extension = 'info';
}
else {
$extension = $type;
}
// Check if we had already scanned this directory/extension combination.
if (!isset($dirs[$dir][$extension])) {
// Log that we have now scanned this directory/extension combination
// into a static variable so as to prevent unnecessary file scans.
$dirs[$dir][$extension] = TRUE;
if (!function_exists('drupal_system_listing')) {
require_once DRUPAL_ROOT . '/includes/common.inc';
}
// Scan the appropriate directories for all files with the requested
// extension, not just the file we are currently looking for. This
// prevents unnecessary scans from being repeated when this function is
// called more than once in the same page request.
$matches = drupal_system_listing("/^" . DRUPAL_PHP_FUNCTION_PATTERN . "\.$extension$/", $dir, 'name', 0);
foreach ($matches as $matched_name => $file) {
// Log the locations found in the file scan into a static variable.
$files[$type][$matched_name] = $file->uri;
}
}
// Return the results of the file system scan, or FALSE to indicate the file
// was not found.
return isset($files[$type][$name]) ? $files[$type][$name] : FALSE;
}
/**
* Triggers a user-level warning for missing or unexpectedly moved files.
*
* @param $type
* The type of the item (theme, theme_engine, module, profile).
* @param $name
* The name of the item for which the filename is requested.
* @param $error_type
* The type of the error ('missing' or 'moved').
*
* @see drupal_get_filename()
* @see _drupal_get_filename_fallback()
*/
function _drupal_get_filename_fallback_trigger_error($type, $name, $error_type) {
// Hide messages due to known bugs that will appear on a lot of sites.
// @todo Remove this in https://www.drupal.org/node/2383823
if (empty($name)) {
return;
}
// Make sure we only show any missing or moved file errors only once per
// request.
static $errors_triggered = array();
if (empty($errors_triggered[$type][$name][$error_type])) {
// Use _drupal_trigger_error_with_delayed_logging() here since these are
// triggered during low-level operations that cannot necessarily be
// interrupted by a watchdog() call.
if ($error_type == 'missing') {
_drupal_trigger_error_with_delayed_logging(format_string('The following @type is missing from the file system: %name. For information about how to fix this, see the documentation page .', array('@type' => $type, '%name' => $name, '@documentation' => 'https://www.drupal.org/node/2487215')), E_USER_WARNING);
}
elseif ($error_type == 'moved') {
_drupal_trigger_error_with_delayed_logging(format_string('The following @type has moved within the file system: %name. In order to fix this, clear caches or put the @type back in its original location. For more information, see the documentation page .', array('@type' => $type, '%name' => $name, '@documentation' => 'https://www.drupal.org/node/2487215')), E_USER_WARNING);
}
$errors_triggered[$type][$name][$error_type] = TRUE;
}
}
/**
* Invokes trigger_error() with logging delayed until the end of the request.
*
* This is an alternative to PHP's trigger_error() function which can be used
* during low-level Drupal core operations that need to avoid being interrupted
* by a watchdog() call.
*
* Normally, Drupal's error handler calls watchdog() in response to a
* trigger_error() call. However, this invokes hook_watchdog() which can run
* arbitrary code. If the trigger_error() happens in the middle of an
* operation such as a rebuild operation which should not be interrupted by
* arbitrary code, that could potentially break or trigger the rebuild again.
* This function protects against that by delaying the watchdog() call until
* the end of the current page request.
*
* This is an internal function which should only be called by low-level Drupal
* core functions. It may be removed in a future Drupal 7 release.
*
* @param string $error_msg
* The error message to trigger. As with trigger_error() itself, this is
* limited to 1024 bytes; additional characters beyond that will be removed.
* @param int $error_type
* (optional) The type of error. This should be one of the E_USER family of
* constants. As with trigger_error() itself, this defaults to E_USER_NOTICE
* if not provided.
*
* @see _drupal_log_error()
*/
function _drupal_trigger_error_with_delayed_logging($error_msg, $error_type = E_USER_NOTICE) {
$delay_logging = &drupal_static(__FUNCTION__, FALSE);
$delay_logging = TRUE;
trigger_error($error_msg, $error_type);
$delay_logging = FALSE;
}
/**
* Invoke trigger_error() using a fatal error that will terminate the request.
*
* Normally, Drupal's error handler does not terminate script execution on
* user-level errors, even if the error is of type E_USER_ERROR. This function
* triggers an error of type E_USER_ERROR that is explicitly forced to be a
* fatal error which terminates script execution.
*
* @param string $error_msg
* The error message to trigger. As with trigger_error() itself, this is
* limited to 1024 bytes; additional characters beyond that will be removed.
*
* @see _drupal_error_handler_real()
*/
function drupal_trigger_fatal_error($error_msg) {
$fatal_error = &drupal_static(__FUNCTION__, FALSE);
$fatal_error = TRUE;
trigger_error($error_msg, E_USER_ERROR);
$fatal_error = FALSE;
// The standard Drupal error handler should have treated this as a fatal
// error and already ended the page request. But in case another error
// handler is being used, terminate execution explicitly here also.
exit;
}
/**
* Writes the file scan cache to the persistent cache.
*
* This cache stores all files marked as missing or moved after a file scan
* to prevent unnecessary file scans in subsequent requests. This cache is
* cleared in system_list_reset() (i.e. after a module/theme rebuild).
*/
function drupal_file_scan_write_cache() {
// Only write to the persistent cache if requested, and if we know that any
// data previously in the cache was successfully loaded and merged in by
// _drupal_file_scan_cache().
$file_scans = &_drupal_file_scan_cache();
if (isset($file_scans['#write_cache']) && isset($file_scans['#cache_merge_done'])) {
unset($file_scans['#write_cache']);
cache_set('_drupal_file_scan_cache', $file_scans, 'cache_bootstrap');
}
}
/**
* Loads the persistent variable table.
*
* The variable table is composed of values that have been saved in the table
* with variable_set() as well as those explicitly specified in the
* configuration file.
*/
function variable_initialize($conf = array()) {
// NOTE: caching the variables improves performance by 20% when serving
// cached pages.
if ($cached = cache_get('variables', 'cache_bootstrap')) {
$variables = $cached->data;
}
else {
// Cache miss. Avoid a stampede by acquiring a lock. If the lock fails to
// acquire, optionally just continue with uncached processing.
$name = 'variable_init';
$lock_acquired = lock_acquire($name, 1);
if (!$lock_acquired && variable_get('variable_initialize_wait_for_lock', FALSE)) {
lock_wait($name);
return variable_initialize($conf);
}
else {
// Load the variables from the table.
$variables = array_map('unserialize', db_query('SELECT name, value FROM {variable}')->fetchAllKeyed());
if ($lock_acquired) {
cache_set('variables', $variables, 'cache_bootstrap');
lock_release($name);
}
}
}
foreach ($conf as $name => $value) {
$variables[$name] = $value;
}
return $variables;
}
/**
* Returns a persistent variable.
*
* Case-sensitivity of the variable_* functions depends on the database
* collation used. To avoid problems, always use lower case for persistent
* variable names.
*
* @param $name
* The name of the variable to return.
* @param $default
* The default value to use if this variable has never been set.
*
* @return
* The value of the variable. Unserialization is taken care of as necessary.
*
* @see variable_del()
* @see variable_set()
*/
function variable_get($name, $default = NULL) {
global $conf;
return isset($conf[$name]) ? $conf[$name] : $default;
}
/**
* Sets a persistent variable.
*
* Case-sensitivity of the variable_* functions depends on the database
* collation used. To avoid problems, always use lower case for persistent
* variable names.
*
* @param $name
* The name of the variable to set.
* @param $value
* The value to set. This can be any PHP data type; these functions take care
* of serialization as necessary.
*
* @see variable_del()
* @see variable_get()
*/
function variable_set($name, $value) {
global $conf;
db_merge('variable')->key(array('name' => $name))->fields(array('value' => serialize($value)))->execute();
cache_clear_all('variables', 'cache_bootstrap');
$conf[$name] = $value;
}
/**
* Unsets a persistent variable.
*
* Case-sensitivity of the variable_* functions depends on the database
* collation used. To avoid problems, always use lower case for persistent
* variable names.
*
* @param $name
* The name of the variable to undefine.
*
* @see variable_get()
* @see variable_set()
*/
function variable_del($name) {
global $conf;
db_delete('variable')
->condition('name', $name)
->execute();
cache_clear_all('variables', 'cache_bootstrap');
unset($conf[$name]);
}
/**
* Retrieves the current page from the cache.
*
* Note: we do not serve cached pages to authenticated users, or to anonymous
* users when $_SESSION is non-empty. $_SESSION may contain status messages
* from a form submission, the contents of a shopping cart, or other user-
* specific content that should not be cached and displayed to other users.
*
* @param $check_only
* (optional) Set to TRUE to only return whether a previous call found a
* cache entry.
*
* @return
* The cache object, if the page was found in the cache, NULL otherwise.
*/
function drupal_page_get_cache($check_only = FALSE) {
global $base_root;
static $cache_hit = FALSE;
if ($check_only) {
return $cache_hit;
}
if (drupal_page_is_cacheable()) {
$cache = cache_get($base_root . request_uri(), 'cache_page');
if ($cache !== FALSE) {
$cache_hit = TRUE;
}
return $cache;
}
}
/**
* Determines the cacheability of the current page.
*
* @param $allow_caching
* Set to FALSE if you want to prevent this page from being cached.
*
* @return
* TRUE if the current page can be cached, FALSE otherwise.
*/
function drupal_page_is_cacheable($allow_caching = NULL) {
$allow_caching_static = &drupal_static(__FUNCTION__, TRUE);
if (isset($allow_caching)) {
$allow_caching_static = $allow_caching;
}
return $allow_caching_static && ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD')
&& !drupal_is_cli();
}
/**
* Invokes a bootstrap hook in all bootstrap modules that implement it.
*
* @param $hook
* The name of the bootstrap hook to invoke.
*
* @see bootstrap_hooks()
*/
function bootstrap_invoke_all($hook) {
// Bootstrap modules should have been loaded when this function is called, so
// we don't need to tell module_list() to reset its internal list (and we
// therefore leave the first parameter at its default value of FALSE). We
// still pass in TRUE for the second parameter, though; in case this is the
// first time during the bootstrap that module_list() is called, we want to
// make sure that its internal cache is primed with the bootstrap modules
// only.
foreach (module_list(FALSE, TRUE) as $module) {
drupal_load('module', $module);
module_invoke($module, $hook);
}
}
/**
* Includes a file with the provided type and name.
*
* This prevents including a theme, engine, module, etc., more than once.
*
* @param $type
* The type of item to load (i.e. theme, theme_engine, module).
* @param $name
* The name of the item to load.
*
* @return
* TRUE if the item is loaded or has already been loaded.
*/
function drupal_load($type, $name) {
// Once a file is included this can't be reversed during a request so do not
// use drupal_static() here.
static $files = array();
if (isset($files[$type][$name])) {
return TRUE;
}
$filename = drupal_get_filename($type, $name);
if ($filename) {
include_once DRUPAL_ROOT . '/' . $filename;
$files[$type][$name] = TRUE;
return TRUE;
}
return FALSE;
}
/**
* Sets an HTTP response header for the current page.
*
* Note: When sending a Content-Type header, always include a 'charset' type,
* too. This is necessary to avoid security bugs (e.g. UTF-7 XSS).
*
* @param $name
* The HTTP header name, or the special 'Status' header name.
* @param $value
* The HTTP header value; if equal to FALSE, the specified header is unset.
* If $name is 'Status', this is expected to be a status code followed by a
* reason phrase, e.g. "404 Not Found".
* @param $append
* Whether to append the value to an existing header or to replace it.
*/
function drupal_add_http_header($name, $value, $append = FALSE) {
// The headers as name/value pairs.
$headers = &drupal_static('drupal_http_headers', array());
$name_lower = strtolower($name);
_drupal_set_preferred_header_name($name);
if ($value === FALSE) {
$headers[$name_lower] = FALSE;
}
elseif (isset($headers[$name_lower]) && $append) {
// Multiple headers with identical names may be combined using comma (RFC
// 2616, section 4.2).
$headers[$name_lower] .= ',' . $value;
}
else {
$headers[$name_lower] = $value;
}
drupal_send_headers(array($name => $headers[$name_lower]), TRUE);
}
/**
* Gets the HTTP response headers for the current page.
*
* @param $name
* An HTTP header name. If omitted, all headers are returned as name/value
* pairs. If an array value is FALSE, the header has been unset.
*
* @return
* A string containing the header value, or FALSE if the header has been set,
* or NULL if the header has not been set.
*/
function drupal_get_http_header($name = NULL) {
$headers = &drupal_static('drupal_http_headers', array());
if (isset($name)) {
$name = strtolower($name);
return isset($headers[$name]) ? $headers[$name] : NULL;
}
else {
return $headers;
}
}
/**
* Sets the preferred name for the HTTP header.
*
* Header names are case-insensitive, but for maximum compatibility they should
* follow "common form" (see RFC 2617, section 4.2).
*/
function _drupal_set_preferred_header_name($name = NULL) {
static $header_names = array();
if (!isset($name)) {
return $header_names;
}
$header_names[strtolower($name)] = $name;
}
/**
* Sends the HTTP response headers that were previously set, adding defaults.
*
* Headers are set in drupal_add_http_header(). Default headers are not set
* if they have been replaced or unset using drupal_add_http_header().
*
* @param array $default_headers
* (optional) An array of headers as name/value pairs.
* @param bool $only_default
* (optional) If TRUE and headers have already been sent, send only the
* specified headers.
*/
function drupal_send_headers($default_headers = array(), $only_default = FALSE) {
$headers_sent = &drupal_static(__FUNCTION__, FALSE);
$headers = drupal_get_http_header();
if ($only_default && $headers_sent) {
$headers = array();
}
$headers_sent = TRUE;
$header_names = _drupal_set_preferred_header_name();
foreach ($default_headers as $name => $value) {
$name_lower = strtolower($name);
if (!isset($headers[$name_lower])) {
$headers[$name_lower] = $value;
$header_names[$name_lower] = $name;
}
}
foreach ($headers as $name_lower => $value) {
if ($name_lower == 'status') {
header($_SERVER['SERVER_PROTOCOL'] . ' ' . $value);
}
// Skip headers that have been unset.
elseif ($value !== FALSE) {
header($header_names[$name_lower] . ': ' . $value);
}
}
}
/**
* Sets HTTP headers in preparation for a page response.
*
* Authenticated users are always given a 'no-cache' header, and will fetch a
* fresh page on every request. This prevents authenticated users from seeing
* locally cached pages.
*
* ETag and Last-Modified headers are not set per default for authenticated
* users so that browsers do not send If-Modified-Since headers from
* authenticated user pages. drupal_serve_page_from_cache() will set appropriate
* ETag and Last-Modified headers for cached pages.
*
* @see drupal_page_set_cache()
*/
function drupal_page_header() {
$headers_sent = &drupal_static(__FUNCTION__, FALSE);
if ($headers_sent) {
return TRUE;
}
$headers_sent = TRUE;
$default_headers = array(
'Expires' => 'Sun, 19 Nov 1978 05:00:00 GMT',
'Cache-Control' => 'no-cache, must-revalidate',
// Prevent browsers from sniffing a response and picking a MIME type
// different from the declared content-type, since that can lead to
// XSS and other vulnerabilities.
'X-Content-Type-Options' => 'nosniff',
);
drupal_send_headers($default_headers);
}
/**
* Sets HTTP headers in preparation for a cached page response.
*
* The headers allow as much as possible in proxies and browsers without any
* particular knowledge about the pages. Modules can override these headers
* using drupal_add_http_header().
*
* If the request is conditional (using If-Modified-Since and If-None-Match),
* and the conditions match those currently in the cache, a 304 Not Modified
* response is sent.
*/
function drupal_serve_page_from_cache(stdClass $cache) {
// Negotiate whether to use compression.
$page_compression = !empty($cache->data['page_compressed']) && !empty($cache->data['body']);
$return_compressed = $page_compression && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE;
// Get headers set in hook_boot(). Keys are lower-case.
$hook_boot_headers = drupal_get_http_header();
// Headers generated in this function, that may be replaced or unset using
// drupal_add_http_headers(). Keys are mixed-case.
$default_headers = array();
foreach ($cache->data['headers'] as $name => $value) {
// In the case of a 304 response, certain headers must be sent, and the
// remaining may not (see RFC 2616, section 10.3.5). Do not override
// headers set in hook_boot().
$name_lower = strtolower($name);
if (in_array($name_lower, array('content-location', 'expires', 'cache-control', 'vary')) && !isset($hook_boot_headers[$name_lower])) {
drupal_add_http_header($name, $value);
unset($cache->data['headers'][$name]);
}
}
// If the client sent a session cookie, a cached copy will only be served
// to that one particular client due to Vary: Cookie. Thus, do not set
// max-age > 0, allowing the page to be cached by external proxies, when a
// session cookie is present unless the Vary header has been replaced or
// unset in hook_boot().
$max_age = !isset($_COOKIE[session_name()]) || isset($hook_boot_headers['vary']) ? variable_get('page_cache_maximum_age', 0) : 0;
$default_headers['Cache-Control'] = 'public, max-age=' . $max_age;
// Entity tag should change if the output changes.
$etag = '"' . $cache->created . '-' . intval($return_compressed) . '"';
header('Etag: ' . $etag);
// See if the client has provided the required HTTP headers.
$if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : FALSE;
$if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) : FALSE;
if ($if_modified_since && $if_none_match
&& $if_none_match == $etag // etag must match
&& $if_modified_since == $cache->created) { // if-modified-since must match
header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified');
drupal_send_headers($default_headers);
return;
}
// Send the remaining headers.
foreach ($cache->data['headers'] as $name => $value) {
drupal_add_http_header($name, $value);
}
$default_headers['Last-Modified'] = gmdate(DATE_RFC7231, $cache->created);
// HTTP/1.0 proxies does not support the Vary header, so prevent any caching
// by sending an Expires date in the past. HTTP/1.1 clients ignores the
// Expires header if a Cache-Control: max-age= directive is specified (see RFC
// 2616, section 14.9.3).
$default_headers['Expires'] = 'Sun, 19 Nov 1978 05:00:00 GMT';
drupal_send_headers($default_headers);
// Allow HTTP proxies to cache pages for anonymous users without a session
// cookie. The Vary header is used to indicates the set of request-header
// fields that fully determines whether a cache is permitted to use the
// response to reply to a subsequent request for a given URL without
// revalidation. If a Vary header has been set in hook_boot(), it is assumed
// that the module knows how to cache the page.
if (!isset($hook_boot_headers['vary']) && !variable_get('omit_vary_cookie')) {
header('Vary: Cookie');
}
if ($page_compression) {
header('Vary: Accept-Encoding', FALSE);
// If page_compression is enabled, the cache contains gzipped data.
if ($return_compressed) {
// $cache->data['body'] is already gzip'ed, so make sure
// zlib.output_compression does not compress it once more.
ini_set('zlib.output_compression', '0');
header('Content-Encoding: gzip');
}
else {
// The client does not support compression, so unzip the data in the
// cache. Strip the gzip header and run uncompress.
$cache->data['body'] = gzinflate(substr(substr($cache->data['body'], 10), 0, -8));
}
}
// Print the page.
print $cache->data['body'];
}
/**
* Defines the critical hooks that force modules to always be loaded.
*/
function bootstrap_hooks() {
return array('boot', 'exit', 'watchdog', 'language_init');
}
/**
* Unserializes and appends elements from a serialized string.
*
* @param $obj
* The object to which the elements are appended.
* @param $field
* The attribute of $obj whose value should be unserialized.
*/
function drupal_unpack($obj, $field = 'data') {
if ($obj->$field && $data = unserialize($obj->$field)) {
foreach ($data as $key => $value) {
if (!empty($key) && !isset($obj->$key)) {
$obj->$key = $value;
}
}
}
return $obj;
}
/**
* Translates a string to the current language or to a given language.
*
* The t() function serves two purposes. First, at run-time it translates
* user-visible text into the appropriate language. Second, various mechanisms
* that figure out what text needs to be translated work off t() -- the text
* inside t() calls is added to the database of strings to be translated.
* These strings are expected to be in English, so the first argument should
* always be in English. To enable a fully-translatable site, it is important
* that all human-readable text that will be displayed on the site or sent to
* a user is passed through the t() function, or a related function. See the
* @link http://drupal.org/node/322729 Localization API @endlink pages for
* more information, including recommendations on how to break up or not
* break up strings for translation.
*
* @section sec_translating_vars Translating Variables
* You should never use t() to translate variables, such as calling
* @code t($text); @endcode, unless the text that the variable holds has been
* passed through t() elsewhere (e.g., $text is one of several translated
* literal strings in an array). It is especially important never to call
* @code t($user_text); @endcode, where $user_text is some text that a user
* entered - doing that can lead to cross-site scripting and other security
* problems. However, you can use variable substitution in your string, to put
* variable text such as user names or link URLs into translated text. Variable
* substitution looks like this:
* @code
* $text = t("@name's blog", array('@name' => format_username($account)));
* @endcode
* Basically, you can put variables like @name into your string, and t() will
* substitute their sanitized values at translation time. (See the
* Localization API pages referenced above and the documentation of
* format_string() for details about how to define variables in your string.)
* Translators can then rearrange the string as necessary for the language
* (e.g., in Spanish, it might be "blog de @name").
*
* @section sec_alt_funcs_install Use During Installation Phase
* During the Drupal installation phase, some resources used by t() wil not be
* available to code that needs localization. See st() and get_t() for
* alternatives.
*
* @section sec_context String context
* Matching source strings are normally only translated once, and the same
* translation is used everywhere that has a matching string. However, in some
* cases, a certain English source string needs to have multiple translations.
* One example of this is the string "May", which could be used as either a
* full month name or a 3-letter abbreviated month. In other languages where
* the month name for May has more than 3 letters, you would need to provide
* two different translations (one for the full name and one abbreviated), and
* the correct form would need to be chosen, depending on how "May" is being
* used. To facilitate this, the "May" string should be provided with two
* different contexts in the $options parameter when calling t(). For example:
* @code
* t('May', array(), array('context' => 'Long month name')
* t('May', array(), array('context' => 'Abbreviated month name')
* @endcode
* See https://localize.drupal.org/node/2109 for more information.
*
* @param $string
* A string containing the English string to translate.
* @param $args
* An associative array of replacements to make after translation. Based
* on the first character of the key, the value is escaped and/or themed.
* See format_string() for details.
* @param $options
* An associative array of additional options, with the following elements:
* - 'langcode' (defaults to the current language): The language code to
* translate to a language other than what is used to display the page.
* - 'context' (defaults to the empty context): A string giving the context
* that the source string belongs to. See @ref sec_context above for more
* information.
*
* @return
* The translated string.
*
* @see st()
* @see get_t()
* @see format_string()
* @ingroup sanitization
*/
function t($string, array $args = array(), array $options = array()) {
global $language;
static $custom_strings;
// Merge in default.
if (empty($options['langcode'])) {
$options['langcode'] = isset($language->language) ? $language->language : 'en';
}
if (empty($options['context'])) {
$options['context'] = '';
}
// First, check for an array of customized strings. If present, use the array
// *instead of* database lookups. This is a high performance way to provide a
// handful of string replacements. See settings.php for examples.
// Cache the $custom_strings variable to improve performance.
if (!isset($custom_strings[$options['langcode']])) {
$custom_strings[$options['langcode']] = variable_get('locale_custom_strings_' . $options['langcode'], array());
}
// Custom strings work for English too, even if locale module is disabled.
if (isset($custom_strings[$options['langcode']][$options['context']][$string])) {
$string = $custom_strings[$options['langcode']][$options['context']][$string];
}
// Translate with locale module if enabled.
elseif ($options['langcode'] != 'en' && function_exists('locale')) {
$string = locale($string, $options['context'], $options['langcode']);
}
if (empty($args)) {
return $string;
}
else {
return format_string($string, $args);
}
}
/**
* Formats a string for HTML display by replacing variable placeholders.
*
* This function replaces variable placeholders in a string with the requested
* values and escapes the values so they can be safely displayed as HTML. It
* should be used on any unknown text that is intended to be printed to an HTML
* page (especially text that may have come from untrusted users, since in that
* case it prevents cross-site scripting and other security problems).
*
* In most cases, you should use t() rather than calling this function
* directly, since it will translate the text (on non-English-only sites) in
* addition to formatting it.
*
* @param $string
* A string containing placeholders.
* @param $args
* An associative array of replacements to make. Occurrences in $string of
* any key in $args are replaced with the corresponding value, after optional
* sanitization and formatting. The type of sanitization and formatting
* depends on the first character of the key:
* - @variable: Escaped to HTML using check_plain(). Use this as the default
* choice for anything displayed on a page on the site.
* - %variable: Escaped to HTML and formatted using drupal_placeholder(),
* which makes it display as emphasized text.
* - !variable: Inserted as is, with no sanitization or formatting. Only use
* this for text that has already been prepared for HTML display (for
* example, user-supplied text that has already been run through
* check_plain() previously, or is expected to contain some limited HTML
* tags and has already been run through filter_xss() previously).
*
* @see t()
* @ingroup sanitization
*/
function format_string($string, array $args = array()) {
// Transform arguments before inserting them.
foreach ($args as $key => $value) {
switch ($key[0]) {
case '@':
// Escaped only.
$args[$key] = check_plain($value);
break;
case '%':
default:
// Escaped and placeholder.
$args[$key] = drupal_placeholder($value);
break;
case '!':
// Pass-through.
}
}
return strtr($string, $args);
}
/**
* Encodes special characters in a plain-text string for display as HTML.
*
* Also validates strings as UTF-8 to prevent cross site scripting attacks on
* Internet Explorer 6.
*
* @param string $text
* The text to be checked or processed.
*
* @return string
* An HTML safe version of $text. If $text is not valid UTF-8, an empty string
* is returned and, on PHP < 5.4, a warning may be issued depending on server
* configuration (see @link https://bugs.php.net/bug.php?id=47494 @endlink).
*
* @see drupal_validate_utf8()
* @ingroup sanitization
*/
function check_plain($text) {
return htmlspecialchars((string) $text, ENT_QUOTES, 'UTF-8');
}
/**
* Checks whether a string is valid UTF-8.
*
* All functions designed to filter input should use drupal_validate_utf8
* to ensure they operate on valid UTF-8 strings to prevent bypass of the
* filter.
*
* When text containing an invalid UTF-8 lead byte (0xC0 - 0xFF) is presented
* as UTF-8 to Internet Explorer 6, the program may misinterpret subsequent
* bytes. When these subsequent bytes are HTML control characters such as
* quotes or angle brackets, parts of the text that were deemed safe by filters
* end up in locations that are potentially unsafe; An onerror attribute that
* is outside of a tag, and thus deemed safe by a filter, can be interpreted
* by the browser as if it were inside the tag.
*
* The function does not return FALSE for strings containing character codes
* above U+10FFFF, even though these are prohibited by RFC 3629.
*
* @param $text
* The text to check.
*
* @return
* TRUE if the text is valid UTF-8, FALSE if not.
*/
function drupal_validate_utf8($text) {
if (strlen((string) $text) == 0) {
return TRUE;
}
// With the PCRE_UTF8 modifier 'u', preg_match() fails silently on strings
// containing invalid UTF-8 byte sequences. It does not reject character
// codes above U+10FFFF (represented by 4 or more octets), though.
return (preg_match('/^./us', $text) == 1);
}
/**
* Returns the equivalent of Apache's $_SERVER['REQUEST_URI'] variable.
*
* Because $_SERVER['REQUEST_URI'] is only available on Apache, we generate an
* equivalent using other environment variables.
*/
function request_uri() {
if (isset($_SERVER['REQUEST_URI'])) {
$uri = $_SERVER['REQUEST_URI'];
}
else {
if (isset($_SERVER['argv'])) {
$uri = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['argv'][0];
}
elseif (isset($_SERVER['QUERY_STRING'])) {
$uri = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['QUERY_STRING'];
}
else {
$uri = $_SERVER['SCRIPT_NAME'];
}
}
// Prevent multiple slashes to avoid cross site requests via the Form API.
$uri = '/' . ltrim($uri, '/');
return $uri;
}
/**
* Logs an exception.
*
* This is a wrapper function for watchdog() which automatically decodes an
* exception.
*
* @param $type
* The category to which this message belongs.
* @param $exception
* The exception that is going to be logged.
* @param $message
* The message to store in the log. If empty, a text that contains all useful
* information about the passed-in exception is used.
* @param $variables
* Array of variables to replace in the message on display. Defaults to the
* return value of _drupal_decode_exception().
* @param $severity
* The severity of the message, as per RFC 3164.
* @param $link
* A link to associate with the message.
*
* @see watchdog()
* @see _drupal_decode_exception()
*/
function watchdog_exception($type, Exception $exception, $message = NULL, $variables = array(), $severity = WATCHDOG_ERROR, $link = NULL) {
// Use a default value if $message is not set.
if (empty($message)) {
// The exception message is run through check_plain() by _drupal_decode_exception().
$message = '%type: !message in %function (line %line of %file).';
}
// $variables must be an array so that we can add the exception information.
if (!is_array($variables)) {
$variables = array();
}
require_once DRUPAL_ROOT . '/includes/errors.inc';
$variables += _drupal_decode_exception($exception);
watchdog($type, $message, $variables, $severity, $link);
}
/**
* Logs a system message.
*
* @param $type
* The category to which this message belongs. Can be any string, but the
* general practice is to use the name of the module calling watchdog().
* @param $message
* The message to store in the log. Keep $message translatable
* by not concatenating dynamic values into it! Variables in the
* message should be added by using placeholder strings alongside
* the variables argument to declare the value of the placeholders.
* See t() for documentation on how $message and $variables interact.
* @param $variables
* Array of variables to replace in the message on display or
* NULL if message is already translated or not possible to
* translate.
* @param $severity
* The severity of the message; one of the following values as defined in
* @link http://www.faqs.org/rfcs/rfc3164.html RFC 3164: @endlink
* - WATCHDOG_EMERGENCY: Emergency, system is unusable.
* - WATCHDOG_ALERT: Alert, action must be taken immediately.
* - WATCHDOG_CRITICAL: Critical conditions.
* - WATCHDOG_ERROR: Error conditions.
* - WATCHDOG_WARNING: Warning conditions.
* - WATCHDOG_NOTICE: (default) Normal but significant conditions.
* - WATCHDOG_INFO: Informational messages.
* - WATCHDOG_DEBUG: Debug-level messages.
* @param $link
* A link to associate with the message.
*
* @see watchdog_severity_levels()
* @see hook_watchdog()
*/
function watchdog($type, $message, $variables = array(), $severity = WATCHDOG_NOTICE, $link = NULL) {
global $user, $base_root;
static $in_error_state = FALSE;
// It is possible that the error handling will itself trigger an error. In that case, we could
// end up in an infinite loop. To avoid that, we implement a simple static semaphore.
if (!$in_error_state && function_exists('module_invoke_all')) {
$in_error_state = TRUE;
// The user object may not exist in all conditions, so 0 is substituted if needed.
$user_uid = isset($user->uid) ? $user->uid : 0;
// Prepare the fields to be logged
$log_entry = array(
'type' => $type,
'message' => $message,
'variables' => $variables,
'severity' => $severity,
'link' => $link,
'user' => $user,
'uid' => $user_uid,
'request_uri' => $base_root . request_uri(),
'referer' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '',
'ip' => ip_address(),
// Request time isn't accurate for long processes, use time() instead.
'timestamp' => time(),
);
// Call the logging hooks to log/process the message
module_invoke_all('watchdog', $log_entry);
// It is critical that the semaphore is only cleared here, in the parent
// watchdog() call (not outside the loop), to prevent recursive execution.
$in_error_state = FALSE;
}
}
/**
* Sets a message to display to the user.
*
* Messages are stored in a session variable and displayed in page.tpl.php via
* the $messages theme variable.
*
* Example usage:
* @code
* drupal_set_message(t('An error occurred and processing did not complete.'), 'error');
* @endcode
*
* @param string $message
* (optional) The translated message to be displayed to the user. For
* consistency with other messages, it should begin with a capital letter and
* end with a period.
* @param string $type
* (optional) The message's type. Defaults to 'status'. These values are
* supported:
* - 'status'
* - 'warning'
* - 'error'
* @param bool $repeat
* (optional) If this is FALSE and the message is already set, then the
* message won't be repeated. Defaults to TRUE.
*
* @return array|null
* A multidimensional array with keys corresponding to the set message types.
* The indexed array values of each contain the set messages for that type.
* Or, if there are no messages set, the function returns NULL.
*
* @see drupal_get_messages()
* @see theme_status_messages()
*/
function drupal_set_message($message = NULL, $type = 'status', $repeat = TRUE) {
if ($message || $message === '0' || $message === 0) {
if (!isset($_SESSION['messages'][$type])) {
$_SESSION['messages'][$type] = array();
}
if ($repeat || !in_array($message, $_SESSION['messages'][$type])) {
$_SESSION['messages'][$type][] = $message;
}
// Mark this page as being uncacheable.
drupal_page_is_cacheable(FALSE);
}
// Messages not set when DB connection fails.
return isset($_SESSION['messages']) ? $_SESSION['messages'] : NULL;
}
/**
* Returns all messages that have been set with drupal_set_message().
*
* @param string $type
* (optional) Limit the messages returned by type. Defaults to NULL, meaning
* all types. These values are supported:
* - NULL
* - 'status'
* - 'warning'
* - 'error'
* @param bool $clear_queue
* (optional) If this is TRUE, the queue will be cleared of messages of the
* type specified in the $type parameter. Otherwise the queue will be left
* intact. Defaults to TRUE.
*
* @return array
* A multidimensional array with keys corresponding to the set message types.
* The indexed array values of each contain the set messages for that type.
* The messages returned are limited to the type specified in the $type
* parameter. If there are no messages of the specified type, an empty array
* is returned.
*
* @see drupal_set_message()
* @see theme_status_messages()
*/
function drupal_get_messages($type = NULL, $clear_queue = TRUE) {
if ($messages = drupal_set_message()) {
if ($type) {
if ($clear_queue) {
unset($_SESSION['messages'][$type]);
}
if (isset($messages[$type])) {
return array($type => $messages[$type]);
}
}
else {
if ($clear_queue) {
unset($_SESSION['messages']);
}
return $messages;
}
}
return array();
}
/**
* Gets the title of the current page.
*
* The title is displayed on the page and in the title bar.
*
* @return
* The current page's title.
*/
function drupal_get_title() {
$title = drupal_set_title();
// During a bootstrap, menu.inc is not included and thus we cannot provide a title.
if (!isset($title) && function_exists('menu_get_active_title')) {
$title = check_plain(menu_get_active_title());
}
return $title;
}
/**
* Sets the title of the current page.
*
* The title is displayed on the page and in the title bar.
*
* @param $title
* Optional string value to assign to the page title; or if set to NULL
* (default), leaves the current title unchanged.
* @param $output
* Optional flag - normally should be left as CHECK_PLAIN. Only set to
* PASS_THROUGH if you have already removed any possibly dangerous code
* from $title using a function like check_plain() or filter_xss(). With this
* flag the string will be passed through unchanged.
*
* @return
* The updated title of the current page.
*/
function drupal_set_title($title = NULL, $output = CHECK_PLAIN) {
$stored_title = &drupal_static(__FUNCTION__);
if (isset($title)) {
$stored_title = ($output == PASS_THROUGH) ? $title : check_plain($title);
}
return $stored_title;
}
/**
* Checks to see if an IP address has been blocked.
*
* Blocked IP addresses are stored in the database by default. However for
* performance reasons we allow an override in settings.php. This allows us
* to avoid querying the database at this critical stage of the bootstrap if
* an administrative interface for IP address blocking is not required.
*
* @param $ip
* IP address to check.
*
* @return bool
* TRUE if access is denied, FALSE if access is allowed.
*/
function drupal_is_denied($ip) {
// Because this function is called on every page request, we first check
// for an array of IP addresses in settings.php before querying the
// database.
$blocked_ips = variable_get('blocked_ips');
$denied = FALSE;
if (isset($blocked_ips) && is_array($blocked_ips)) {
$denied = in_array($ip, $blocked_ips);
}
// Only check if database.inc is loaded already. If
// $conf['page_cache_without_database'] = TRUE; is set in settings.php,
// then the database won't be loaded here so the IPs in the database
// won't be denied. However the user asked explicitly not to use the
// database and also in this case it's quite likely that the user relies
// on higher performance solutions like a firewall.
elseif (class_exists('Database', FALSE)) {
$denied = (bool)db_query("SELECT 1 FROM {blocked_ips} WHERE ip = :ip", array(':ip' => $ip))->fetchField();
}
return $denied;
}
/**
* Handles denied users.
*
* @param $ip
* IP address to check. Prints a message and exits if access is denied.
*/
function drupal_block_denied($ip) {
// Deny access to blocked IP addresses - t() is not yet available.
if (drupal_is_denied($ip)) {
header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
print 'Sorry, ' . check_plain(ip_address()) . ' has been banned.';
exit();
}
}
/**
* Returns a URL-safe, base64 encoded string of highly randomized bytes (over the full 8-bit range).
*
* @param $byte_count
* The number of random bytes to fetch and base64 encode.
*
* @return string
* The base64 encoded result will have a length of up to 4 * $byte_count.
*/
function drupal_random_key($byte_count = 32) {
return drupal_base64_encode(drupal_random_bytes($byte_count));
}
/**
* Returns a URL-safe, base64 encoded version of the supplied string.
*
* @param $string
* The string to convert to base64.
*
* @return string
*/
function drupal_base64_encode($string) {
$data = base64_encode($string);
// Modify the output so it's safe to use in URLs.
return strtr($data, array('+' => '-', '/' => '_', '=' => ''));
}
/**
* Returns a string of highly randomized bytes (over the full 8-bit range).
*
* This function is better than simply calling mt_rand() or any other built-in
* PHP function because it can return a long string of bytes (compared to < 4
* bytes normally from mt_rand()) and uses the best available pseudo-random
* source.
*
* @param $count
* The number of characters (bytes) to return in the string.
*/
function drupal_random_bytes($count) {
// $random_state does not use drupal_static as it stores random bytes.
static $random_state, $bytes, $has_openssl;
$missing_bytes = $count - strlen((string) $bytes);
if ($missing_bytes > 0) {
// PHP versions prior 5.3.4 experienced openssl_random_pseudo_bytes()
// locking on Windows and rendered it unusable.
if (!isset($has_openssl)) {
$has_openssl = version_compare(PHP_VERSION, '5.3.4', '>=') && function_exists('openssl_random_pseudo_bytes');
}
// openssl_random_pseudo_bytes() will find entropy in a system-dependent
// way.
if ($has_openssl) {
$bytes .= openssl_random_pseudo_bytes($missing_bytes);
}
// Else, read directly from /dev/urandom, which is available on many *nix
// systems and is considered cryptographically secure.
elseif ($fh = @fopen('/dev/urandom', 'rb')) {
// PHP only performs buffered reads, so in reality it will always read
// at least 4096 bytes. Thus, it costs nothing extra to read and store
// that much so as to speed any additional invocations.
$bytes .= fread($fh, max(4096, $missing_bytes));
fclose($fh);
}
// If we couldn't get enough entropy, this simple hash-based PRNG will
// generate a good set of pseudo-random bytes on any system.
// Note that it may be important that our $random_state is passed
// through hash() prior to being rolled into $output, that the two hash()
// invocations are different, and that the extra input into the first one -
// the microtime() - is prepended rather than appended. This is to avoid
// directly leaking $random_state via the $output stream, which could
// allow for trivial prediction of further "random" numbers.
if (strlen((string) $bytes) < $count) {
// Initialize on the first call. The contents of $_SERVER includes a mix of
// user-specific and system information that varies a little with each page.
if (!isset($random_state)) {
$random_state = print_r($_SERVER, TRUE);
if (function_exists('getmypid')) {
// Further initialize with the somewhat random PHP process ID.
$random_state .= getmypid();
}
$bytes = '';
}
do {
$random_state = hash('sha256', microtime() . mt_rand() . $random_state);
$bytes .= hash('sha256', mt_rand() . $random_state, TRUE);
}
while (strlen($bytes) < $count);
}
}
$output = substr($bytes, 0, $count);
$bytes = substr($bytes, $count);
return $output;
}
/**
* Calculates a base-64 encoded, URL-safe sha-256 hmac.
*
* @param string $data
* String to be validated with the hmac.
* @param string $key
* A secret string key.
*
* @return string
* A base-64 encoded sha-256 hmac, with + replaced with -, / with _ and
* any = padding characters removed.
*/
function drupal_hmac_base64($data, $key) {
// Casting $data and $key to strings here is necessary to avoid empty string
// results of the hash function if they are not scalar values. As this
// function is used in security-critical contexts like token validation it is
// important that it never returns an empty string.
$hmac = base64_encode(hash_hmac('sha256', (string) $data, (string) $key, TRUE));
// Modify the hmac so it's safe to use in URLs.
return strtr($hmac, array('+' => '-', '/' => '_', '=' => ''));
}
/**
* Calculates a base-64 encoded, URL-safe sha-256 hash.
*
* @param $data
* String to be hashed.
*
* @return
* A base-64 encoded sha-256 hash, with + replaced with -, / with _ and
* any = padding characters removed.
*/
function drupal_hash_base64($data) {
$hash = base64_encode(hash('sha256', $data, TRUE));
// Modify the hash so it's safe to use in URLs.
return strtr($hash, array('+' => '-', '/' => '_', '=' => ''));
}
/**
* Merges multiple arrays, recursively, and returns the merged array.
*
* This function is similar to PHP's array_merge_recursive() function, but it
* handles non-array values differently. When merging values that are not both
* arrays, the latter value replaces the former rather than merging with it.
*
* Example:
* @code
* $link_options_1 = array('fragment' => 'x', 'attributes' => array('title' => t('X'), 'class' => array('a', 'b')));
* $link_options_2 = array('fragment' => 'y', 'attributes' => array('title' => t('Y'), 'class' => array('c', 'd')));
*
* // This results in array('fragment' => array('x', 'y'), 'attributes' => array('title' => array(t('X'), t('Y')), 'class' => array('a', 'b', 'c', 'd'))).
* $incorrect = array_merge_recursive($link_options_1, $link_options_2);
*
* // This results in array('fragment' => 'y', 'attributes' => array('title' => t('Y'), 'class' => array('a', 'b', 'c', 'd'))).
* $correct = drupal_array_merge_deep($link_options_1, $link_options_2);
* @endcode
*
* @param ...
* Arrays to merge.
*
* @return
* The merged array.
*
* @see drupal_array_merge_deep_array()
*/
function drupal_array_merge_deep() {
$args = func_get_args();
return drupal_array_merge_deep_array($args);
}
/**
* Merges multiple arrays, recursively, and returns the merged array.
*
* This function is equivalent to drupal_array_merge_deep(), except the
* input arrays are passed as a single array parameter rather than a variable
* parameter list.
*
* The following are equivalent:
* - drupal_array_merge_deep($a, $b);
* - drupal_array_merge_deep_array(array($a, $b));
*
* The following are also equivalent:
* - call_user_func_array('drupal_array_merge_deep', $arrays_to_merge);
* - drupal_array_merge_deep_array($arrays_to_merge);
*
* @see drupal_array_merge_deep()
*/
function drupal_array_merge_deep_array($arrays) {
$result = array();
foreach ($arrays as $array) {
foreach ($array as $key => $value) {
// Renumber integer keys as array_merge_recursive() does. Note that PHP
// automatically converts array keys that are integer strings (e.g., '1')
// to integers.
if (is_integer($key)) {
$result[] = $value;
}
// Recurse when both values are arrays.
elseif (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
$result[$key] = drupal_array_merge_deep_array(array($result[$key], $value));
}
// Otherwise, use the latter value, overriding any previous value.
else {
$result[$key] = $value;
}
}
}
return $result;
}
/**
* Generates a default anonymous $user object.
*
* @return Object - the user object.
*/
function drupal_anonymous_user() {
$user = variable_get('drupal_anonymous_user_object', new stdClass);
$user->uid = 0;
$user->hostname = ip_address();
$user->roles = array();
$user->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user';
$user->cache = 0;
return $user;
}
/**
* Ensures Drupal is bootstrapped to the specified phase.
*
* In order to bootstrap Drupal from another PHP script, you can use this code:
* @code
* define('DRUPAL_ROOT', '/path/to/drupal');
* require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
* drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
* @endcode
*
* @param int $phase
* A constant telling which phase to bootstrap to. When you bootstrap to a
* particular phase, all earlier phases are run automatically. Possible
* values:
* - DRUPAL_BOOTSTRAP_CONFIGURATION: Initializes configuration.
* - DRUPAL_BOOTSTRAP_PAGE_CACHE: Tries to serve a cached page.
* - DRUPAL_BOOTSTRAP_DATABASE: Initializes the database layer.
* - DRUPAL_BOOTSTRAP_VARIABLES: Initializes the variable system.
* - DRUPAL_BOOTSTRAP_SESSION: Initializes session handling.
* - DRUPAL_BOOTSTRAP_PAGE_HEADER: Sets up the page header.
* - DRUPAL_BOOTSTRAP_LANGUAGE: Finds out the language of the page.
* - DRUPAL_BOOTSTRAP_FULL: Fully loads Drupal. Validates and fixes input
* data.
* @param boolean $new_phase
* A boolean, set to FALSE if calling drupal_bootstrap from inside a
* function called from drupal_bootstrap (recursion).
*
* @return int
* The most recently completed phase.
*/
function drupal_bootstrap($phase = NULL, $new_phase = TRUE) {
// Not drupal_static(), because does not depend on any run-time information.
static $phases = array(
DRUPAL_BOOTSTRAP_CONFIGURATION,
DRUPAL_BOOTSTRAP_PAGE_CACHE,
DRUPAL_BOOTSTRAP_DATABASE,
DRUPAL_BOOTSTRAP_VARIABLES,
DRUPAL_BOOTSTRAP_SESSION,
DRUPAL_BOOTSTRAP_PAGE_HEADER,
DRUPAL_BOOTSTRAP_LANGUAGE,
DRUPAL_BOOTSTRAP_FULL,
);
// Not drupal_static(), because the only legitimate API to control this is to
// call drupal_bootstrap() with a new phase parameter.
static $final_phase;
// Not drupal_static(), because it's impossible to roll back to an earlier
// bootstrap state.
static $stored_phase = -1;
if (isset($phase)) {
// When not recursing, store the phase name so it's not forgotten while
// recursing but take care of not going backwards.
if ($new_phase && $phase >= $stored_phase) {
$final_phase = $phase;
}
// Call a phase if it has not been called before and is below the requested
// phase.
while ($phases && $phase > $stored_phase && $final_phase > $stored_phase) {
$current_phase = array_shift($phases);
// This function is re-entrant. Only update the completed phase when the
// current call actually resulted in a progress in the bootstrap process.
if ($current_phase > $stored_phase) {
$stored_phase = $current_phase;
}
switch ($current_phase) {
case DRUPAL_BOOTSTRAP_CONFIGURATION:
require_once DRUPAL_ROOT . '/includes/request-sanitizer.inc';
_drupal_bootstrap_configuration();
break;
case DRUPAL_BOOTSTRAP_PAGE_CACHE:
_drupal_bootstrap_page_cache();
break;
case DRUPAL_BOOTSTRAP_DATABASE:
_drupal_bootstrap_database();
break;
case DRUPAL_BOOTSTRAP_VARIABLES:
_drupal_bootstrap_variables();
break;
case DRUPAL_BOOTSTRAP_SESSION:
require_once DRUPAL_ROOT . '/' . variable_get('session_inc', 'includes/session.inc');
drupal_session_initialize();
break;
case DRUPAL_BOOTSTRAP_PAGE_HEADER:
_drupal_bootstrap_page_header();
break;
case DRUPAL_BOOTSTRAP_LANGUAGE:
drupal_language_initialize();
break;
case DRUPAL_BOOTSTRAP_FULL:
require_once DRUPAL_ROOT . '/includes/common.inc';
_drupal_bootstrap_full();
break;
}
}
}
return $stored_phase;
}
/**
* Returns the time zone of the current user.
*/
function drupal_get_user_timezone() {
global $user;
if (variable_get('configurable_timezones', 1) && $user->uid && $user->timezone) {
return $user->timezone;
}
else {
// Ignore PHP strict notice if time zone has not yet been set in the php.ini
// configuration.
return variable_get('date_default_timezone', @date_default_timezone_get());
}
}
/**
* Gets a salt useful for hardening against SQL injection.
*
* @return
* A salt based on information in settings.php, not in the database.
*/
function drupal_get_hash_salt() {
global $drupal_hash_salt, $databases;
// If the $drupal_hash_salt variable is empty, a hash of the serialized
// database credentials is used as a fallback salt.
return empty($drupal_hash_salt) ? hash('sha256', serialize($databases)) : $drupal_hash_salt;
}
/**
* Provides custom PHP error handling.
*
* @param $error_level
* The level of the error raised.
* @param $message
* The error message.
* @param $filename
* The filename that the error was raised in.
* @param $line
* The line number the error was raised at.
*/
function _drupal_error_handler($error_level, $message, $filename, $line) {
require_once DRUPAL_ROOT . '/includes/errors.inc';
_drupal_error_handler_real($error_level, $message, $filename, $line);
}
/**
* Provides custom PHP exception handling.
*
* Uncaught exceptions are those not enclosed in a try/catch block. They are
* always fatal: the execution of the script will stop as soon as the exception
* handler exits.
*
* @param $exception
* The exception object that was thrown.
*/
function _drupal_exception_handler($exception) {
require_once DRUPAL_ROOT . '/includes/errors.inc';
try {
// Log the message to the watchdog and return an error page to the user.
_drupal_log_error(_drupal_decode_exception($exception), TRUE);
}
catch (Exception $exception2) {
// Add a 500 status code in case an exception was thrown before the 500
// status could be set (e.g. while loading a maintenance theme from cache).
drupal_add_http_header('Status', '500 Internal Server Error');
// Another uncaught exception was thrown while handling the first one.
// If we are displaying errors, then do so with no possibility of a further uncaught exception being thrown.
if (error_displayable()) {
print 'Additional uncaught exception thrown while handling exception. ';
print 'Original ' . _drupal_render_exception_safe($exception) . '
';
print 'Additional ' . _drupal_render_exception_safe($exception2) . '
';
}
}
}
/**
* Sets up the script environment and loads settings.php.
*/
function _drupal_bootstrap_configuration() {
// Set the Drupal custom error handler.
set_error_handler('_drupal_error_handler');
set_exception_handler('_drupal_exception_handler');
drupal_environment_initialize();
// Start a page timer:
timer_start('page');
// Initialize the configuration, including variables from settings.php.
drupal_settings_initialize();
// Sanitize unsafe keys from the request.
DrupalRequestSanitizer::sanitize();
}
/**
* Attempts to serve a page from the cache.
*/
function _drupal_bootstrap_page_cache() {
global $user;
// Allow specifying special cache handlers in settings.php, like
// using memcached or files for storing cache information.
require_once DRUPAL_ROOT . '/includes/cache.inc';
foreach (variable_get('cache_backends', array()) as $include) {
require_once DRUPAL_ROOT . '/' . $include;
}
// Check for a cache mode force from settings.php.
if (variable_get('page_cache_without_database')) {
$cache_enabled = TRUE;
}
else {
drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES, FALSE);
$cache_enabled = variable_get('cache');
}
drupal_block_denied(ip_address());
// If there is no session cookie and cache is enabled (or forced), try
// to serve a cached page.
if (!isset($_COOKIE[session_name()]) && $cache_enabled) {
// Make sure there is a user object because its timestamp will be
// checked, hook_boot might check for anonymous user etc.
$user = drupal_anonymous_user();
// Get the page from the cache.
$cache = drupal_page_get_cache();
// If there is a cached page, display it.
if (is_object($cache)) {
header('X-Drupal-Cache: HIT');
// Restore the metadata cached with the page.
$_GET['q'] = $cache->data['path'];
drupal_set_title($cache->data['title'], PASS_THROUGH);
date_default_timezone_set(drupal_get_user_timezone());
// If the skipping of the bootstrap hooks is not enforced, call
// hook_boot.
if (variable_get('page_cache_invoke_hooks', TRUE)) {
bootstrap_invoke_all('boot');
}
drupal_serve_page_from_cache($cache);
// If the skipping of the bootstrap hooks is not enforced, call
// hook_exit.
if (variable_get('page_cache_invoke_hooks', TRUE)) {
bootstrap_invoke_all('exit');
}
// We are done.
exit;
}
else {
header('X-Drupal-Cache: MISS');
}
}
}
/**
* Initializes the database system and registers autoload functions.
*/
function _drupal_bootstrap_database() {
// Redirect the user to the installation script if Drupal has not been
// installed yet (i.e., if no $databases array has been defined in the
// settings.php file) and we are not already installing.
if (empty($GLOBALS['databases']) && !drupal_installation_attempted()) {
include_once DRUPAL_ROOT . '/includes/install.inc';
install_goto('install.php');
}
// The user agent header is used to pass a database prefix in the request when
// running tests. However, for security reasons, it is imperative that we
// validate we ourselves made the request.
if ($test_prefix = drupal_valid_test_ua()) {
// Set the test run id for use in other parts of Drupal.
$test_info = &$GLOBALS['drupal_test_info'];
$test_info['test_run_id'] = $test_prefix;
$test_info['in_child_site'] = TRUE;
foreach ($GLOBALS['databases']['default'] as &$value) {
// Extract the current default database prefix.
if (!isset($value['prefix'])) {
$current_prefix = '';
}
elseif (is_array($value['prefix'])) {
$current_prefix = $value['prefix']['default'];
}
else {
$current_prefix = $value['prefix'];
}
// Remove the current database prefix and replace it by our own.
$value['prefix'] = array(
'default' => $current_prefix . $test_prefix,
);
}
}
// Initialize the database system. Note that the connection
// won't be initialized until it is actually requested.
require_once DRUPAL_ROOT . '/includes/database/database.inc';
// Register autoload functions so that we can access classes and interfaces.
// The database autoload routine comes first so that we can load the database
// system without hitting the database. That is especially important during
// the install or upgrade process.
spl_autoload_register('drupal_autoload_class');
spl_autoload_register('drupal_autoload_interface');
if (version_compare(PHP_VERSION, '5.4') >= 0) {
spl_autoload_register('drupal_autoload_trait');
}
}
/**
* Loads system variables and all enabled bootstrap modules.
*/
function _drupal_bootstrap_variables() {
global $conf;
// Initialize the lock system.
require_once DRUPAL_ROOT . '/' . variable_get('lock_inc', 'includes/lock.inc');
lock_initialize();
// Load variables from the database, but do not overwrite variables set in settings.php.
$conf = variable_initialize(isset($conf) ? $conf : array());
// Load bootstrap modules.
require_once DRUPAL_ROOT . '/includes/module.inc';
module_load_all(TRUE);
// Sanitize the destination parameter (which is often used for redirects) to
// prevent open redirect attacks leading to other domains. Sanitize both
// $_GET['destination'] and $_REQUEST['destination'] to protect code that
// relies on either, but do not sanitize $_POST to avoid interfering with
// unrelated form submissions. The sanitization happens here because
// url_is_external() requires the variable system to be available.
if (isset($_GET['destination']) || isset($_REQUEST['destination'])) {
require_once DRUPAL_ROOT . '/includes/common.inc';
// If the destination is an external URL, remove it.
if (isset($_GET['destination']) && url_is_external($_GET['destination'])) {
unset($_GET['destination']);
unset($_REQUEST['destination']);
}
// Use the DrupalRequestSanitizer to ensure that the destination's query
// parameters are not dangerous.
if (isset($_GET['destination'])) {
DrupalRequestSanitizer::cleanDestination();
}
// If there's still something in $_REQUEST['destination'] that didn't come
// from $_GET, check it too.
if (isset($_REQUEST['destination']) && (!isset($_GET['destination']) || $_REQUEST['destination'] != $_GET['destination']) && url_is_external($_REQUEST['destination'])) {
unset($_REQUEST['destination']);
}
}
}
/**
* Invokes hook_boot(), initializes locking system, and sends HTTP headers.
*/
function _drupal_bootstrap_page_header() {
bootstrap_invoke_all('boot');
if (!drupal_is_cli()) {
ob_start();
drupal_page_header();
}
}
/**
* Returns the current bootstrap phase for this Drupal process.
*
* The current phase is the one most recently completed by drupal_bootstrap().
*
* @see drupal_bootstrap()
*/
function drupal_get_bootstrap_phase() {
return drupal_bootstrap(NULL, FALSE);
}
/**
* Returns the test prefix if this is an internal request from SimpleTest.
*
* @return
* Either the simpletest prefix (the string "simpletest" followed by any
* number of digits) or FALSE if the user agent does not contain a valid
* HMAC and timestamp.
*/
function drupal_valid_test_ua() {
// No reason to reset this.
static $test_prefix;
if (isset($test_prefix)) {
return $test_prefix;
}
if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^(simpletest\d+);(.+);(.+);(.+)$/", $_SERVER['HTTP_USER_AGENT'], $matches)) {
list(, $prefix, $time, $salt, $hmac) = $matches;
$check_string = $prefix . ';' . $time . ';' . $salt;
// We use the salt from settings.php to make the HMAC key, since
// the database is not yet initialized and we can't access any Drupal variables.
// The file properties add more entropy not easily accessible to others.
$key = drupal_get_hash_salt() . filectime(__FILE__) . fileinode(__FILE__);
$time_diff = REQUEST_TIME - $time;
// Since we are making a local request a 5 second time window is allowed,
// and the HMAC must match.
if ($time_diff >= 0 && $time_diff <= 5 && $hmac == drupal_hmac_base64($check_string, $key)) {
$test_prefix = $prefix;
return $test_prefix;
}
}
$test_prefix = FALSE;
return $test_prefix;
}
/**
* Generates a user agent string with a HMAC and timestamp for simpletest.
*/
function drupal_generate_test_ua($prefix) {
static $key;
if (!isset($key)) {
// We use the salt from settings.php to make the HMAC key, since
// the database is not yet initialized and we can't access any Drupal variables.
// The file properties add more entropy not easily accessible to others.
$key = drupal_get_hash_salt() . filectime(__FILE__) . fileinode(__FILE__);
}
// Generate a moderately secure HMAC based on the database credentials.
$salt = uniqid('', TRUE);
$check_string = $prefix . ';' . time() . ';' . $salt;
return $check_string . ';' . drupal_hmac_base64($check_string, $key);
}
/**
* Enables use of the theme system without requiring database access.
*
* Loads and initializes the theme system for site installs, updates and when
* the site is in maintenance mode. This also applies when the database fails.
*
* @see _drupal_maintenance_theme()
*/
function drupal_maintenance_theme() {
require_once DRUPAL_ROOT . '/includes/theme.maintenance.inc';
_drupal_maintenance_theme();
}
/**
* Returns a simple 404 Not Found page.
*
* If fast 404 pages are enabled, and this is a matching page then print a
* simple 404 page and exit.
*
* This function is called from drupal_deliver_html_page() at the time when a
* a normal 404 page is generated, but it can also optionally be called directly
* from settings.php to prevent a Drupal bootstrap on these pages. See
* documentation in settings.php for the benefits and drawbacks of using this.
*
* Paths to dynamically-generated content, such as image styles, should also be
* accounted for in this function.
*/
function drupal_fast_404() {
$exclude_paths = variable_get('404_fast_paths_exclude', FALSE);
if ($exclude_paths && !preg_match($exclude_paths, $_GET['q'])) {
$fast_paths = variable_get('404_fast_paths', FALSE);
if ($fast_paths && preg_match($fast_paths, $_GET['q'])) {
drupal_add_http_header('Status', '404 Not Found');
$fast_404_html = variable_get('404_fast_html', '404 Not Found Not Found The requested URL "@path" was not found on this server.
');
// Replace @path in the variable with the page path.
print strtr($fast_404_html, array('@path' => check_plain(request_uri())));
exit;
}
}
}
/**
* Returns TRUE if a Drupal installation is currently being attempted.
*/
function drupal_installation_attempted() {
return defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'install';
}
/**
* Returns the name of the proper localization function.
*
* get_t() exists to support localization for code that might run during
* the installation phase, when some elements of the system might not have
* loaded.
*
* This would include implementations of hook_install(), which could run
* during the Drupal installation phase, and might also be run during
* non-installation time, such as while installing the module from the
* module administration page.
*
* Example usage:
* @code
* $t = get_t();
* $translated = $t('translate this');
* @endcode
*
* Use t() if your code will never run during the Drupal installation phase.
* Use st() if your code will only run during installation and never any other
* time. Use get_t() if your code could run in either circumstance.
*
* @see t()
* @see st()
* @ingroup sanitization
*/
function get_t() {
static $t;
// This is not converted to drupal_static because there is no point in
// resetting this as it can not change in the course of a request.
if (!isset($t)) {
$t = drupal_installation_attempted() ? 'st' : 't';
}
return $t;
}
/**
* Initializes all the defined language types.
*/
function drupal_language_initialize() {
$types = language_types();
// Ensure the language is correctly returned, even without multilanguage
// support. Also make sure we have a $language fallback, in case a language
// negotiation callback needs to do a full bootstrap.
// Useful for eg. XML/HTML 'lang' attributes.
$default = language_default();
foreach ($types as $type) {
$GLOBALS[$type] = $default;
}
if (drupal_multilingual()) {
include_once DRUPAL_ROOT . '/includes/language.inc';
foreach ($types as $type) {
$GLOBALS[$type] = language_initialize($type);
}
// Allow modules to react on language system initialization in multilingual
// environments.
bootstrap_invoke_all('language_init');
}
}
/**
* Returns a list of the built-in language types.
*
* @return
* An array of key-values pairs where the key is the language type and the
* value is its configurability.
*/
function drupal_language_types() {
return array(
LANGUAGE_TYPE_INTERFACE => TRUE,
LANGUAGE_TYPE_CONTENT => FALSE,
LANGUAGE_TYPE_URL => FALSE,
);
}
/**
* Returns TRUE if there is more than one language enabled.
*
* @return
* TRUE if more than one language is enabled.
*/
function drupal_multilingual() {
// The "language_count" variable stores the number of enabled languages to
// avoid unnecessarily querying the database when building the list of
// enabled languages on monolingual sites.
return variable_get('language_count', 1) > 1;
}
/**
* Returns an array of the available language types.
*
* @return
* An array of all language types where the keys of each are the language type
* name and its value is its configurability (TRUE/FALSE).
*/
function language_types() {
return array_keys(variable_get('language_types', drupal_language_types()));
}
/**
* Returns a list of installed languages, indexed by the specified key.
*
* @param $field
* (optional) The field to index the list with.
*
* @return
* An associative array, keyed on the values of $field.
* - If $field is 'weight' or 'enabled', the array is nested, with the outer
* array's values each being associative arrays with language codes as
* keys and language objects as values.
* - For all other values of $field, the array is only one level deep, and
* the array's values are language objects.
*/
function language_list($field = 'language') {
$languages = &drupal_static(__FUNCTION__);
// Init language list
if (!isset($languages)) {
if (drupal_multilingual() || module_exists('locale')) {
$languages['language'] = db_query('SELECT * FROM {languages} ORDER BY weight ASC, name ASC')->fetchAllAssoc('language');
// Users cannot uninstall the native English language. However, we allow
// it to be hidden from the installed languages. Therefore, at least one
// other language must be enabled then.
if (!$languages['language']['en']->enabled && !variable_get('language_native_enabled', TRUE)) {
unset($languages['language']['en']);
}
}
else {
// No locale module, so use the default language only.
$default = language_default();
$languages['language'][$default->language] = $default;
}
}
// Return the array indexed by the right field
if (!isset($languages[$field])) {
$languages[$field] = array();
foreach ($languages['language'] as $lang) {
// Some values should be collected into an array
if (in_array($field, array('enabled', 'weight'))) {
$languages[$field][$lang->$field][$lang->language] = $lang;
}
else {
$languages[$field][$lang->$field] = $lang;
}
}
}
return $languages[$field];
}
/**
* Returns the default language, as an object, or one of its properties.
*
* @param $property
* (optional) The property of the language object to return.
*
* @return
* Either the language object for the default language used on the site,
* or the property of that object named in the $property parameter.
*/
function language_default($property = NULL) {
$language = variable_get('language_default', (object) array('language' => 'en', 'name' => 'English', 'native' => 'English', 'direction' => 0, 'enabled' => 1, 'plurals' => 0, 'formula' => '', 'domain' => '', 'prefix' => '', 'weight' => 0, 'javascript' => ''));
return $property ? $language->$property : $language;
}
/**
* Returns the requested URL path of the page being viewed.
*
* Examples:
* - http://example.com/node/306 returns "node/306".
* - http://example.com/drupalfolder/node/306 returns "node/306" while
* base_path() returns "/drupalfolder/".
* - http://example.com/path/alias (which is a path alias for node/306) returns
* "path/alias" as opposed to the internal path.
* - http://example.com/index.php returns an empty string (meaning: front page).
* - http://example.com/index.php?page=1 returns an empty string.
*
* @return
* The requested Drupal URL path.
*
* @see current_path()
*/
function request_path() {
static $path;
if (isset($path)) {
return $path;
}
if (isset($_GET['q']) && is_string($_GET['q'])) {
// This is a request with a ?q=foo/bar query string. $_GET['q'] is
// overwritten in drupal_path_initialize(), but request_path() is called
// very early in the bootstrap process, so the original value is saved in
// $path and returned in later calls.
$path = $_GET['q'];
}
elseif (isset($_SERVER['REQUEST_URI'])) {
// This request is either a clean URL, or 'index.php', or nonsense.
// Extract the path from REQUEST_URI.
$request_path = strtok($_SERVER['REQUEST_URI'], '?');
$base_path_len = strlen(rtrim(dirname($_SERVER['SCRIPT_NAME']), '\/'));
// Unescape and strip $base_path prefix, leaving q without a leading slash.
$path = substr(urldecode($request_path), $base_path_len + 1);
// If the path equals the script filename, either because 'index.php' was
// explicitly provided in the URL, or because the server added it to
// $_SERVER['REQUEST_URI'] even when it wasn't provided in the URL (some
// versions of Microsoft IIS do this), the front page should be served.
if ($path == basename($_SERVER['PHP_SELF'])) {
$path = '';
}
}
else {
// This is the front page.
$path = '';
}
// Under certain conditions Apache's RewriteRule directive prepends the value
// assigned to $_GET['q'] with a slash. Moreover we can always have a trailing
// slash in place, hence we need to normalize $_GET['q'].
$path = trim($path, '/');
return $path;
}
/**
* Returns a component of the current Drupal path.
*
* When viewing a page at the path "admin/structure/types", for example, arg(0)
* returns "admin", arg(1) returns "structure", and arg(2) returns "types".
*
* Avoid use of this function where possible, as resulting code is hard to
* read. In menu callback functions, attempt to use named arguments. See the
* explanation in menu.inc for how to construct callbacks that take arguments.
* When attempting to use this function to load an element from the current
* path, e.g. loading the node on a node page, use menu_get_object() instead.
*
* @param $index
* The index of the component, where each component is separated by a '/'
* (forward-slash), and where the first component has an index of 0 (zero).
* @param $path
* A path to break into components. Defaults to the path of the current page.
*
* @return
* The component specified by $index, or NULL if the specified component was
* not found. If called without arguments, it returns an array containing all
* the components of the current path.
*/
function arg($index = NULL, $path = NULL) {
// Even though $arguments doesn't need to be resettable for any functional
// reasons (the result of explode() does not depend on any run-time
// information), it should be resettable anyway in case a module needs to
// free up the memory used by it.
// Use the advanced drupal_static() pattern, since this is called very often.
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast['arguments'] = &drupal_static(__FUNCTION__);
}
$arguments = &$drupal_static_fast['arguments'];
if (!isset($path)) {
$path = $_GET['q'];
}
if (!isset($arguments[$path])) {
$arguments[$path] = explode('/', $path);
}
if (!isset($index)) {
return $arguments[$path];
}
if (isset($arguments[$path][$index])) {
return $arguments[$path][$index];
}
}
/**
* Returns the IP address of the client machine.
*
* If Drupal is behind a reverse proxy, we use the X-Forwarded-For header
* instead of $_SERVER['REMOTE_ADDR'], which would be the IP address of
* the proxy server, and not the client's. The actual header name can be
* configured by the reverse_proxy_header variable.
*
* @return
* IP address of client machine, adjusted for reverse proxy and/or cluster
* environments.
*/
function ip_address() {
$ip_address = &drupal_static(__FUNCTION__);
if (!isset($ip_address)) {
$ip_address = $_SERVER['REMOTE_ADDR'];
if (variable_get('reverse_proxy', 0)) {
$reverse_proxy_header = variable_get('reverse_proxy_header', 'HTTP_X_FORWARDED_FOR');
if (!empty($_SERVER[$reverse_proxy_header])) {
// If an array of known reverse proxy IPs is provided, then trust
// the XFF header if request really comes from one of them.
$reverse_proxy_addresses = variable_get('reverse_proxy_addresses', array());
// Turn XFF header into an array.
$forwarded = explode(',', $_SERVER[$reverse_proxy_header]);
// Trim the forwarded IPs; they may have been delimited by commas and spaces.
$forwarded = array_map('trim', $forwarded);
// Tack direct client IP onto end of forwarded array.
$forwarded[] = $ip_address;
// Eliminate all trusted IPs.
$untrusted = array_diff($forwarded, $reverse_proxy_addresses);
if (!empty($untrusted)) {
// The right-most IP is the most specific we can trust.
$ip_address = array_pop($untrusted);
}
else {
// All IP addresses in the forwarded array are configured proxy IPs
// (and thus trusted). We take the leftmost IP.
$ip_address = array_shift($forwarded);
}
}
}
}
return $ip_address;
}
/**
* @addtogroup schemaapi
* @{
*/
/**
* Gets the schema definition of a table, or the whole database schema.
*
* The returned schema will include any modifications made by any
* module that implements hook_schema_alter(). To get the schema without
* modifications, use drupal_get_schema_unprocessed().
*
*
* @param $table
* The name of the table. If not given, the schema of all tables is returned.
* @param $rebuild
* If true, the schema will be rebuilt instead of retrieved from the cache.
*/
function drupal_get_schema($table = NULL, $rebuild = FALSE) {
static $schema;
if ($rebuild || !isset($table)) {
$schema = drupal_get_complete_schema($rebuild);
}
elseif (!isset($schema)) {
$schema = new SchemaCache();
}
if (!isset($table)) {
return $schema;
}
if (isset($schema[$table])) {
return $schema[$table];
}
else {
return FALSE;
}
}
/**
* Extends DrupalCacheArray to allow for dynamic building of the schema cache.
*/
class SchemaCache extends DrupalCacheArray {
/**
* Constructs a SchemaCache object.
*/
public function __construct() {
// Cache by request method.
parent::__construct('schema:runtime:' . ($_SERVER['REQUEST_METHOD'] == 'GET'), 'cache');
}
/**
* Overrides DrupalCacheArray::resolveCacheMiss().
*/
protected function resolveCacheMiss($offset) {
$complete_schema = drupal_get_complete_schema();
$value = isset($complete_schema[$offset]) ? $complete_schema[$offset] : NULL;
$this->storage[$offset] = $value;
$this->persist($offset);
return $value;
}
}
/**
* Gets the whole database schema.
*
* The returned schema will include any modifications made by any
* module that implements hook_schema_alter().
*
* @param $rebuild
* If true, the schema will be rebuilt instead of retrieved from the cache.
*/
function drupal_get_complete_schema($rebuild = FALSE) {
static $schema = array();
if (empty($schema) || $rebuild) {
// Try to load the schema from cache.
if (!$rebuild && $cached = cache_get('schema')) {
$schema = $cached->data;
}
// Otherwise, rebuild the schema cache.
else {
$schema = array();
// Load the .install files to get hook_schema.
// On some databases this function may be called before bootstrap has
// been completed, so we force the functions we need to load just in case.
if (function_exists('module_load_all_includes')) {
// This function can be called very early in the bootstrap process, so
// we force the module_list() cache to be refreshed to ensure that it
// contains the complete list of modules before we go on to call
// module_load_all_includes().
module_list(TRUE);
module_load_all_includes('install');
}
require_once DRUPAL_ROOT . '/includes/common.inc';
// Invoke hook_schema for all modules.
foreach (module_implements('schema') as $module) {
// Cast the result of hook_schema() to an array, as a NULL return value
// would cause array_merge() to set the $schema variable to NULL as well.
// That would break modules which use $schema further down the line.
$current = (array) module_invoke($module, 'schema');
// Set 'module' and 'name' keys for each table, and remove descriptions,
// as they needlessly slow down cache_get() for every single request.
_drupal_schema_initialize($current, $module);
$schema = array_merge($schema, $current);
}
drupal_alter('schema', $schema);
// If the schema is empty, avoid saving it: some database engines require
// the schema to perform queries, and this could lead to infinite loops.
if (!empty($schema) && (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL)) {
cache_set('schema', $schema);
}
if ($rebuild) {
cache_clear_all('schema:', 'cache', TRUE);
}
}
}
return $schema;
}
/**
* @} End of "addtogroup schemaapi".
*/
/**
* @addtogroup registry
* @{
*/
/**
* Confirms that an interface is available.
*
* This function is rarely called directly. Instead, it is registered as an
* spl_autoload() handler, and PHP calls it for us when necessary.
*
* @param $interface
* The name of the interface to check or load.
*
* @return
* TRUE if the interface is currently available, FALSE otherwise.
*/
function drupal_autoload_interface($interface) {
return _registry_check_code('interface', $interface);
}
/**
* Confirms that a class is available.
*
* This function is rarely called directly. Instead, it is registered as an
* spl_autoload() handler, and PHP calls it for us when necessary.
*
* @param $class
* The name of the class to check or load.
*
* @return
* TRUE if the class is currently available, FALSE otherwise.
*/
function drupal_autoload_class($class) {
return _registry_check_code('class', $class);
}
/**
* Confirms that a trait is available.
*
* This function is rarely called directly. Instead, it is registered as an
* spl_autoload() handler, and PHP calls it for us when necessary.
*
* @param string $trait
* The name of the trait to check or load.
*
* @return bool
* TRUE if the trait is currently available, FALSE otherwise.
*/
function drupal_autoload_trait($trait) {
return _registry_check_code('trait', $trait);
}
/**
* Checks for a resource in the registry.
*
* @param $type
* The type of resource we are looking up, or one of the constants
* REGISTRY_RESET_LOOKUP_CACHE or REGISTRY_WRITE_LOOKUP_CACHE, which
* signal that we should reset or write the cache, respectively.
* @param $name
* The name of the resource, or NULL if either of the REGISTRY_* constants
* is passed in.
*
* @return
* TRUE if the resource was found, FALSE if not.
* NULL if either of the REGISTRY_* constants is passed in as $type.
*/
function _registry_check_code($type, $name = NULL) {
static $lookup_cache, $cache_update_needed;
if ($type == 'class' && class_exists($name) || $type == 'interface' && interface_exists($name) || $type == 'trait' && trait_exists($name)) {
return TRUE;
}
if (!isset($lookup_cache)) {
$lookup_cache = array();
if ($cache = cache_get('lookup_cache', 'cache_bootstrap')) {
$lookup_cache = $cache->data;
}
}
// When we rebuild the registry, we need to reset this cache so
// we don't keep lookups for resources that changed during the rebuild.
if ($type == REGISTRY_RESET_LOOKUP_CACHE) {
$cache_update_needed = TRUE;
$lookup_cache = NULL;
return;
}
// Called from drupal_page_footer, we write to permanent storage if there
// changes to the lookup cache for this request.
if ($type == REGISTRY_WRITE_LOOKUP_CACHE) {
if ($cache_update_needed) {
cache_set('lookup_cache', $lookup_cache, 'cache_bootstrap');
}
return;
}
// $type is either 'interface' or 'class', so we only need the first letter to
// keep the cache key unique.
$cache_key = $type[0] . $name;
if (isset($lookup_cache[$cache_key])) {
if ($lookup_cache[$cache_key]) {
include_once DRUPAL_ROOT . '/' . $lookup_cache[$cache_key];
}
return (bool) $lookup_cache[$cache_key];
}
// This function may get called when the default database is not active, but
// there is no reason we'd ever want to not use the default database for
// this query.
$file = Database::getConnection('default', 'default')
->select('registry', 'r', array('target' => 'default'))
->fields('r', array('filename'))
// Use LIKE here to make the query case-insensitive.
->condition('r.name', db_like($name), 'LIKE')
->condition('r.type', $type)
->execute()
->fetchField();
// Flag that we've run a lookup query and need to update the cache.
$cache_update_needed = TRUE;
// Misses are valuable information worth caching, so cache even if
// $file is FALSE.
$lookup_cache[$cache_key] = $file;
if ($file) {
include_once DRUPAL_ROOT . '/' . $file;
return TRUE;
}
else {
return FALSE;
}
}
/**
* Rescans all enabled modules and rebuilds the registry.
*
* Rescans all code in modules or includes directories, storing the location of
* each interface or class in the database.
*/
function registry_rebuild() {
system_rebuild_module_data();
registry_update();
}
/**
* Updates the registry based on the latest files listed in the database.
*
* This function should be used when system_rebuild_module_data() does not need
* to be called, because it is already known that the list of files in the
* {system} table matches those in the file system.
*
* @return
* TRUE if the registry was rebuilt, FALSE if another thread was rebuilding
* in parallel and the current thread just waited for completion.
*
* @see registry_rebuild()
*/
function registry_update() {
// install_system_module() calls module_enable() which calls into this
// function during initial system installation, so the lock system is neither
// loaded nor does its storage exist yet.
$in_installer = drupal_installation_attempted();
if (!$in_installer && !lock_acquire(__FUNCTION__)) {
// Another request got the lock, wait for it to finish.
lock_wait(__FUNCTION__);
return FALSE;
}
require_once DRUPAL_ROOT . '/includes/registry.inc';
_registry_update();
if (!$in_installer) {
lock_release(__FUNCTION__);
}
return TRUE;
}
/**
* @} End of "addtogroup registry".
*/
/**
* Provides central static variable storage.
*
* All functions requiring a static variable to persist or cache data within
* a single page request are encouraged to use this function unless it is
* absolutely certain that the static variable will not need to be reset during
* the page request. By centralizing static variable storage through this
* function, other functions can rely on a consistent API for resetting any
* other function's static variables.
*
* Example:
* @code
* function language_list($field = 'language') {
* $languages = &drupal_static(__FUNCTION__);
* if (!isset($languages)) {
* // If this function is being called for the first time after a reset,
* // query the database and execute any other code needed to retrieve
* // information about the supported languages.
* ...
* }
* if (!isset($languages[$field])) {
* // If this function is being called for the first time for a particular
* // index field, then execute code needed to index the information already
* // available in $languages by the desired field.
* ...
* }
* // Subsequent invocations of this function for a particular index field
* // skip the above two code blocks and quickly return the already indexed
* // information.
* return $languages[$field];
* }
* function locale_translate_overview_screen() {
* // When building the content for the translations overview page, make
* // sure to get completely fresh information about the supported languages.
* drupal_static_reset('language_list');
* ...
* }
* @endcode
*
* In a few cases, a function can have certainty that there is no legitimate
* use-case for resetting that function's static variable. This is rare,
* because when writing a function, it's hard to forecast all the situations in
* which it will be used. A guideline is that if a function's static variable
* does not depend on any information outside of the function that might change
* during a single page request, then it's ok to use the "static" keyword
* instead of the drupal_static() function.
*
* Example:
* @code
* function actions_do(...) {
* // $stack tracks the number of recursive calls.
* static $stack;
* $stack++;
* if ($stack > variable_get('actions_max_stack', 35)) {
* ...
* return;
* }
* ...
* $stack--;
* }
* @endcode
*
* In a few cases, a function needs a resettable static variable, but the
* function is called many times (100+) during a single page request, so
* every microsecond of execution time that can be removed from the function
* counts. These functions can use a more cumbersome, but faster variant of
* calling drupal_static(). It works by storing the reference returned by
* drupal_static() in the calling function's own static variable, thereby
* removing the need to call drupal_static() for each iteration of the function.
* Conceptually, it replaces:
* @code
* $foo = &drupal_static(__FUNCTION__);
* @endcode
* with:
* @code
* // Unfortunately, this does not work.
* static $foo = &drupal_static(__FUNCTION__);
* @endcode
* However, the above line of code does not work, because PHP only allows static
* variables to be initializied by literal values, and does not allow static
* variables to be assigned to references.
* - http://php.net/manual/language.variables.scope.php#language.variables.scope.static
* - http://php.net/manual/language.variables.scope.php#language.variables.scope.references
* The example below shows the syntax needed to work around both limitations.
* For benchmarks and more information, see http://drupal.org/node/619666.
*
* Example:
* @code
* function user_access($string, $account = NULL) {
* // Use the advanced drupal_static() pattern, since this is called very often.
* static $drupal_static_fast;
* if (!isset($drupal_static_fast)) {
* $drupal_static_fast['perm'] = &drupal_static(__FUNCTION__);
* }
* $perm = &$drupal_static_fast['perm'];
* ...
* }
* @endcode
*
* @param $name
* Globally unique name for the variable. For a function with only one static,
* variable, the function name (e.g. via the PHP magic __FUNCTION__ constant)
* is recommended. For a function with multiple static variables add a
* distinguishing suffix to the function name for each one.
* @param $default_value
* Optional default value.
* @param $reset
* TRUE to reset one or all variables(s). This parameter is only used
* internally and should not be passed in; use drupal_static_reset() instead.
* (This function's return value should not be used when TRUE is passed in.)
*
* @return
* Returns a variable by reference.
*
* @see drupal_static_reset()
*/
function &drupal_static($name, $default_value = NULL, $reset = FALSE) {
static $data = array(), $default = array();
// First check if dealing with a previously defined static variable.
if (isset($data[$name]) || array_key_exists($name, $data)) {
// Non-NULL $name and both $data[$name] and $default[$name] statics exist.
if ($reset) {
// Reset pre-existing static variable to its default value.
$data[$name] = $default[$name];
}
return $data[$name];
}
// Neither $data[$name] nor $default[$name] static variables exist.
if (isset($name)) {
if ($reset) {
// Reset was called before a default is set and yet a variable must be
// returned.
return $data;
}
// First call with new non-NULL $name. Initialize a new static variable.
$default[$name] = $data[$name] = $default_value;
return $data[$name];
}
// Reset all: ($name == NULL). This needs to be done one at a time so that
// references returned by earlier invocations of drupal_static() also get
// reset.
foreach ($default as $name => $value) {
$data[$name] = $value;
}
// As the function returns a reference, the return should always be a
// variable.
return $data;
}
/**
* Resets one or all centrally stored static variable(s).
*
* @param $name
* Name of the static variable to reset. Omit to reset all variables.
* Resetting all variables should only be used, for example, for running unit
* tests with a clean environment.
*/
function drupal_static_reset($name = NULL) {
drupal_static($name, NULL, TRUE);
}
/**
* Detects whether the current script is running in a command-line environment.
*/
function drupal_is_cli() {
return (!isset($_SERVER['SERVER_SOFTWARE']) && (php_sapi_name() == 'cli' || (is_numeric($_SERVER['argc']) && $_SERVER['argc'] > 0)));
}
/**
* Formats text for emphasized display in a placeholder inside a sentence.
*
* Used automatically by format_string().
*
* @param $text
* The text to format (plain-text).
*
* @return
* The formatted text (html).
*/
function drupal_placeholder($text) {
return '' . check_plain($text) . ' ';
}
/**
* Registers a function for execution on shutdown.
*
* Wrapper for register_shutdown_function() that catches thrown exceptions to
* avoid "Exception thrown without a stack frame in Unknown".
*
* @param $callback
* The shutdown function to register.
* @param ...
* Additional arguments to pass to the shutdown function.
*
* @return
* Array of shutdown functions to be executed.
*
* @see register_shutdown_function()
* @ingroup php_wrappers
*/
function &drupal_register_shutdown_function($callback = NULL) {
// We cannot use drupal_static() here because the static cache is reset during
// batch processing, which breaks batch handling.
static $callbacks = array();
if (isset($callback)) {
// Only register the internal shutdown function once.
if (empty($callbacks)) {
register_shutdown_function('_drupal_shutdown_function');
}
$args = func_get_args();
array_shift($args);
// Save callback and arguments
$callbacks[] = array('callback' => $callback, 'arguments' => $args);
}
return $callbacks;
}
/**
* Executes registered shutdown functions.
*/
function _drupal_shutdown_function() {
$callbacks = &drupal_register_shutdown_function();
// Set the CWD to DRUPAL_ROOT as it is not guaranteed to be the same as it
// was in the normal context of execution.
chdir(DRUPAL_ROOT);
try {
// Manually iterate over the array instead of using a foreach loop.
// A foreach operates on a copy of the array, so any shutdown functions that
// were added from other shutdown functions would never be called.
while ($callback = current($callbacks)) {
call_user_func_array($callback['callback'], $callback['arguments']);
next($callbacks);
}
}
catch (Exception $exception) {
// If we are displaying errors, then do so with no possibility of a further uncaught exception being thrown.
require_once DRUPAL_ROOT . '/includes/errors.inc';
if (error_displayable()) {
print 'Uncaught exception thrown in shutdown function. ';
print '' . _drupal_render_exception_safe($exception) . '
';
}
}
}
/**
* Compares the memory required for an operation to the available memory.
*
* @param $required
* The memory required for the operation, expressed as a number of bytes with
* optional SI or IEC binary unit prefix (e.g. 2, 3K, 5MB, 10G, 6GiB, 8bytes,
* 9mbytes).
* @param $memory_limit
* (optional) The memory limit for the operation, expressed as a number of
* bytes with optional SI or IEC binary unit prefix (e.g. 2, 3K, 5MB, 10G,
* 6GiB, 8bytes, 9mbytes). If no value is passed, the current PHP
* memory_limit will be used. Defaults to NULL.
*
* @return
* TRUE if there is sufficient memory to allow the operation, or FALSE
* otherwise.
*/
function drupal_check_memory_limit($required, $memory_limit = NULL) {
if (!isset($memory_limit)) {
$memory_limit = ini_get('memory_limit');
}
// There is sufficient memory if:
// - No memory limit is set.
// - The memory limit is set to unlimited (-1).
// - The memory limit is greater than the memory required for the operation.
return ((!$memory_limit) || ($memory_limit == -1) || (parse_size($memory_limit) >= parse_size($required)));
}
/**
* Invalidates a PHP file from any active opcode caches.
*
* If the opcode cache does not support the invalidation of individual files,
* the entire cache will be flushed.
*
* @param string $filepath
* The absolute path of the PHP file to invalidate.
*/
function drupal_clear_opcode_cache($filepath) {
if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 50300) {
// Below PHP 5.3, clearstatcache does not accept any function parameters.
clearstatcache();
}
else {
clearstatcache(TRUE, $filepath);
}
// Zend OPcache.
if (function_exists('opcache_invalidate')) {
opcache_invalidate($filepath, TRUE);
}
// APC.
if (function_exists('apc_delete_file')) {
// apc_delete_file() throws a PHP warning in case the specified file was
// not compiled yet.
// @see http://php.net/apc-delete-file
@apc_delete_file($filepath);
}
}
/**
* Drupal's wrapper around PHP's setcookie() function.
*
* This allows the cookie's $value and $options to be altered.
*
* @param $name
* The name of the cookie.
* @param $value
* The value of the cookie.
* @param $options
* An associative array which may have any of the keys expires, path, domain,
* secure, httponly, samesite.
*
* @see setcookie()
* @ingroup php_wrappers
*/
function drupal_setcookie($name, $value, $options) {
$options = _drupal_cookie_params($options);
if (\PHP_VERSION_ID >= 70300) {
setcookie($name, $value, $options);
}
else {
$defaults = array(
'expires' => 0,
'path' => '',
'domain' => '',
'secure' => FALSE,
'httponly' => FALSE,
);
$options += $defaults;
setcookie($name, $value, $options['expires'], $options['path'], $options['domain'], $options['secure'], $options['httponly']);
}
}
/**
* Process the params for cookies. This emulates support for the SameSite
* attribute in earlier versions of PHP, and allows the value of that attribute
* to be overridden.
*
* @param $options
* An associative array which may have any of the keys expires, path, domain,
* secure, httponly, samesite.
*
* @return
* An associative array which may have any of the keys expires, path, domain,
* secure, httponly, and samesite.
*/
function _drupal_cookie_params($options) {
$options['samesite'] = _drupal_samesite_cookie($options);
if (\PHP_VERSION_ID < 70300) {
// Emulate SameSite support in older PHP versions.
if (!empty($options['samesite'])) {
// Ensure the SameSite attribute is only added once.
if (!preg_match('/SameSite=/i', $options['path'])) {
$options['path'] .= '; SameSite=' . $options['samesite'];
}
}
}
return $options;
}
/**
* Determine the value for the samesite cookie attribute, in the following order
* of precedence:
*
* 1) A value explicitly passed to drupal_setcookie()
* 2) A value set in $conf['samesite_cookie_value']
* 3) The setting from php ini
* 4) The default of None, or FALSE (no attribute) if the cookie is not Secure
*
* @param $options
* An associative array as passed to drupal_setcookie().
* @return
* The value for the samesite cookie attribute.
*/
function _drupal_samesite_cookie($options) {
if (isset($options['samesite'])) {
return $options['samesite'];
}
$override = variable_get('samesite_cookie_value', NULL);
if ($override !== NULL) {
return $override;
}
$ini_options = session_get_cookie_params();
if (isset($ini_options['samesite'])) {
return $ini_options['samesite'];
}
return empty($options['secure']) ? FALSE : 'None';
}
get($cid);
}
/**
* Returns data from the persistent cache when given an array of cache IDs.
*
* @param $cids
* An array of cache IDs for the data to retrieve. This is passed by
* reference, and will have the IDs successfully returned from cache removed.
* @param $bin
* The cache bin where the data is stored.
*
* @return
* An array of the items successfully returned from cache indexed by cid.
*/
function cache_get_multiple(array &$cids, $bin = 'cache') {
return _cache_get_object($bin)->getMultiple($cids);
}
/**
* Stores data in the persistent cache.
*
* The persistent cache is split up into several cache bins. In the default
* cache implementation, each cache bin corresponds to a database table by the
* same name. Other implementations might want to store several bins in data
* structures that get flushed together. While it is not a problem for most
* cache bins if the entries in them are flushed before their expire time, some
* might break functionality or are extremely expensive to recalculate. The
* other bins are expired automatically by core. Contributed modules can add
* additional bins and get them expired automatically by implementing
* hook_flush_caches().
*
* The reasons for having several bins are as follows:
* - Smaller bins mean smaller database tables and allow for faster selects and
* inserts.
* - We try to put fast changing cache items and rather static ones into
* different bins. The effect is that only the fast changing bins will need a
* lot of writes to disk. The more static bins will also be better cacheable
* with MySQL's query cache.
*
* @param $cid
* The cache ID of the data to store.
* @param $data
* The data to store in the cache. Complex data types will be automatically
* serialized before insertion. Strings will be stored as plain text and are
* not serialized. Some storage engines only allow objects up to a maximum of
* 1MB in size to be stored by default. When caching large arrays or similar,
* take care to ensure $data does not exceed this size.
* @param $bin
* (optional) The cache bin to store the data in. Valid core values are:
* - cache: (default) Generic cache storage bin (used for theme registry,
* locale date, list of simpletest tests, etc.).
* - cache_block: Stores the content of various blocks.
* - cache_bootstrap: Stores the class registry, the system list of modules,
* the list of which modules implement which hooks, and the Drupal variable
* list.
* - cache_field: Stores the field data belonging to a given object.
* - cache_filter: Stores filtered pieces of content.
* - cache_form: Stores multistep forms. Flushing this bin means that some
* forms displayed to users lose their state and the data already submitted
* to them. This bin should not be flushed before its expired time.
* - cache_menu: Stores the structure of visible navigation menus per page.
* - cache_page: Stores generated pages for anonymous users. It is flushed
* very often, whenever a page changes, at least for every node and comment
* submission. This is the only bin affected by the page cache setting on
* the administrator panel.
* - cache_path: Stores the system paths that have an alias.
* @param $expire
* (optional) Controls the maximum lifetime of this cache entry. Note that
* caches might be subject to clearing at any time, so this setting does not
* guarantee a minimum lifetime. With this in mind, the cache should not be
* used for data that must be kept during a cache clear, like sessions.
*
* Use one of the following values:
* - CACHE_PERMANENT: Indicates that the item should never be removed unless
* explicitly told to using cache_clear_all() with a cache ID.
* - CACHE_TEMPORARY: Indicates that the item should be removed at the next
* general cache wipe.
* - A Unix timestamp: Indicates that the item should be kept at least until
* the given time, after which it behaves like CACHE_TEMPORARY.
*
* @see _update_cache_set()
* @see cache_get()
*/
function cache_set($cid, $data, $bin = 'cache', $expire = CACHE_PERMANENT) {
return _cache_get_object($bin)->set($cid, $data, $expire);
}
/**
* Expires data from the cache.
*
* If called with the arguments $cid and $bin set to NULL or omitted, then
* expirable entries will be cleared from the cache_page and cache_block bins,
* and the $wildcard argument is ignored.
*
* @param $cid
* If set, the cache ID or an array of cache IDs. Otherwise, all cache entries
* that can expire are deleted. The $wildcard argument will be ignored if set
* to NULL.
* @param $bin
* If set, the cache bin to delete from. Mandatory argument if $cid is set.
* @param $wildcard
* If TRUE, the $cid argument must contain a string value and cache IDs
* starting with $cid are deleted in addition to the exact cache ID specified
* by $cid. If $wildcard is TRUE and $cid is '*', the entire cache is emptied.
*/
function cache_clear_all($cid = NULL, $bin = NULL, $wildcard = FALSE) {
if (!isset($cid) && !isset($bin)) {
// Clear the block cache first, so stale data will
// not end up in the page cache.
if (module_exists('block')) {
cache_clear_all(NULL, 'cache_block');
}
cache_clear_all(NULL, 'cache_page');
return;
}
return _cache_get_object($bin)->clear($cid, $wildcard);
}
/**
* Checks if a cache bin is empty.
*
* A cache bin is considered empty if it does not contain any valid data for any
* cache ID.
*
* @param $bin
* The cache bin to check.
*
* @return
* TRUE if the cache bin specified is empty.
*/
function cache_is_empty($bin) {
return _cache_get_object($bin)->isEmpty();
}
/**
* Defines an interface for cache implementations.
*
* All cache implementations have to implement this interface.
* DrupalDatabaseCache provides the default implementation, which can be
* consulted as an example.
*
* To make Drupal use your implementation for a certain cache bin, you have to
* set a variable with the name of the cache bin as its key and the name of
* your class as its value. For example, if your implementation of
* DrupalCacheInterface was called MyCustomCache, the following line would make
* Drupal use it for the 'cache_page' bin:
* @code
* variable_set('cache_class_cache_page', 'MyCustomCache');
* @endcode
*
* Additionally, you can register your cache implementation to be used by
* default for all cache bins by setting the variable 'cache_default_class' to
* the name of your implementation of the DrupalCacheInterface, e.g.
* @code
* variable_set('cache_default_class', 'MyCustomCache');
* @endcode
*
* To implement a completely custom cache bin, use the same variable format:
* @code
* variable_set('cache_class_custom_bin', 'MyCustomCache');
* @endcode
* To access your custom cache bin, specify the name of the bin when storing
* or retrieving cached data:
* @code
* cache_set($cid, $data, 'custom_bin', $expire);
* cache_get($cid, 'custom_bin');
* @endcode
*
* @see _cache_get_object()
* @see DrupalDatabaseCache
*/
interface DrupalCacheInterface {
/**
* Returns data from the persistent cache.
*
* Data may be stored as either plain text or as serialized data. cache_get()
* will automatically return unserialized objects and arrays.
*
* @param $cid
* The cache ID of the data to retrieve.
*
* @return
* The cache or FALSE on failure.
*/
function get($cid);
/**
* Returns data from the persistent cache when given an array of cache IDs.
*
* @param $cids
* An array of cache IDs for the data to retrieve. This is passed by
* reference, and will have the IDs successfully returned from cache
* removed.
*
* @return
* An array of the items successfully returned from cache indexed by cid.
*/
function getMultiple(&$cids);
/**
* Stores data in the persistent cache.
*
* @param $cid
* The cache ID of the data to store.
* @param $data
* The data to store in the cache. Complex data types will be automatically
* serialized before insertion. Strings will be stored as plain text and not
* serialized. Some storage engines only allow objects up to a maximum of
* 1MB in size to be stored by default. When caching large arrays or
* similar, take care to ensure $data does not exceed this size.
* @param $expire
* (optional) Controls the maximum lifetime of this cache entry. Note that
* caches might be subject to clearing at any time, so this setting does not
* guarantee a minimum lifetime. With this in mind, the cache should not be
* used for data that must be kept during a cache clear, like sessions.
*
* Use one of the following values:
* - CACHE_PERMANENT: Indicates that the item should never be removed unless
* explicitly told to using cache_clear_all() with a cache ID.
* - CACHE_TEMPORARY: Indicates that the item should be removed at the next
* general cache wipe.
* - A Unix timestamp: Indicates that the item should be kept at least until
* the given time, after which it behaves like CACHE_TEMPORARY.
*/
function set($cid, $data, $expire = CACHE_PERMANENT);
/**
* Expires data from the cache.
*
* If called without arguments, expirable entries will be cleared from the
* cache_page and cache_block bins.
*
* @param $cid
* If set, the cache ID or an array of cache IDs. Otherwise, all cache
* entries that can expire are deleted. The $wildcard argument will be
* ignored if set to NULL.
* @param $wildcard
* If TRUE, the $cid argument must contain a string value and cache IDs
* starting with $cid are deleted in addition to the exact cache ID
* specified by $cid. If $wildcard is TRUE and $cid is '*', the entire
* cache is emptied.
*/
function clear($cid = NULL, $wildcard = FALSE);
/**
* Checks if a cache bin is empty.
*
* A cache bin is considered empty if it does not contain any valid data for
* any cache ID.
*
* @return
* TRUE if the cache bin specified is empty.
*/
function isEmpty();
}
/**
* Defines a default cache implementation.
*
* This is Drupal's default cache implementation. It uses the database to store
* cached data. Each cache bin corresponds to a database table by the same name.
*/
class DrupalDatabaseCache implements DrupalCacheInterface {
protected $bin;
/**
* Constructs a DrupalDatabaseCache object.
*
* @param $bin
* The cache bin for which the object is created.
*/
function __construct($bin) {
$this->bin = $bin;
}
/**
* Implements DrupalCacheInterface::get().
*/
function get($cid) {
$cids = array($cid);
$cache = $this->getMultiple($cids);
return reset($cache);
}
/**
* Implements DrupalCacheInterface::getMultiple().
*/
function getMultiple(&$cids) {
try {
// Garbage collection necessary when enforcing a minimum cache lifetime.
$this->garbageCollection($this->bin);
// When serving cached pages, the overhead of using db_select() was found
// to add around 30% overhead to the request. Since $this->bin is a
// variable, this means the call to db_query() here uses a concatenated
// string. This is highly discouraged under any other circumstances, and
// is used here only due to the performance overhead we would incur
// otherwise. When serving an uncached page, the overhead of using
// db_select() is a much smaller proportion of the request.
$result = db_query('SELECT cid, data, created, expire, serialized FROM {' . db_escape_table($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids));
$cache = array();
foreach ($result as $item) {
$item = $this->prepareItem($item);
if ($item) {
$cache[$item->cid] = $item;
}
}
$cids = array_diff($cids, array_keys($cache));
return $cache;
}
catch (Exception $e) {
// If the database is never going to be available, cache requests should
// return FALSE in order to allow exception handling to occur.
return array();
}
}
/**
* Garbage collection for get() and getMultiple().
*
* @param $bin
* The bin being requested.
*/
protected function garbageCollection() {
$cache_lifetime = variable_get('cache_lifetime', 0);
// Clean-up the per-user cache expiration session data, so that the session
// handler can properly clean-up the session data for anonymous users.
if (isset($_SESSION['cache_expiration'])) {
$expire = REQUEST_TIME - $cache_lifetime;
foreach ($_SESSION['cache_expiration'] as $bin => $timestamp) {
if ($timestamp < $expire) {
unset($_SESSION['cache_expiration'][$bin]);
}
}
if (!$_SESSION['cache_expiration']) {
unset($_SESSION['cache_expiration']);
}
}
// Garbage collection of temporary items is only necessary when enforcing
// a minimum cache lifetime.
if (!$cache_lifetime) {
return;
}
// When cache lifetime is in force, avoid running garbage collection too
// often since this will remove temporary cache items indiscriminately.
$cache_flush = variable_get('cache_flush_' . $this->bin, 0);
if ($cache_flush && ($cache_flush + $cache_lifetime <= REQUEST_TIME)) {
// Reset the variable immediately to prevent a meltdown in heavy load situations.
variable_set('cache_flush_' . $this->bin, 0);
// Time to flush old cache data
db_delete($this->bin)
->condition('expire', CACHE_PERMANENT, '<>')
->condition('expire', $cache_flush, '<=')
->execute();
}
}
/**
* Prepares a cached item.
*
* Checks that items are either permanent or did not expire, and unserializes
* data as appropriate.
*
* @param $cache
* An item loaded from cache_get() or cache_get_multiple().
*
* @return
* The item with data unserialized as appropriate or FALSE if there is no
* valid item to load.
*/
protected function prepareItem($cache) {
global $user;
if (!isset($cache->data)) {
return FALSE;
}
// If the cached data is temporary and subject to a per-user minimum
// lifetime, compare the cache entry timestamp with the user session
// cache_expiration timestamp. If the cache entry is too old, ignore it.
if ($cache->expire != CACHE_PERMANENT && variable_get('cache_lifetime', 0) && isset($_SESSION['cache_expiration'][$this->bin]) && $_SESSION['cache_expiration'][$this->bin] > $cache->created) {
// Ignore cache data that is too old and thus not valid for this user.
return FALSE;
}
// If the data is permanent or not subject to a minimum cache lifetime,
// unserialize and return the cached data.
if ($cache->serialized) {
$cache->data = unserialize($cache->data);
}
return $cache;
}
/**
* Implements DrupalCacheInterface::set().
*/
function set($cid, $data, $expire = CACHE_PERMANENT) {
$fields = array(
'serialized' => 0,
'created' => REQUEST_TIME,
'expire' => $expire,
);
if (!is_string($data)) {
$fields['data'] = serialize($data);
$fields['serialized'] = 1;
}
else {
$fields['data'] = $data;
$fields['serialized'] = 0;
}
try {
db_merge($this->bin)
->key(array('cid' => $cid))
->fields($fields)
->execute();
}
catch (Exception $e) {
// The database may not be available, so we'll ignore cache_set requests.
}
}
/**
* Implements DrupalCacheInterface::clear().
*/
function clear($cid = NULL, $wildcard = FALSE) {
global $user;
if (empty($cid)) {
if (variable_get('cache_lifetime', 0)) {
// We store the time in the current user's session. We then simulate
// that the cache was flushed for this user by not returning cached
// data that was cached before the timestamp.
$_SESSION['cache_expiration'][$this->bin] = REQUEST_TIME;
$cache_flush = variable_get('cache_flush_' . $this->bin, 0);
if ($cache_flush == 0) {
// This is the first request to clear the cache, start a timer.
variable_set('cache_flush_' . $this->bin, REQUEST_TIME);
}
elseif (REQUEST_TIME > ($cache_flush + variable_get('cache_lifetime', 0))) {
// Clear the cache for everyone, cache_lifetime seconds have
// passed since the first request to clear the cache.
db_delete($this->bin)
->condition('expire', CACHE_PERMANENT, '<>')
->condition('expire', REQUEST_TIME, '<')
->execute();
variable_set('cache_flush_' . $this->bin, 0);
}
}
else {
// No minimum cache lifetime, flush all temporary cache entries now.
db_delete($this->bin)
->condition('expire', CACHE_PERMANENT, '<>')
->condition('expire', REQUEST_TIME, '<')
->execute();
}
}
else {
if ($wildcard) {
if ($cid == '*') {
// Check if $this->bin is a cache table before truncating. Other
// cache_clear_all() operations throw a PDO error in this situation,
// so we don't need to verify them first. This ensures that non-cache
// tables cannot be truncated accidentally.
if ($this->isValidBin()) {
db_truncate($this->bin)->execute();
}
else {
throw new Exception(t('Invalid or missing cache bin specified: %bin', array('%bin' => $this->bin)));
}
}
else {
db_delete($this->bin)
->condition('cid', db_like($cid) . '%', 'LIKE')
->execute();
}
}
elseif (is_array($cid)) {
// Delete in chunks when a large array is passed.
do {
db_delete($this->bin)
->condition('cid', array_splice($cid, 0, 1000), 'IN')
->execute();
}
while (count($cid));
}
else {
db_delete($this->bin)
->condition('cid', $cid)
->execute();
}
}
}
/**
* Implements DrupalCacheInterface::isEmpty().
*/
function isEmpty() {
$this->garbageCollection();
$query = db_select($this->bin);
$query->addExpression('1');
$result = $query->range(0, 1)
->execute()
->fetchField();
return empty($result);
}
/**
* Checks if $this->bin represents a valid cache table.
*
* This check is required to ensure that non-cache tables are not truncated
* accidentally when calling cache_clear_all().
*
* @return boolean
*/
function isValidBin() {
if ($this->bin == 'cache' || substr($this->bin, 0, 6) == 'cache_') {
// Skip schema check for bins with standard table names.
return TRUE;
}
// These fields are required for any cache table.
$fields = array('cid', 'data', 'expire', 'created', 'serialized');
// Load the table schema.
$schema = drupal_get_schema($this->bin);
// Confirm that all fields are present.
return isset($schema['fields']) && !array_diff($fields, array_keys($schema['fields']));
}
}
$uri) {
$xml_rdf_namespaces[] = 'xmlns:' . $prefix . '="' . $uri . '"';
}
}
return count($xml_rdf_namespaces) ? "\n " . implode("\n ", $xml_rdf_namespaces) : '';
}
/**
* Adds output to the HEAD tag of the HTML page.
*
* This function can be called as long as the headers aren't sent. Pass no
* 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()
*/
function drupal_add_html_head($data = NULL, $key = NULL) {
$stored_head = &drupal_static(__FUNCTION__);
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;
}
return $stored_head;
}
/**
* 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(
'http-equiv' => 'Content-Type',
'content' => 'text/html; charset=utf-8',
),
// 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;
}
/**
* Retrieves output to be displayed in the HEAD tag of the HTML page.
*/
function drupal_get_html_head() {
$elements = drupal_add_html_head();
drupal_alter('html_head', $elements);
return drupal_render($elements);
}
/**
* Adds a feed URL for the current page.
*
* This function can be called as long the HTML header hasn't been sent.
*
* @param $url
* An internal system path or a fully qualified external URL of the feed.
* @param $title
* The title of the feed.
*/
function drupal_add_feed($url = NULL, $title = '') {
$stored_feed_links = &drupal_static(__FUNCTION__, array());
if (isset($url)) {
$stored_feed_links[$url] = theme('feed_icon', array('url' => $url, 'title' => $title));
drupal_add_html_head_link(array(
'rel' => 'alternate',
'type' => 'application/rss+xml',
'title' => $title,
// Force the URL to be absolute, for consistency with other tags
// output by Drupal.
'href' => url($url, array('absolute' => TRUE)),
));
}
return $stored_feed_links;
}
/**
* Gets the feed URLs for the current page.
*
* @param $delimiter
* A delimiter to split feeds by.
*/
function drupal_get_feeds($delimiter = "\n") {
$feeds = drupal_add_feed();
return implode($delimiter, $feeds);
}
/**
* @defgroup http_handling HTTP handling
* @{
* Functions to properly handle HTTP responses.
*/
/**
* Processes a URL query parameter array to remove unwanted elements.
*
* @param $query
* (optional) An array to be processed. Defaults to $_GET.
* @param $exclude
* (optional) A list of $query array keys to remove. Use "parent[child]" to
* exclude nested items. Defaults to array('q').
* @param $parent
* Internal use only. Used to build the $query array key for nested items.
*
* @return
* An array containing query parameters, which can be used for url().
*/
function drupal_get_query_parameters(array $query = NULL, array $exclude = array('q'), $parent = '') {
// 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);
}
$params = array();
foreach ($query as $key => $value) {
$string_key = ($parent ? $parent . '[' . $key . ']' : $key);
if (isset($exclude[$string_key])) {
continue;
}
if (is_array($value)) {
$params[$key] = drupal_get_query_parameters($value, $exclude, $string_key);
}
else {
$params[$key] = $value;
}
}
return $params;
}
/**
* Splits a URL-encoded query string into an array.
*
* @param $query
* The query string to split.
*
* @return
* An array of URL decoded couples $param_name => $value.
*/
function drupal_get_query_array($query) {
$result = array();
if (!empty($query)) {
foreach (explode('&', $query) as $param) {
$param = explode('=', $param, 2);
$result[$param[0]] = isset($param[1]) ? rawurldecode($param[1]) : '';
}
}
return $result;
}
/**
* Parses an array into a valid, rawurlencoded query string.
*
* 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);
// Recurse into children.
if (is_array($value)) {
$params[] = drupal_http_build_query($value, $key);
}
// If a query parameter value is NULL, only append its key.
elseif (!isset($value)) {
$params[] = $key;
}
else {
// For better readability of paths in query strings, we decode slashes.
$params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value));
}
}
return implode('&', $params);
}
/**
* Prepares a 'destination' URL query parameter for use with drupal_goto().
*
* 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.
*
* @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()
* @see drupal_goto()
*/
function drupal_get_destination() {
$destination = &drupal_static(__FUNCTION__);
if (isset($destination)) {
return $destination;
}
if (isset($_GET['destination'])) {
$destination = array('destination' => $_GET['destination']);
}
else {
$path = $_GET['q'];
$query = drupal_http_build_query(drupal_get_query_parameters());
if ($query != '') {
$path .= '?' . $query;
}
$destination = array('destination' => $path);
}
return $destination;
}
/**
* Parses a URL string into its path, query, and fragment components.
*
* This function splits both internal paths like @code node?b=c#d @endcode and
* external URLs like @code https://example.com/a?b=c#d @endcode into their
* component parts. See
* @link http://tools.ietf.org/html/rfc3986#section-3 RFC 3986 @endlink for an
* explanation of what the component parts are.
*
* Note that, unlike the RFC, when passed an external URL, this function
* groups the scheme, authority, and path together into the path component.
*
* @param string $url
* The internal path or external URL string to parse.
*
* @return array
* An associative array containing:
* - path: The path component of $url. If $url is an external URL, this
* includes the scheme, authority, and path.
* - query: An array of query parameters from $url, if they exist.
* - fragment: The fragment component from $url, if it exists.
*
* @see drupal_goto()
* @see l()
* @see url()
* @see http://tools.ietf.org/html/rfc3986
*
* @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 {
// 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);
if (isset($parts['query'])) {
parse_str($parts['query'], $options['query']);
}
if (isset($parts['fragment'])) {
$options['fragment'] = $parts['fragment'];
}
}
// The 'q' parameter contains the path of the current page if clean URLs are
// disabled. It overrides the 'path' of the URL when present, even if clean
// URLs are enabled, due to how Apache rewriting rules work. The path
// parameter must be a string.
if (isset($options['query']['q']) && is_string($options['query']['q'])) {
$options['path'] = $options['query']['q'];
unset($options['query']['q']);
}
return $options;
}
/**
* Encodes a Drupal path for use in a URL.
*
* For aesthetic reasons slashes are not escaped.
*
* Note that url() takes care of calling this function, so a path passed to that
* function should not be encoded in advance.
*
* @param $path
* The Drupal path to encode.
*/
function drupal_encode_path($path) {
return str_replace('%2F', '/', rawurlencode((string) $path));
}
/**
* Sends the user to a different page.
*
* This issues an on-site HTTP redirect. The function makes sure the redirected
* URL is formatted correctly.
*
* Usually the redirected URL is constructed from this function's input
* parameters. However you may override that behavior by setting a
* destination in either the $_REQUEST-array (i.e. by using
* the query string of an URI) This is used to direct the user back to
* the proper page after completing a form. For example, after editing
* a post on the 'admin/content'-page or after having logged on using the
* 'user login'-block in a sidebar. The function drupal_get_destination()
* can be used to help set the destination URL.
*
* Drupal will ensure that messages set by drupal_set_message() and other
* session data are written to the database before the user is redirected.
*
* This function ends the request; use it instead of a return in your menu
* callback.
*
* @param $path
* (optional) A Drupal path or a full URL, which will be passed to url() to
* compute the redirect for the URL.
* @param $options
* (optional) An associative array of additional URL options to pass to url().
* @param $http_response_code
* (optional) The HTTP status code to use for the redirection, defaults to
* 302. The valid values for 3xx redirection status codes are defined in
* @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.
*
* @see drupal_get_destination()
* @see url()
*/
function drupal_goto($path = '', array $options = array(), $http_response_code = 302) {
// A destination in $_GET always overrides the function arguments.
// 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'])) {
$destination = drupal_parse_url($_GET['destination']);
// Double check the path derived by drupal_parse_url() is not external.
if (!url_is_external($destination['path'])) {
$path = $destination['path'];
}
$options['query'] = $destination['query'];
$options['fragment'] = $destination['fragment'];
}
// In some cases modules call drupal_goto(current_path()). We need to ensure
// that such a redirect is not to an external URL.
if ($path === current_path() && empty($options['external']) && url_is_external($path)) {
// Force url() to generate a non-external URL.
$options['external'] = FALSE;
}
drupal_alter('drupal_goto', $path, $options, $http_response_code);
// The 'Location' HTTP header must be absolute.
$options['absolute'] = TRUE;
$url = url($path, $options);
header('Location: ' . $url, TRUE, $http_response_code);
// The "Location" header sends a redirect status code to the HTTP daemon. In
// some cases this can be wrong, so we make sure none of the code below the
// drupal_goto() call gets executed upon redirection.
drupal_exit($url);
}
/**
* Delivers a "site is under maintenance" message to the browser.
*
* Page callback functions wanting to report a "site offline" message should
* return MENU_SITE_OFFLINE instead of calling drupal_site_offline(). However,
* functions that are invoked in contexts where that return value might not
* bubble up to menu_execute_active_handler() should call drupal_site_offline().
*/
function drupal_site_offline() {
drupal_deliver_page(MENU_SITE_OFFLINE);
}
/**
* Delivers a "page not found" error to the browser.
*
* Page callback functions wanting to report a "page not found" message should
* return MENU_NOT_FOUND instead of calling drupal_not_found(). However,
* functions that are invoked in contexts where that return value might not
* bubble up to menu_execute_active_handler() should call drupal_not_found().
*/
function drupal_not_found() {
drupal_deliver_page(MENU_NOT_FOUND);
}
/**
* Delivers an "access denied" error to the browser.
*
* Page callback functions wanting to report an "access denied" message should
* return MENU_ACCESS_DENIED instead of calling drupal_access_denied(). However,
* functions that are invoked in contexts where that return value might not
* bubble up to menu_execute_active_handler() should call
* drupal_access_denied().
*/
function drupal_access_denied() {
drupal_deliver_page(MENU_ACCESS_DENIED);
}
/**
* Performs an HTTP request.
*
* This is a flexible and powerful HTTP client implementation. Correctly
* handles GET, POST, PUT or any other HTTP requests. Handles redirects.
*
* @param $url
* A string containing a fully qualified URI.
* @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: An array containing the values for the request body or a string
* containing the request body, formatted as
* 'param=value¶m=value&...'; to generate this, use
* drupal_http_build_query(). 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().
*
* @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.
* - redirect_url: If redirected, a string containing the URL of the redirect
* target.
* - 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.
*
* @see drupal_http_build_query()
*/
function drupal_http_request($url, array $options = array()) {
// Allow an alternate HTTP client library to replace Drupal's default
// implementation.
$override_function = variable_get('drupal_http_request_function', FALSE);
if (!empty($override_function) && function_exists($override_function)) {
return $override_function($url, $options);
}
$result = new stdClass();
// Parse the URL and make sure we can handle the schema.
$uri = @parse_url($url);
if ($uri == FALSE) {
$result->error = 'unable to parse URL';
$result->code = -1001;
return $result;
}
if (!isset($uri['scheme'])) {
$result->error = 'missing schema';
$result->code = -1002;
return $result;
}
timer_start(__FUNCTION__);
// Merge the default options.
$options += array(
'headers' => array(),
'method' => 'GET',
'data' => NULL,
'max_redirects' => 3,
'timeout' => 30.0,
'context' => NULL,
);
// Merge the default headers.
$options['headers'] += array(
'User-Agent' => 'Drupal (+http://drupal.org/)',
);
// stream_socket_client() requires timeout to be a float.
$options['timeout'] = (float) $options['timeout'];
// 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;
}
}
switch ($uri['scheme']) {
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.
if (!isset($options['headers']['Host'])) {
$options['headers']['Host'] = $uri['host'];
$options['headers']['Host'] .= isset($uri['port']) && $uri['port'] != 80 ? ':' . $uri['port'] : '';
}
break;
case 'http':
case 'feed':
$port = isset($uri['port']) ? $uri['port'] : 80;
$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.
if (!isset($options['headers']['Host'])) {
$options['headers']['Host'] = $uri['host'] . ($port != 80 ? ':' . $port : '');
}
break;
case 'https':
// Note: Only works when PHP is compiled with OpenSSL support.
$port = isset($uri['port']) ? $uri['port'] : 443;
$socket = 'ssl://' . $uri['host'] . ':' . $port;
if (!isset($options['headers']['Host'])) {
$options['headers']['Host'] = $uri['host'] . ($port != 443 ? ':' . $port : '');
}
break;
default:
$result->error = 'invalid schema ' . $uri['scheme'];
$result->code = -1003;
return $result;
}
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']);
}
// Make sure the socket opened properly.
if (!$fp) {
// When a network error occurs, we use a negative number so it does not
// clash with the HTTP status codes.
$result->code = -$errno;
$result->error = trim($errstr) ? trim($errstr) : t('Error opening socket @socket', array('@socket' => $socket));
// Mark that this request failed. This will trigger a check of the web
// server's ability to make outgoing HTTP requests the next time that
// requirements checking is performed.
// See system_requirements().
variable_set('drupal_http_request_fails', TRUE);
return $result;
}
// Construct the path to act on.
$path = isset($uri['path']) ? $uri['path'] : '/';
if (isset($uri['query'])) {
$path .= '?' . $uri['query'];
}
// Convert array $options['data'] to query string.
if (is_array($options['data'])) {
$options['data'] = drupal_http_build_query($options['data']);
}
// 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.
$content_length = strlen((string) $options['data']);
if ($content_length > 0 || $options['method'] == 'POST' || $options['method'] == 'PUT') {
$options['headers']['Content-Length'] = $content_length;
}
// If the server URL has a user then attempt to use basic authentication.
if (isset($uri['user'])) {
$options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (isset($uri['pass']) ? ':' . $uri['pass'] : ':'));
}
// 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.
$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']);
}
$request = $options['method'] . ' ' . $path . " HTTP/1.0\r\n";
foreach ($options['headers'] as $name => $value) {
$request .= $name . ': ' . trim($value) . "\r\n";
}
$request .= "\r\n" . $options['data'];
$result->request = $request;
// 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);
}
// 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'];
$response = '';
while ($alive) {
// Calculate how much time is left of the original timeout value.
$timeout = $options['timeout'] - timer_read(__FUNCTION__) / 1000;
if ($timeout <= 0) {
$info['timed_out'] = TRUE;
break;
}
stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1)));
$chunk = fread($fp, 1024);
$response .= $chunk;
$info = stream_get_meta_data($fp);
$alive = !$info['eof'] && !$info['timed_out'] && $chunk;
}
fclose($fp);
if ($info['timed_out']) {
$result->code = HTTP_REQUEST_TIMEOUT;
$result->error = 'request timed out';
return $result;
}
// Parse response headers from the response body.
// 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);
$response = preg_split("/\r\n|\n|\r/", $response);
// Parse the response status line.
$response_status_array = _drupal_parse_response_status(trim(array_shift($response)));
$result->protocol = $response_status_array['http_version'];
$result->status_message = $response_status_array['reason_phrase'];
$code = $response_status_array['response_code'];
$result->headers = array();
// Parse the response headers.
while ($line = trim((string) array_shift($response))) {
list($name, $value) = explode(':', $line, 2);
$name = strtolower($name);
if (isset($result->headers[$name]) && $name == 'set-cookie') {
// RFC 2109: the Set-Cookie response header comprises the token Set-
// Cookie:, followed by a comma-separated list of one or more cookies.
$result->headers[$name] .= ',' . trim($value);
}
else {
$result->headers[$name] = trim($value);
}
}
$responses = array(
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',
);
// RFC 2616 states that all unknown HTTP codes must be treated the same as the
// base code in their class.
if (!isset($responses[$code])) {
$code = floor($code / 100) * 100;
}
$result->code = $code;
switch ($code) {
case 200: // OK
case 201: // Created
case 202: // Accepted
case 203: // Non-Authoritative Information
case 204: // No Content
case 205: // Reset Content
case 206: // Partial Content
case 304: // Not modified
break;
case 301: // Moved permanently
case 302: // Moved temporarily
case 307: // Moved temporarily
$location = $result->headers['location'];
$options['timeout'] -= timer_read(__FUNCTION__) / 1000;
if ($options['timeout'] <= 0) {
$result->code = HTTP_REQUEST_TIMEOUT;
$result->error = 'request timed out';
}
elseif ($options['max_redirects']) {
// Redirect to the new location.
$options['max_redirects']--;
// Check if we need to remove any potentially sensitive headers before
// following the redirect.
// @see https://www.rfc-editor.org/rfc/rfc9110.html#name-redirection-3xx
if (_drupal_should_strip_sensitive_headers_on_http_redirect($url, $location)) {
unset($options['headers']['Cookie']);
unset($options['headers']['Authorization']);
}
// We need to unset the 'Host' header
// as we are redirecting to a new location.
unset($options['headers']['Host']);
$result = drupal_http_request($location, $options);
$result->redirect_code = $code;
}
if (!isset($result->redirect_url)) {
$result->redirect_url = $location;
}
break;
default:
$result->error = $result->status_message;
}
return $result;
}
/**
* Determine whether to strip sensitive headers from a request when redirected.
*
* @param string $url
* The url from the original outbound http request.
*
* @param string $location
* The location to which the request has been redirected.
*
* @return boolean
* Whether sensitive headers should be stripped from the request before
* following the redirect.
*/
function _drupal_should_strip_sensitive_headers_on_http_redirect($url, $location) {
$url_parsed = parse_url($url);
$location_parsed = parse_url($location);
if (!isset($location_parsed['host'])) {
return FALSE;
}
$strip_on_host_change = variable_get('drupal_http_request_strip_sensitive_headers_on_host_change', TRUE);
$strip_on_https_downgrade = variable_get('drupal_http_request_strip_sensitive_headers_on_https_downgrade', TRUE);
if ($strip_on_host_change && strcasecmp($url_parsed['host'], $location_parsed['host']) !== 0) {
return TRUE;
}
if ($strip_on_https_downgrade && $url_parsed['scheme'] !== $location_parsed['scheme'] && 'https' !== $location_parsed['scheme']) {
return TRUE;
}
return FALSE;
}
/**
* Splits an HTTP response status line into components.
*
* See the @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html status line definition @endlink
* in RFC 2616.
*
* @param string $respone
* The response status line, for example 'HTTP/1.1 500 Internal Server Error'.
*
* @return array
* Keyed array containing the component parts. If the response is malformed,
* all possible parts will be extracted. 'reason_phrase' could be empty.
* Possible keys:
* - 'http_version'
* - 'response_code'
* - 'reason_phrase'
*/
function _drupal_parse_response_status($response) {
$response_array = explode(' ', trim($response), 3);
// Set up empty values.
$result = array(
'reason_phrase' => '',
);
$result['http_version'] = $response_array[0];
$result['response_code'] = $response_array[1];
if (isset($response_array[2])) {
$result['reason_phrase'] = $response_array[2];
}
return $result;
}
/**
* 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);
}
/**
* @} End of "HTTP handling".
*/
/**
* Strips slashes from a string or array of strings.
*
* Callback for array_walk() within fix_gpx_magic().
*
* @param $item
* An individual string or array of strings from superglobals.
*/
function _fix_gpc_magic(&$item) {
if (is_array($item)) {
array_walk($item, '_fix_gpc_magic');
}
else {
$item = stripslashes($item);
}
}
/**
* Strips slashes from $_FILES items.
*
* Callback for array_walk() within fix_gpc_magic().
*
* The tmp_name key is skipped keys since PHP generates single backslashes for
* file paths on Windows systems.
*
* @param $item
* An item from $_FILES.
* @param $key
* The key for the item within $_FILES.
*
* @see http://php.net/manual/features.file-upload.php#42280
*/
function _fix_gpc_magic_files(&$item, $key) {
if ($key != 'tmp_name') {
if (is_array($item)) {
array_walk($item, '_fix_gpc_magic_files');
}
else {
$item = stripslashes($item);
}
}
}
/**
* Fixes double-escaping caused by "magic quotes" in some PHP installations.
*
* @see _fix_gpc_magic()
* @see _fix_gpc_magic_files()
*/
function fix_gpc_magic() {
static $fixed = FALSE;
if (!$fixed && ini_get('magic_quotes_gpc')) {
array_walk($_GET, '_fix_gpc_magic');
array_walk($_POST, '_fix_gpc_magic');
array_walk($_COOKIE, '_fix_gpc_magic');
array_walk($_REQUEST, '_fix_gpc_magic');
array_walk($_FILES, '_fix_gpc_magic_files');
}
$fixed = TRUE;
}
/**
* @defgroup validation Input validation
* @{
* Functions to validate user input.
*/
/**
* Verifies the syntax of the given e-mail address.
*
* This uses the
* @link http://php.net/manual/filter.filters.validate.php PHP e-mail validation filter. @endlink
* However, a module may override this by implementing
* hook_valid_email_address_alter(&$valid, $mail).
*
* @see hook_valid_email_address_alter()
*
* @param $mail
* A string containing an e-mail address.
*
* @return
* TRUE if the address is in a valid format.
*/
function valid_email_address($mail) {
$valid = (bool) filter_var($mail, FILTER_VALIDATE_EMAIL);
drupal_alter('valid_email_address', $valid, $mail);
return $valid;
}
/**
* Verifies the syntax of the given URL.
*
* This function should only be used on actual URLs. It should not be used for
* Drupal menu paths, which can contain arbitrary characters.
* Valid values per RFC 3986.
* @param $url
* The URL to verify.
* @param $absolute
* Whether the URL is absolute (beginning with a scheme such as "http:").
*
* @return
* TRUE if the URL is in a valid format.
*/
function valid_url($url, $absolute = FALSE) {
if ($absolute) {
return (bool)preg_match("
/^ # Start at the beginning of the text
(?:ftp|https?|feed):\/\/ # Look for ftp, http, https or feed schemes
(?: # Userinfo (optional) which is typically
(?:(?:[\w\.\-\+!$&'\(\)*\+,;=]|%[0-9a-f]{2})+:)* # a username or a username and password
(?:[\w\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})+@ # combination
)?
(?:
(?:[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)
(?:[\/|\?]
(?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2}) # The path and query (optional)
*)?
$/xi", $url);
}
else {
return (bool)preg_match("/^(?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})+$/i", $url);
}
}
/**
* @} End of "defgroup validation".
*/
/**
* Registers an event for the current visitor to the flood control mechanism.
*
* @param $name
* The name of an event.
* @param $window
* Optional number of seconds before this event expires. Defaults to 3600 (1
* hour). Typically uses the same value as the flood_is_allowed() $window
* parameter. Expired events are purged on cron run to prevent the flood table
* from growing indefinitely.
* @param $identifier
* Optional identifier (defaults to the current user's IP address).
*/
function flood_register_event($name, $window = 3600, $identifier = NULL) {
if (!isset($identifier)) {
$identifier = ip_address();
}
db_insert('flood')
->fields(array(
'event' => $name,
'identifier' => $identifier,
'timestamp' => REQUEST_TIME,
'expiration' => REQUEST_TIME + $window,
))
->execute();
}
/**
* Makes the flood control mechanism forget an event for the current visitor.
*
* @param $name
* The name of an event.
* @param $identifier
* Optional identifier (defaults to the current user's IP address).
*/
function flood_clear_event($name, $identifier = NULL) {
if (!isset($identifier)) {
$identifier = ip_address();
}
db_delete('flood')
->condition('event', $name)
->condition('identifier', $identifier)
->execute();
}
/**
* Checks whether a user is allowed to proceed with the specified event.
*
* Events can have thresholds saying that each user can only do that event
* a certain number of times in a time window. This function verifies that the
* current user has not exceeded this threshold.
*
* @param $name
* The unique name of the event.
* @param $threshold
* The maximum number of times each user can do this event per time window.
* @param $window
* Number of seconds in the time window for this event (default is 3600
* seconds, or 1 hour).
* @param $identifier
* Unique identifier of the current user. Defaults to their IP address.
*
* @return
* TRUE if the user is allowed to proceed. FALSE if they have exceeded the
* threshold and should not be allowed to proceed.
*/
function flood_is_allowed($name, $threshold, $window = 3600, $identifier = NULL) {
if (!isset($identifier)) {
$identifier = ip_address();
}
$number = db_query("SELECT COUNT(*) FROM {flood} WHERE event = :event AND identifier = :identifier AND timestamp > :timestamp", array(
':event' => $name,
':identifier' => $identifier,
':timestamp' => REQUEST_TIME - $window))
->fetchField();
return ($number < $threshold);
}
/**
* @defgroup sanitization Sanitization functions
* @{
* Functions to sanitize values.
*
* See http://drupal.org/writing-secure-code for information
* on writing secure code.
*/
/**
* Strips dangerous protocols (e.g. 'javascript:') from a URI.
*
* This function must be called for all URIs within user-entered input prior
* to being output to an HTML attribute value. It is often called as part of
* check_url() or filter_xss(), but those functions return an HTML-encoded
* string, so this function can be called independently when the output needs to
* be a plain-text string for passing to t(), l(), drupal_attributes(), or
* another function that will call check_plain() separately.
*
* @param $uri
* A plain-text URI that might contain dangerous protocols.
*
* @return
* A plain-text URI stripped of dangerous protocols. As with all plain-text
* strings, this return value must not be output to an HTML page without
* check_plain() being called on it. However, it can be passed to functions
* expecting plain-text strings.
*
* @see check_url()
*/
function drupal_strip_dangerous_protocols($uri) {
static $allowed_protocols;
if (!isset($allowed_protocols)) {
$allowed_protocols = array_flip(variable_get('filter_allowed_protocols', array('ftp', 'http', 'https', 'irc', 'mailto', 'news', 'nntp', 'rtsp', 'sftp', 'ssh', 'tel', 'telnet', 'webcal')));
}
// Iteratively remove any invalid protocol found.
do {
$before = $uri;
$colonpos = strpos($uri, ':');
if ($colonpos > 0) {
// We found a colon, possibly a protocol. Verify.
$protocol = substr($uri, 0, $colonpos);
// If a colon is preceded by a slash, question mark or hash, it cannot
// possibly be part of the URL scheme. This must be a relative URL, which
// inherits the (safe) protocol of the base document.
if (preg_match('![/?#]!', $protocol)) {
break;
}
// Check if this is a disallowed protocol. Per RFC2616, section 3.2.3
// (URI Comparison) scheme comparison must be case-insensitive.
if (!isset($allowed_protocols[strtolower($protocol)])) {
$uri = substr($uri, $colonpos + 1);
}
}
} while ($before != $uri);
return $uri;
}
/**
* Strips dangerous protocols from a URI and encodes it for output to HTML.
*
* @param $uri
* A plain-text URI that might contain dangerous protocols.
*
* @return
* A URI stripped of dangerous protocols and encoded for output to an HTML
* attribute value. Because it is already encoded, it should not be set as a
* value within a $attributes array passed to drupal_attributes(), because
* drupal_attributes() expects those values to be plain-text strings. To pass
* a filtered URI to drupal_attributes(), call
* drupal_strip_dangerous_protocols() instead.
*
* @see drupal_strip_dangerous_protocols()
*/
function check_url($uri) {
return check_plain(drupal_strip_dangerous_protocols($uri));
}
/**
* Applies a very permissive XSS/HTML filter for admin-only use.
*
* Use only for fields where it is impractical to use the
* whole filter system, but where some (mainly inline) mark-up
* is desired (so check_plain() is not acceptable).
*
* Allows all tags that can be used inside an HTML body, save
* for scripts and styles.
*/
function filter_xss_admin($string) {
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'));
}
/**
* Filters HTML to prevent cross-site-scripting (XSS) vulnerabilities.
*
* Based on kses by Ulf Harnhammar, see http://sourceforge.net/projects/kses.
* For examples of various XSS attacks, see: http://ha.ckers.org/xss.html.
*
* This code does four things:
* - Removes characters and constructs that can trick browsers.
* - Makes sure all HTML entities are well-formed.
* - Makes sure all HTML tags and attributes are well-formed.
* - Makes sure no HTML tags contain URLs with a disallowed protocol (e.g.
* javascript:).
*
* @param $string
* The string with raw HTML in it. It will be stripped of everything that can
* cause an XSS attack.
* @param $allowed_tags
* An array of allowed tags.
*
* @return
* An XSS safe version of $string, or an empty string if $string is not
* valid UTF-8.
*
* @see drupal_validate_utf8()
*/
function filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd')) {
// Only operate on valid UTF-8 strings. This is necessary to prevent cross
// site scripting issues on Internet Explorer 6.
if (!drupal_validate_utf8($string)) {
return '';
}
// Store the text format.
_filter_xss_split($allowed_tags, TRUE);
// Remove NULL characters (ignored by some browsers).
$string = str_replace(chr(0), '', (string) $string);
// Remove Netscape 4 JS entities.
$string = preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string);
// Defuse all HTML entities.
$string = str_replace('&', '&', $string);
// Change back only well-formed entities in our whitelist:
// Decimal numeric entities.
$string = preg_replace('/&#([0-9]+;)/', '\1', $string);
// Hexadecimal numeric entities.
$string = preg_replace('/&#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/', '\1', $string);
// Named entities.
$string = preg_replace('/&([A-Za-z][A-Za-z0-9]*;)/', '&\1', $string);
return preg_replace_callback('%
(
<(?=[^a-zA-Z!/]) # a lone <
| # or
# a comment
| # or
<[^>]*(>|$) # a string that starts with a <, up until the > or the end of the string
| # or
> # just a >
)%x', '_filter_xss_split', $string);
}
/**
* Processes an HTML tag.
*
* @param $m
* An array with various meaning depending on the value of $store.
* If $store is TRUE then the array contains the allowed tags.
* If $store is FALSE then the array has one element, the HTML tag to process.
* @param $store
* Whether to store $m.
*
* @return
* If the element isn't allowed, an empty string. Otherwise, the cleaned up
* version of the HTML element.
*/
function _filter_xss_split($m, $store = FALSE) {
static $allowed_html;
if ($store) {
$allowed_html = array_flip($m);
return;
}
$string = $m[1];
if (substr($string, 0, 1) != '<') {
// We matched a lone ">" character.
return '>';
}
elseif (strlen($string) == 1) {
// We matched a lone "<" character.
return '<';
}
if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9\-]+)\s*([^>]*)>?|()$%', $string, $matches)) {
// Seriously malformed.
return '';
}
$slash = trim($matches[1]);
$elem = &$matches[2];
$attrlist = &$matches[3];
$comment = &$matches[4];
if ($comment) {
$elem = '!--';
}
if (!isset($allowed_html[strtolower($elem)])) {
// Disallowed HTML element.
return '';
}
if ($comment) {
return $comment;
}
if ($slash != '') {
return "$elem>";
}
// Is there a closing XHTML slash at the end of the attributes?
$attrlist = preg_replace('%(\s?)/\s*$%', '\1', $attrlist, -1, $count);
$xhtml_slash = $count ? ' /' : '';
// Clean up attributes.
$attr2 = implode(' ', _filter_xss_attributes($attrlist));
$attr2 = preg_replace('/[<>]/', '', $attr2);
$attr2 = strlen($attr2) ? ' ' . $attr2 : '';
return "<$elem$attr2$xhtml_slash>";
}
/**
* Processes a string of HTML attributes.
*
* @return
* Cleaned up version of the HTML attributes.
*/
function _filter_xss_attributes($attr) {
$attrarr = array();
$mode = 0;
$attrname = '';
while (strlen($attr) != 0) {
// Was the last operation successful?
$working = 0;
switch ($mode) {
case 0:
// Attribute name, href for instance.
if (preg_match('/^([-a-zA-Z]+)/', $attr, $match)) {
$attrname = strtolower($match[1]);
$skip = (
$attrname == 'style' ||
substr($attrname, 0, 2) == 'on' ||
substr($attrname, 0, 1) == '-' ||
// Ignore long attributes to avoid unnecessary processing overhead.
strlen($attrname) > 96
);
$working = $mode = 1;
$attr = preg_replace('/^[-a-zA-Z]+/', '', $attr);
}
break;
case 1:
// Equals sign or valueless ("selected").
if (preg_match('/^\s*=\s*/', $attr)) {
$working = 1; $mode = 2;
$attr = preg_replace('/^\s*=\s*/', '', $attr);
break;
}
if (preg_match('/^\s+/', $attr)) {
$working = 1; $mode = 0;
if (!$skip) {
$attrarr[] = $attrname;
}
$attr = preg_replace('/^\s+/', '', $attr);
}
break;
case 2:
// Attribute value, a URL after href= for instance.
if (preg_match('/^"([^"]*)"(\s+|$)/', $attr, $match)) {
$thisval = filter_xss_bad_protocol($match[1]);
if (!$skip) {
$attrarr[] = "$attrname=\"$thisval\"";
}
$working = 1;
$mode = 0;
$attr = preg_replace('/^"[^"]*"(\s+|$)/', '', $attr);
break;
}
if (preg_match("/^'([^']*)'(\s+|$)/", $attr, $match)) {
$thisval = filter_xss_bad_protocol($match[1]);
if (!$skip) {
$attrarr[] = "$attrname='$thisval'";
}
$working = 1; $mode = 0;
$attr = preg_replace("/^'[^']*'(\s+|$)/", '', $attr);
break;
}
if (preg_match("%^([^\s\"']+)(\s+|$)%", $attr, $match)) {
$thisval = filter_xss_bad_protocol($match[1]);
if (!$skip) {
$attrarr[] = "$attrname=\"$thisval\"";
}
$working = 1; $mode = 0;
$attr = preg_replace("%^[^\s\"']+(\s+|$)%", '', $attr);
}
break;
}
if ($working == 0) {
// Not well formed; remove and try again.
$attr = preg_replace('/
^
(
"[^"]*("|$) # - a string that starts with a double quote, up until the next double quote or the end of the string
| # or
\'[^\']*(\'|$)| # - a string that starts with a quote, up until the next quote or the end of the string
| # or
\S # - a non-whitespace character
)* # any number of the above three
\s* # any number of whitespaces
/x', '', $attr);
$mode = 0;
}
}
// The attribute list ends with a valueless attribute like "selected".
if ($mode == 1 && !$skip) {
$attrarr[] = $attrname;
}
return $attrarr;
}
/**
* Processes an HTML attribute value and strips dangerous protocols from URLs.
*
* @param $string
* The string with the attribute value.
* @param $decode
* (deprecated) Whether to decode entities in the $string. Set to FALSE if the
* $string is in plain text, TRUE otherwise. Defaults to TRUE. This parameter
* is deprecated and will be removed in Drupal 8. To process a plain-text URI,
* call drupal_strip_dangerous_protocols() or check_url() instead.
*
* @return
* Cleaned up and HTML-escaped version of $string.
*/
function filter_xss_bad_protocol($string, $decode = TRUE) {
// Get the plain text representation of the attribute value (i.e. its meaning).
// @todo Remove the $decode parameter in Drupal 8, and always assume an HTML
// string that needs decoding.
if ($decode) {
if (!function_exists('decode_entities')) {
require_once DRUPAL_ROOT . '/includes/unicode.inc';
}
$string = decode_entities($string);
}
return check_plain(drupal_strip_dangerous_protocols($string));
}
/**
* @} End of "defgroup sanitization".
*/
/**
* @defgroup format Formatting
* @{
* Functions to format numbers, strings, dates, etc.
*/
/**
* Formats an RSS channel.
*
* Arbitrary elements may be added using the $args associative array.
*/
function format_rss_channel($title, $link, $description, $items, $langcode = NULL, $args = array()) {
global $language_content;
$langcode = $langcode ? $langcode : $language_content->language;
$output = "\n";
$output .= ' ' . check_plain($title) . " \n";
$output .= ' ' . check_url($link) . "\n";
// The RSS 2.0 "spec" doesn't indicate HTML can be used in the description.
// We strip all HTML tags, but need to prevent double encoding from properly
// escaped source data (such as & becoming &).
$output .= ' ' . check_plain(decode_entities(strip_tags($description))) . " \n";
$output .= ' ' . check_plain($langcode) . " \n";
$output .= format_xml_elements($args);
$output .= $items;
$output .= " \n";
return $output;
}
/**
* Formats a single RSS item.
*
* Arbitrary elements may be added using the $args associative array.
*/
function format_rss_item($title, $link, $description, $args = array()) {
$output = "- \n";
$output .= '
' . check_plain($title) . " \n";
$output .= ' ' . check_url($link) . "\n";
$output .= ' ' . check_plain($description) . " \n";
$output .= format_xml_elements($args);
$output .= " \n";
return $output;
}
/**
* Formats XML elements.
*
* @param $array
* An array where each item represents an element and is either a:
* - (key => value) pair (value )
* - Associative array with fields:
* - 'key': element name
* - 'value': element contents
* - 'attributes': associative array of element attributes
* - 'encoded': TRUE if 'value' is already encoded
*
* In both cases, 'value' can be a simple string, or it can be another array
* with the same format as $array itself for nesting.
*
* If 'encoded' is TRUE it is up to the caller to ensure that 'value' is either
* entity-encoded or CDATA-escaped. Using this option is not recommended when
* working with untrusted user input, since failing to escape the data
* correctly has security implications.
*/
function format_xml_elements($array) {
$output = '';
foreach ($array as $key => $value) {
if (is_numeric($key)) {
if ($value['key']) {
$output .= ' <' . $value['key'];
if (isset($value['attributes']) && is_array($value['attributes'])) {
$output .= drupal_attributes($value['attributes']);
}
if (isset($value['value']) && $value['value'] != '') {
$output .= '>' . (is_array($value['value']) ? format_xml_elements($value['value']) : (!empty($value['encoded']) ? $value['value'] : check_plain($value['value']))) . '' . $value['key'] . ">\n";
}
else {
$output .= " />\n";
}
}
}
else {
$output .= ' <' . $key . '>' . (is_array($value) ? format_xml_elements($value) : check_plain($value)) . "$key>\n";
}
}
return $output;
}
/**
* Formats a string containing a count of items.
*
* This function ensures that the string is pluralized correctly. Since t() is
* called by this function, make sure not to pass already-localized strings to
* it.
*
* For example:
* @code
* $output = format_plural($node->comment_count, '1 comment', '@count comments');
* @endcode
*
* Example with additional replacements:
* @code
* $output = format_plural($update_count,
* 'Changed the content type of 1 post from %old-type to %new-type.',
* 'Changed the content type of @count posts from %old-type to %new-type.',
* array('%old-type' => $info->old_type, '%new-type' => $info->new_type));
* @endcode
*
* @param $count
* The item count to display.
* @param $singular
* The string for the singular case. Make sure it is clear this is singular,
* to ease translation (e.g. use "1 new comment" instead of "1 new"). Do not
* use @count in the singular string.
* @param $plural
* The string for the plural case. Make sure it is clear this is plural, to
* ease translation. Use @count in place of the item count, as in
* "@count new comments".
* @param $args
* An associative array of replacements to make after translation. Instances
* of any key in this array are replaced with the corresponding value.
* Based on the first character of the key, the value is escaped and/or
* themed. See format_string(). Note that you do not need to include @count
* in this array; this replacement is done automatically for the plural case.
* @param $options
* An associative array of additional options. See t() for allowed keys.
*
* @return
* A translated string.
*
* @see t()
* @see format_string()
*/
function format_plural($count, $singular, $plural, array $args = array(), array $options = array()) {
$args['@count'] = $count;
if ($count == 1) {
return t($singular, $args, $options);
}
// Get the plural index through the gettext formula.
$index = (function_exists('locale_get_plural')) ? locale_get_plural($count, isset($options['langcode']) ? $options['langcode'] : NULL) : -1;
// If the index cannot be computed, use the plural as a fallback (which
// allows for most flexiblity with the replaceable @count value).
if ($index < 0) {
return t($plural, $args, $options);
}
else {
switch ($index) {
case "0":
return t($singular, $args, $options);
case "1":
return t($plural, $args, $options);
default:
unset($args['@count']);
$args['@count[' . $index . ']'] = $count;
return t(strtr($plural, array('@count' => '@count[' . $index . ']')), $args, $options);
}
}
}
/**
* Parses a given byte count.
*
* @param $size
* A size expressed as a number of bytes with optional SI or IEC binary unit
* prefix (e.g. 2, 3K, 5MB, 10G, 6GiB, 8 bytes, 9mbytes).
*
* @return
* An integer representation of the size in bytes.
*/
function parse_size($size) {
$unit = preg_replace('/[^bkmgtpezy]/i', '', $size); // Remove the non-unit characters from the size.
$size = (float) preg_replace('/[^0-9\.]/', '', $size); // Remove the non-numeric characters from the size.
if ($unit) {
// Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by.
return round($size * pow(DRUPAL_KILOBYTE, stripos('bkmgtpezy', $unit[0])));
}
else {
return round($size);
}
}
/**
* Generates a string representation for the given byte count.
*
* @param $size
* A size in bytes.
* @param $langcode
* Optional language code to translate to a language other than what is used
* to display the page.
*
* @return
* A translated string representation of the size.
*/
function format_size($size, $langcode = NULL) {
if ($size < DRUPAL_KILOBYTE) {
return format_plural($size, '1 byte', '@count bytes', array(), array('langcode' => $langcode));
}
else {
$size = $size / DRUPAL_KILOBYTE; // Convert bytes to kilobytes.
$units = array(
t('@size KB', array(), array('langcode' => $langcode)),
t('@size MB', array(), array('langcode' => $langcode)),
t('@size GB', array(), array('langcode' => $langcode)),
t('@size TB', array(), array('langcode' => $langcode)),
t('@size PB', array(), array('langcode' => $langcode)),
t('@size EB', array(), array('langcode' => $langcode)),
t('@size ZB', array(), array('langcode' => $langcode)),
t('@size YB', array(), array('langcode' => $langcode)),
);
foreach ($units as $unit) {
if (round($size, 2) >= DRUPAL_KILOBYTE) {
$size = $size / DRUPAL_KILOBYTE;
}
else {
break;
}
}
return str_replace('@size', round($size, 2), $unit);
}
}
/**
* Formats a time interval with the requested granularity.
*
* @param $interval
* The length of the interval in seconds.
* @param $granularity
* How many different units to display in the string.
* @param $langcode
* Optional language code to translate to a language other than
* what is used to display the page.
*
* @return
* A translated string representation of the interval.
*/
function format_interval($interval, $granularity = 2, $langcode = NULL) {
$units = array(
'1 year|@count years' => 31536000,
'1 month|@count months' => 2592000,
'1 week|@count weeks' => 604800,
'1 day|@count days' => 86400,
'1 hour|@count hours' => 3600,
'1 min|@count min' => 60,
'1 sec|@count sec' => 1
);
$output = '';
foreach ($units as $key => $value) {
$key = explode('|', $key);
if ($interval >= $value) {
$output .= ($output ? ' ' : '') . format_plural(floor($interval / $value), $key[0], $key[1], array(), array('langcode' => $langcode));
$interval = (int) $interval % $value;
$granularity--;
}
if ($granularity == 0) {
break;
}
}
return $output ? $output : t('0 sec', array(), array('langcode' => $langcode));
}
/**
* Formats a date, using a date type or a custom date format string.
*
* @param $timestamp
* A UNIX timestamp to format.
* @param $type
* (optional) The format to use, one of:
* - 'short', 'medium', or 'long' (the corresponding built-in date formats).
* - The name of a date type defined by a module in hook_date_format_types(),
* if it's been assigned a format.
* - The machine name of an administrator-defined date format.
* - 'custom', to use $format.
* Defaults to 'medium'.
* @param $format
* (optional) If $type is 'custom', a PHP date format string suitable for
* input to date(). Use a backslash to escape ordinary text, so it does not
* get interpreted as date format characters.
* @param $timezone
* (optional) Time zone identifier, as described at
* http://php.net/manual/timezones.php Defaults to the time zone used to
* display the page.
* @param $langcode
* (optional) Language code to translate to. Defaults to the language used to
* display the page.
*
* @return
* A translated date string in the requested format.
*/
function format_date($timestamp, $type = 'medium', $format = '', $timezone = NULL, $langcode = NULL) {
// Use the advanced drupal_static() pattern, since this is called very often.
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast['timezones'] = &drupal_static(__FUNCTION__);
}
$timezones = &$drupal_static_fast['timezones'];
if (!isset($timezone)) {
$timezone = date_default_timezone_get();
}
// Store DateTimeZone objects in an array rather than repeatedly
// constructing identical objects over the life of a request.
if (!isset($timezones[$timezone])) {
$timezones[$timezone] = timezone_open($timezone);
}
// Use the default langcode if none is set.
global $language;
if (empty($langcode)) {
$langcode = isset($language->language) ? $language->language : 'en';
}
switch ($type) {
case 'short':
$format = variable_get('date_format_short', 'm/d/Y - H:i');
break;
case 'long':
$format = variable_get('date_format_long', 'l, F j, Y - H:i');
break;
case 'custom':
// No change to format.
break;
case 'medium':
default:
// Retrieve the format of the custom $type passed.
if ($type != 'medium') {
$format = variable_get('date_format_' . $type, '');
}
// Fall back to 'medium'.
if ($format === '') {
$format = variable_get('date_format_medium', 'D, m/d/Y - H:i');
}
break;
}
// Create a DateTime object from the timestamp.
$date_time = date_create('@' . $timestamp);
// Set the time zone for the DateTime object.
date_timezone_set($date_time, $timezones[$timezone]);
// Encode markers that should be translated. 'A' becomes '\xEF\AA\xFF'.
// xEF and xFF are invalid UTF-8 sequences, and we assume they are not in the
// input string.
// Paired backslashes are isolated to prevent errors in read-ahead evaluation.
// The read-ahead expression ensures that A matches, but not \A.
$format = preg_replace(array('/\\\\\\\\/', '/(? $langcode,
);
if ($code == 'F') {
$options['context'] = 'Long month name';
}
if ($code == '') {
$cache[$langcode][$code][$string] = $string;
}
else {
$cache[$langcode][$code][$string] = t($string, array(), $options);
}
}
return $cache[$langcode][$code][$string];
}
/**
* Format a username.
*
* This is also the label callback implementation of
* callback_entity_info_label() for user_entity_info().
*
* By default, the passed-in object's 'name' property is used if it exists, or
* else, the site-defined value for the 'anonymous' variable. However, a module
* may override this by implementing hook_username_alter(&$name, $account).
*
* @see hook_username_alter()
*
* @param $account
* The account object for the user whose name is to be formatted.
*
* @return
* An unsanitized string with the username to display. The code receiving
* this result must ensure that check_plain() is called on it before it is
* printed to the page.
*/
function format_username($account) {
$name = !empty($account->name) ? $account->name : variable_get('anonymous', t('Anonymous'));
drupal_alter('username', $name, $account);
return $name;
}
/**
* @} End of "defgroup format".
*/
/**
* Generates an internal or external URL.
*
* When creating links in modules, consider whether l() could be a better
* alternative than url().
*
* @param $path
* (optional) The internal path or external URL being linked to, such as
* "node/34" or "http://example.com/foo". The default value is equivalent to
* passing in ''. A few notes:
* - If you provide a full URL, it will be considered an external URL.
* - If you provide only the path (e.g. "node/34"), it will be
* considered an internal link. In this case, it should be a system URL,
* and it will be replaced with the alias, if one exists. Additional query
* arguments for internal paths must be supplied in $options['query'], not
* included in $path.
* - If you provide an internal path and $options['alias'] is set to TRUE, the
* path is assumed already to be the correct path alias, and the alias is
* not looked up.
* - The special string '' generates a link to the site's base URL.
* - If your external URL contains a query (e.g. http://example.com/foo?a=b),
* then you can either URL encode the query keys and values yourself and
* include them in $path, or use $options['query'] to let this function
* URL encode them.
* @param $options
* (optional) An associative array of additional options, with the following
* elements:
* - 'query': An array of query key/value-pairs (without any URL-encoding) to
* append to the URL.
* - 'fragment': A fragment identifier (named anchor) to append to the URL.
* Do not include the leading '#' character.
* - 'absolute': Defaults to FALSE. Whether to force the output to be an
* absolute link (beginning with http:). Useful for links that will be
* displayed outside the site, such as in an RSS feed.
* - 'alias': Defaults to FALSE. Whether the given path is a URL alias
* already.
* - 'external': Whether the given path is an external URL.
* - 'language': An optional language object. If the path being linked to is
* internal to the site, $options['language'] is used to look up the alias
* for the URL. If $options['language'] is omitted, the global $language_url
* will be used.
* - 'https': Whether this URL should point to a secure location. If not
* defined, the current scheme is used, so the user stays on HTTP or HTTPS
* respectively. TRUE enforces HTTPS and FALSE enforces HTTP, but HTTPS can
* only be enforced when the variable 'https' is set to TRUE.
* - 'base_url': Only used internally, to modify the base URL when a language
* dependent URL requires so.
* - 'prefix': Only used internally, to modify the path when a language
* dependent URL requires so.
* - 'script': The script filename in Drupal's root directory to use when
* clean URLs are disabled, such as 'index.php'. Defaults to an empty
* string, as most modern web servers automatically find 'index.php'. If
* clean URLs are disabled, the value of $path is appended as query
* parameter 'q' to $options['script'] in the returned URL. When deploying
* Drupal on a web server that cannot be configured to automatically find
* index.php, then hook_url_outbound_alter() can be implemented to force
* this value to 'index.php'.
* - 'entity_type': The entity type of the object that called url(). Only
* set if url() is invoked by entity_uri().
* - 'entity': The entity object (such as a node) for which the URL is being
* generated. Only set if url() is invoked by entity_uri().
*
* @return
* A string containing a URL to the given path.
*/
function url($path = NULL, array $options = array()) {
// Merge in defaults.
$options += array(
'fragment' => '',
'query' => array(),
'absolute' => FALSE,
'alias' => FALSE,
'prefix' => ''
);
// Determine whether this is an external link, but ensure that the current
// path is always treated as internal by default (to prevent external link
// injection vulnerabilities).
if (!isset($options['external'])) {
$options['external'] = $path === $_GET['q'] ? FALSE : url_is_external($path);
}
// Preserve the original path before altering or aliasing.
$original_path = $path;
// Allow other modules to alter the outbound URL and options.
drupal_alter('url_outbound', $path, $options, $original_path);
if (isset($options['fragment']) && $options['fragment'] !== '') {
$options['fragment'] = '#' . $options['fragment'];
}
if ($options['external']) {
// Split off the fragment.
if (strpos($path, '#') !== FALSE) {
list($path, $old_fragment) = explode('#', $path, 2);
// If $options contains no fragment, take it over from the path.
if (isset($old_fragment) && !$options['fragment']) {
$options['fragment'] = '#' . $old_fragment;
}
}
// Append the query.
if ($options['query']) {
$path .= (strpos($path, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($options['query']);
}
if (isset($options['https']) && variable_get('https', FALSE)) {
if ($options['https'] === TRUE) {
$path = str_replace('http://', 'https://', $path);
}
elseif ($options['https'] === FALSE) {
$path = str_replace('https://', 'http://', $path);
}
}
// Reassemble.
return $path . $options['fragment'];
}
// Strip leading slashes from internal paths to prevent them becoming external
// URLs without protocol. /example.com should not be turned into
// //example.com.
$path = ltrim((string) $path, '/');
global $base_url, $base_secure_url, $base_insecure_url;
// The base_url might be rewritten from the language rewrite in domain mode.
if (!isset($options['base_url'])) {
if (isset($options['https']) && variable_get('https', FALSE)) {
if ($options['https'] === TRUE) {
$options['base_url'] = $base_secure_url;
$options['absolute'] = TRUE;
}
elseif ($options['https'] === FALSE) {
$options['base_url'] = $base_insecure_url;
$options['absolute'] = TRUE;
}
}
else {
$options['base_url'] = $base_url;
}
}
// The special path '' links to the default front page.
if ($path == '') {
$path = '';
}
elseif (!empty($path) && !$options['alias']) {
$language = isset($options['language']) && isset($options['language']->language) ? $options['language']->language : '';
require_once DRUPAL_ROOT . '/' . variable_get('path_inc', 'includes/path.inc');
$alias = drupal_get_path_alias($original_path, $language);
if ($alias != $original_path) {
// Strip leading slashes from internal path aliases to prevent them
// becoming external URLs without protocol. /example.com should not be
// turned into //example.com.
$path = ltrim($alias, '/');
}
}
$base = $options['absolute'] ? $options['base_url'] . '/' : base_path();
$prefix = empty($path) ? rtrim((string) $options['prefix'], '/') : $options['prefix'];
// With Clean URLs.
if (!empty($GLOBALS['conf']['clean_url'])) {
$path = drupal_encode_path($prefix . $path);
if ($options['query']) {
return $base . $path . '?' . drupal_http_build_query($options['query']) . $options['fragment'];
}
else {
return $base . $path . $options['fragment'];
}
}
// Without Clean URLs.
else {
$path = $prefix . $path;
$query = array();
if (!empty($path)) {
$query['q'] = $path;
}
if ($options['query']) {
// We do not use array_merge() here to prevent overriding $path via query
// parameters.
$query += $options['query'];
}
$query = $query ? ('?' . drupal_http_build_query($query)) : '';
$script = isset($options['script']) ? $options['script'] : '';
return $base . $script . $query . $options['fragment'];
}
}
/**
* Returns TRUE if a path is external to Drupal (e.g. http://example.com).
*
* If a path cannot be assessed by Drupal's menu handler, then we must
* treat it as potentially insecure.
*
* @param $path
* The internal path or external URL being linked to, such as "node/34" or
* "http://example.com/foo".
*
* @return
* Boolean TRUE or FALSE, where TRUE indicates an external path.
*/
function url_is_external($path) {
$path = (string) $path;
$colonpos = strpos($path, ':');
// Some browsers treat \ as / so normalize to forward slashes.
$path = str_replace('\\', '/', $path);
// If the path starts with 2 slashes then it is always considered an external
// URL without an explicit protocol part.
return (strpos($path, '//') === 0)
// Leading control characters may be ignored or mishandled by browsers, so
// assume such a path may lead to an external location. The \p{C} character
// class matches all UTF-8 control, unassigned, and private characters.
|| (preg_match('/^\p{C}/u', $path) !== 0)
// Avoid calling drupal_strip_dangerous_protocols() if there is any slash
// (/), hash (#) or question_mark (?) before the colon (:) occurrence - if
// any - as this would clearly mean it is not a URL.
|| ($colonpos !== FALSE
&& !preg_match('![/?#]!', substr($path, 0, $colonpos))
&& drupal_strip_dangerous_protocols($path) == $path);
}
/**
* Formats an attribute string for an HTTP header.
*
* @param $attributes
* An associative array of attributes such as 'rel'.
*
* @return
* A ; separated string ready for insertion in a HTTP header. No escaping is
* performed for HTML entities, so this string is not safe to be printed.
*
* @see drupal_add_http_header()
*/
function drupal_http_header_attributes(array $attributes = array()) {
foreach ($attributes as $attribute => &$data) {
if (is_array($data)) {
$data = implode(' ', $data);
}
$data = $attribute . '="' . $data . '"';
}
return $attributes ? ' ' . implode('; ', $attributes) : '';
}
/**
* Converts an associative array to an XML/HTML tag attribute string.
*
* Each array key and its value will be formatted into an attribute string.
* If a value is itself an array, then its elements are concatenated to a single
* space-delimited string (for example, a class attribute with multiple values).
*
* Attribute values are sanitized by running them through check_plain().
* Attribute names are not automatically sanitized. When using user-supplied
* attribute names, it is strongly recommended to allow only white-listed names,
* since certain attributes carry security risks and can be abused.
*
* Examples of security aspects when using drupal_attributes:
* @code
* // By running the value in the following statement through check_plain,
* // the malicious script is neutralized.
* drupal_attributes(array('title' => t('')));
*
* // The statement below demonstrates dangerous use of drupal_attributes, and
* // will return an onmouseout attribute with JavaScript code that, when used
* // as attribute in a tag, will cause users to be redirected to another site.
* //
* // In this case, the 'onmouseout' attribute should not be whitelisted --
* // you don't want users to have the ability to add this attribute or others
* // that take JavaScript commands.
* drupal_attributes(array('onmouseout' => 'window.location="http://malicious.com/";')));
* @endcode
*
* @param $attributes
* An associative array of key-value pairs to be converted to attributes.
*
* @return
* A string ready for insertion in a tag (starts with a space).
*
* @ingroup sanitization
*/
function drupal_attributes(array $attributes = array()) {
foreach ($attributes as $attribute => &$data) {
$data = implode(' ', (array) $data);
$data = $attribute . '="' . check_plain($data) . '"';
}
return $attributes ? ' ' . implode(' ', $attributes) : '';
}
/**
* Formats an internal or external URL link as an HTML anchor tag.
*
* This function correctly handles aliased paths and adds an 'active' class
* attribute to links that point to the current page (for theming), so all
* internal links output by modules should be generated by this function if
* possible.
*
* However, for links enclosed in translatable text you should use t() and
* embed the HTML anchor tag directly in the translated string. For example:
* @code
* t('Visit the settings page', array('@url' => url('admin')));
* @endcode
* This keeps the context of the link title ('settings' in the example) for
* translators.
*
* @param string $text
* The translated link text for the anchor tag.
* @param string $path
* The internal path or external URL being linked to, such as "node/34" or
* "http://example.com/foo". After the url() function is called to construct
* the URL from $path and $options, the resulting URL is passed through
* check_plain() before it is inserted into the HTML anchor tag, to ensure
* well-formed HTML. See url() for more information and notes.
* @param array $options
* An associative array of additional options. Defaults to an empty array. It
* may contain the following elements.
* - 'attributes': An associative array of HTML attributes to apply to the
* anchor tag. If element 'class' is included, it must be an array; 'title'
* must be a string; other elements are more flexible, as they just need
* to work in a call to drupal_attributes($options['attributes']).
* - 'html' (default FALSE): Whether $text is HTML or just plain-text. For
* example, to make an image tag into a link, this must be set to TRUE, or
* you will see the escaped HTML image tag. $text is not sanitized if
* 'html' is TRUE. The calling function must ensure that $text is already
* safe.
* - 'language': An optional language object. If the path being linked to is
* internal to the site, $options['language'] is used to determine whether
* the link is "active", or pointing to the current page (the language as
* well as the path must match). This element is also used by url().
* - Additional $options elements used by the url() function.
*
* @return string
* An HTML string containing a link to the given path.
*
* @see url()
*/
function l($text, $path, array $options = array()) {
global $language_url;
static $use_theme = NULL;
// Merge in defaults.
$options += array(
'attributes' => array(),
'html' => FALSE,
);
// Append active class.
if (($path == $_GET['q'] || ($path == '' && drupal_is_front_page())) &&
(empty($options['language']) || $options['language']->language == $language_url->language)) {
$options['attributes']['class'][] = 'active';
}
// Remove all HTML and PHP tags from a tooltip. For best performance, we act only
// if a quick strpos() pre-check gave a suspicion (because strip_tags() is expensive).
if (isset($options['attributes']['title']) && strpos($options['attributes']['title'], '<') !== FALSE) {
$options['attributes']['title'] = strip_tags($options['attributes']['title']);
}
// Determine if rendering of the link is to be done with a theme function
// or the inline default. Inline is faster, but if the theme system has been
// loaded and a module or theme implements a preprocess or process function
// or overrides the theme_link() function, then invoke theme(). Preliminary
// benchmarks indicate that invoking theme() can slow down the l() function
// by 20% or more, and that some of the link-heavy Drupal pages spend more
// than 10% of the total page request time in the l() function.
if (!isset($use_theme) && function_exists('theme')) {
// Allow edge cases to prevent theme initialization and force inline link
// rendering.
if (variable_get('theme_link', TRUE)) {
drupal_theme_initialize();
$registry = theme_get_registry(FALSE);
// We don't want to duplicate functionality that's in theme(), so any
// hint of a module or theme doing anything at all special with the 'link'
// theme hook should simply result in theme() being called. This includes
// the overriding of theme_link() with an alternate function or template,
// the presence of preprocess or process functions, or the presence of
// include files.
$use_theme = !isset($registry['link']['function']) || ($registry['link']['function'] != 'theme_link');
$use_theme = $use_theme || !empty($registry['link']['preprocess functions']) || !empty($registry['link']['process functions']) || !empty($registry['link']['includes']);
}
else {
$use_theme = FALSE;
}
}
$path = (string) $path;
// For backwards compatibility, do not strip a couple of specific javascript
// paths that are harmless.
// @see https://www.drupal.org/project/drupal/issues/3310081
$skip_js_paths = array('javascript:void()', 'javascript:void();', 'javascript:void(0)', 'javascript:void(0);');
if (!in_array(strtolower($path), $skip_js_paths)) {
$path = drupal_strip_dangerous_protocols($path);
}
if ($use_theme) {
return theme('link', array('text' => $text, 'path' => $path, 'options' => $options));
}
// The result of url() is a plain-text URL. Because we are using it here
// in an HTML argument context, we need to encode it properly.
return '' . ($options['html'] ? $text : check_plain($text)) . ' ';
}
/**
* Delivers a page callback result to the browser in the appropriate format.
*
* This function is most commonly called by menu_execute_active_handler(), but
* can also be called by error conditions such as drupal_not_found(),
* drupal_access_denied(), and drupal_site_offline().
*
* When a user requests a page, index.php calls menu_execute_active_handler(),
* which calls the 'page callback' function registered in hook_menu(). The page
* callback function can return one of:
* - NULL: to indicate no content.
* - An integer menu status constant: to indicate an error condition.
* - A string of HTML content.
* - A renderable array of content.
* Returning a renderable array rather than a string of HTML is preferred,
* because that provides modules with more flexibility in customizing the final
* result.
*
* When the page callback returns its constructed content to
* menu_execute_active_handler(), this function gets called. The purpose of
* this function is to determine the most appropriate 'delivery callback'
* function to route the content to. The delivery callback function then
* sends the content to the browser in the needed format. The default delivery
* callback is drupal_deliver_html_page(), which delivers the content as an HTML
* page, complete with blocks in addition to the content. This default can be
* overridden on a per menu router item basis by setting 'delivery callback' in
* hook_menu() or hook_menu_alter(), and can also be overridden on a per request
* basis in hook_page_delivery_callback_alter().
*
* For example, the same page callback function can be used for an HTML
* version of the page and an Ajax version of the page. The page callback
* function just needs to decide what content is to be returned and the
* delivery callback function will send it as an HTML page or an Ajax
* response, as appropriate.
*
* In order for page callbacks to be reusable in different delivery formats,
* they should not issue any "print" or "echo" statements, but instead just
* return content.
*
* Also note that this function does not perform access checks. The delivery
* callback function specified in hook_menu(), hook_menu_alter(), or
* hook_page_delivery_callback_alter() will be called even if the router item
* access checks fail. This is intentional (it is needed for JSON and other
* purposes), but it has security implications. Do not call this function
* directly unless you understand the security implications, and be careful in
* writing delivery callbacks, so that they do not violate security. See
* drupal_deliver_html_page() for an example of a delivery callback that
* respects security.
*
* @param $page_callback_result
* The result of a page callback. Can be one of:
* - NULL: to indicate no content.
* - An integer menu status constant: to indicate an error condition.
* - A string of HTML content.
* - A renderable array of content.
* @param $default_delivery_callback
* (Optional) If given, it is the name of a delivery function most likely
* to be appropriate for the page request as determined by the calling
* function (e.g., menu_execute_active_handler()). If not given, it is
* determined from the menu router information of the current page.
*
* @see menu_execute_active_handler()
* @see hook_menu()
* @see hook_menu_alter()
* @see hook_page_delivery_callback_alter()
*/
function drupal_deliver_page($page_callback_result, $default_delivery_callback = NULL) {
if (!isset($default_delivery_callback) && ($router_item = menu_get_item())) {
$default_delivery_callback = $router_item['delivery_callback'];
}
$delivery_callback = !empty($default_delivery_callback) ? $default_delivery_callback : 'drupal_deliver_html_page';
// Give modules a chance to alter the delivery callback used, based on
// request-time context (e.g., HTTP request headers).
drupal_alter('page_delivery_callback', $delivery_callback);
if (function_exists($delivery_callback)) {
$delivery_callback($page_callback_result);
}
else {
// If a delivery callback is specified, but doesn't exist as a function,
// something is wrong, but don't print anything, since it's not known
// what format the response needs to be in.
watchdog('delivery callback not found', 'callback %callback not found: %q.', array('%callback' => $delivery_callback, '%q' => $_GET['q']), WATCHDOG_ERROR);
}
}
/**
* Packages and sends the result of a page callback to the browser as HTML.
*
* @param $page_callback_result
* The result of a page callback. Can be one of:
* - NULL: to indicate no content.
* - An integer menu status constant: to indicate an error condition.
* - A string of HTML content.
* - A renderable array of content.
*
* @see drupal_deliver_page()
*/
function drupal_deliver_html_page($page_callback_result) {
// Emit the correct charset HTTP header, but not if the page callback
// result is NULL, since that likely indicates that it printed something
// in which case, no further headers may be sent, and not if code running
// for this page request has already set the content type header.
if (isset($page_callback_result) && is_null(drupal_get_http_header('Content-Type'))) {
drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
}
// Send appropriate HTTP-Header for browsers and search engines.
global $language;
drupal_add_http_header('Content-Language', $language->language);
// By default, do not allow the site to be rendered in an iframe on another
// domain, but provide a variable to override this. If the code running for
// this page request already set the X-Frame-Options header earlier, don't
// overwrite it here.
$frame_options = variable_get('x_frame_options', 'SAMEORIGIN');
if ($frame_options && is_null(drupal_get_http_header('X-Frame-Options'))) {
drupal_add_http_header('X-Frame-Options', $frame_options);
}
drupal_add_http_header('X-Content-Type-Options', 'nosniff');
// Menu status constants are integers; page content is a string or array.
if (is_int($page_callback_result)) {
// @todo: Break these up into separate functions?
switch ($page_callback_result) {
case MENU_NOT_FOUND:
// Print a 404 page.
drupal_add_http_header('Status', '404 Not Found');
watchdog('page not found', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);
// Check for and return a fast 404 page if configured.
drupal_fast_404();
// Keep old path for reference, and to allow forms to redirect to it.
if (!isset($_GET['destination'])) {
// Make sure that the current path is not interpreted as external URL.
if (!url_is_external($_GET['q'])) {
$_GET['destination'] = $_GET['q'];
}
}
$path = drupal_get_normal_path(variable_get('site_404', ''));
if ($path && $path != $_GET['q']) {
// Custom 404 handler. Set the active item in case there are tabs to
// display, or other dependencies on the path.
menu_set_active_item($path);
$return = menu_execute_active_handler($path, FALSE);
}
if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {
// Standard 404 handler.
drupal_set_title(t('Page not found'));
$return = t('The requested page "@path" could not be found.', array('@path' => request_uri()));
}
drupal_set_page_content($return);
$page = element_info('page');
print drupal_render_page($page);
break;
case MENU_ACCESS_DENIED:
// Print a 403 page.
drupal_add_http_header('Status', '403 Forbidden');
watchdog('access denied', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);
// Keep old path for reference, and to allow forms to redirect to it.
if (!isset($_GET['destination'])) {
// Make sure that the current path is not interpreted as external URL.
if (!url_is_external($_GET['q'])) {
$_GET['destination'] = $_GET['q'];
}
}
$path = drupal_get_normal_path(variable_get('site_403', ''));
if ($path && $path != $_GET['q']) {
// Custom 403 handler. Set the active item in case there are tabs to
// display or other dependencies on the path.
menu_set_active_item($path);
$return = menu_execute_active_handler($path, FALSE);
}
if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {
// Standard 403 handler.
drupal_set_title(t('Access denied'));
$return = t('You are not authorized to access this page.');
}
print drupal_render_page($return);
break;
case MENU_SITE_OFFLINE:
// Print a 503 page.
drupal_maintenance_theme();
drupal_add_http_header('Status', '503 Service unavailable');
drupal_set_title(t('Site under maintenance'));
print theme('maintenance_page', array('content' => filter_xss_admin(variable_get('maintenance_mode_message',
t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal')))))));
break;
}
}
elseif (isset($page_callback_result)) {
// Print anything besides a menu constant, assuming it's not NULL or
// undefined.
print drupal_render_page($page_callback_result);
}
// Perform end-of-request tasks.
drupal_page_footer();
}
/**
* Performs end-of-request tasks.
*
* This function sets the page cache if appropriate, and allows modules to
* react to the closing of the page by calling hook_exit().
*/
function drupal_page_footer() {
global $user;
module_invoke_all('exit');
// Commit the user session, if needed.
drupal_session_commit();
if (variable_get('cache', 0) && ($cache = drupal_page_set_cache())) {
drupal_serve_page_from_cache($cache);
}
else {
ob_flush();
}
_registry_check_code(REGISTRY_WRITE_LOOKUP_CACHE);
drupal_cache_system_paths();
module_implements_write_cache();
drupal_file_scan_write_cache();
system_run_automated_cron();
}
/**
* Performs end-of-request tasks.
*
* In some cases page requests need to end without calling drupal_page_footer().
* In these cases, call drupal_exit() instead. There should rarely be a reason
* to call exit instead of drupal_exit();
*
* @param $destination
* If this function is called from drupal_goto(), then this argument
* will be a fully-qualified URL that is the destination of the redirect.
* This should be passed along to hook_exit() implementations.
*/
function drupal_exit($destination = NULL) {
if (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL) {
if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
module_invoke_all('exit', $destination);
}
drupal_session_commit();
}
exit;
}
/**
* Forms an associative array from a linear array.
*
* This function walks through the provided array and constructs an associative
* array out of it. The keys of the resulting array will be the values of the
* input array. The values will be the same as the keys unless a function is
* specified, in which case the output of the function is used for the values
* instead.
*
* @param $array
* A linear array.
* @param $function
* A name of a function to apply to all values before output.
*
* @return
* An associative array.
*/
function drupal_map_assoc($array, $function = NULL) {
// array_combine() fails with empty arrays:
// http://bugs.php.net/bug.php?id=34857.
$array = !empty($array) ? array_combine($array, $array) : array();
if (is_callable($function)) {
$array = array_map($function, $array);
}
return $array;
}
/**
* Attempts to set the PHP maximum execution time.
*
* This function is a wrapper around the PHP function set_time_limit().
* When called, set_time_limit() restarts the timeout counter from zero.
* In other words, if the timeout is the default 30 seconds, and 25 seconds
* into script execution a call such as set_time_limit(20) is made, the
* script will run for a total of 45 seconds before timing out.
*
* If the current time limit is not unlimited it is possible to decrease the
* total time limit if the sum of the new time limit and the current time spent
* running the script is inferior to the original time limit. It is inherent to
* the way set_time_limit() works, it should rather be called with an
* appropriate value every time you need to allocate a certain amount of time
* to execute a task than only once at the beginning of the script.
*
* Before calling set_time_limit(), we check if this function is available
* because it could be disabled by the server administrator. We also hide all
* the errors that could occur when calling set_time_limit(), because it is
* not possible to reliably ensure that PHP or a security extension will
* not issue a warning/error if they prevent the use of this function.
*
* @param $time_limit
* An integer specifying the new time limit, in seconds. A value of 0
* indicates unlimited execution time.
*
* @ingroup php_wrappers
*/
function drupal_set_time_limit($time_limit) {
if (function_exists('set_time_limit')) {
$current = ini_get('max_execution_time');
// Do not set time limit if it is currently unlimited.
if ($current != 0) {
@set_time_limit($time_limit);
}
}
}
/**
* Returns the path to a system item (module, theme, etc.).
*
* @param $type
* The type of the item (i.e. theme, theme_engine, module, profile).
* @param $name
* The name of the item for which the path is requested.
*
* @return
* The path to the requested item or an empty string if the item is not found.
*/
function drupal_get_path($type, $name) {
return dirname(drupal_get_filename($type, $name));
}
/**
* Returns the base URL path (i.e., directory) of the Drupal installation.
*
* base_path() adds a "/" to the beginning and end of the returned path if the
* path is not empty. At the very least, this will return "/".
*
* Examples:
* - http://example.com returns "/" because the path is empty.
* - http://example.com/drupal/folder returns "/drupal/folder/".
*/
function base_path() {
return $GLOBALS['base_path'];
}
/**
* Adds a LINK tag with distinct attributes to the page's HEAD.
*
* This function can be called as long the HTML header hasn't been sent, which
* on normal pages is up through the preprocess step of theme('html'). Adding
* a link will overwrite a prior link with the exact same 'rel', 'href' and
* 'hreflang' attributes.
*
* @param $attributes
* Associative array of element attributes including 'href' and 'rel'.
* @param $header
* Optional flag to determine if a HTTP 'Link:' header should be sent.
*/
function drupal_add_html_head_link($attributes, $header = FALSE) {
$element = array(
'#tag' => 'link',
'#attributes' => $attributes,
);
$href = $attributes['href'];
if ($header) {
// Also add a HTTP header "Link:".
$href = '<' . $attributes['href'] . '>;';
unset($attributes['href']);
$element['#attached']['drupal_add_http_header'][] = array('Link', $href . drupal_http_header_attributes($attributes), TRUE);
}
drupal_add_html_head($element, 'drupal_add_html_head_link:' . $attributes['rel'] . ':' . (isset($attributes['hreflang']) ? "{$attributes['hreflang']}:" : '') . $href);
}
/**
* Adds a cascading stylesheet to the stylesheet queue.
*
* Calling drupal_static_reset('drupal_add_css') will clear all cascading
* stylesheets added so far.
*
* If CSS aggregation/compression is enabled, all cascading style sheets added
* with $options['preprocess'] set to TRUE will be merged into one aggregate
* file and compressed by removing all extraneous white space.
* Preprocessed inline stylesheets will not be aggregated into this single file;
* instead, they are just compressed upon output on the page. Externally hosted
* stylesheets are never aggregated or compressed.
*
* The reason for aggregating the files is outlined quite thoroughly here:
* http://www.die.net/musings/page_load_time/ "Load fewer external objects. Due
* to request overhead, one bigger file just loads faster than two smaller ones
* half its size."
*
* $options['preprocess'] should be only set to TRUE when a file is required for
* all typical visitors and most pages of a site. It is critical that all
* preprocessed files are added unconditionally on every page, even if the
* files do not happen to be needed on a page. This is normally done by calling
* drupal_add_css() in a hook_init() implementation.
*
* Non-preprocessed files should only be added to the page when they are
* actually needed.
*
* @param $data
* (optional) The stylesheet data to be added, depending on what is passed
* through to the $options['type'] parameter:
* - 'file': The path to the CSS file relative to the base_path(), or a
* stream wrapper URI. For example: "modules/devel/devel.css" or
* "public://generated_css/stylesheet_1.css". Note that Modules should
* always prefix the names of their CSS files with the module name; for
* example, system-menus.css rather than simply menus.css. Themes can
* override module-supplied CSS files based on their filenames, and this
* prefixing helps prevent confusing name collisions for theme developers.
* See drupal_get_css() where the overrides are performed. Also, if the
* direction of the current language is right-to-left (Hebrew, Arabic,
* etc.), the function will also look for an RTL CSS file and append it to
* the list. The name of this file should have an '-rtl.css' suffix. For
* example, a CSS file called 'mymodule-name.css' will have a
* 'mymodule-name-rtl.css' file added to the list, if exists in the same
* directory. This CSS file should contain overrides for properties which
* should be reversed or otherwise different in a right-to-left display.
* - 'inline': A string of CSS that should be placed in the given scope. Note
* that it is better practice to use 'file' stylesheets, rather than
* 'inline', as the CSS would then be aggregated and cached.
* - 'external': The absolute path to an external CSS file that is not hosted
* on the local server. These files will not be aggregated if CSS
* aggregation is enabled.
* @param $options
* (optional) A string defining the 'type' of CSS that is being added in the
* $data parameter ('file', 'inline', or 'external'), or an array which can
* have any or all of the following keys:
* - 'type': The type of stylesheet being added. Available options are 'file',
* 'inline' or 'external'. Defaults to 'file'.
* - 'basename': Force a basename for the file being added. Modules are
* expected to use stylesheets with unique filenames, but integration of
* external libraries may make this impossible. The basename of
* 'modules/node/node.css' is 'node.css'. If the external library "node.js"
* ships with a 'node.css', then a different, unique basename would be
* 'node.js.css'.
* - 'group': A number identifying the group in which to add the stylesheet.
* Available constants are:
* - CSS_SYSTEM: Any system-layer CSS.
* - CSS_DEFAULT: (default) Any module-layer CSS.
* - CSS_THEME: Any theme-layer CSS.
* The group number serves as a weight: the markup for loading a stylesheet
* within a lower weight group is output to the page before the markup for
* loading a stylesheet within a higher weight group, so CSS within higher
* weight groups take precendence over CSS within lower weight groups.
* - 'every_page': For optimal front-end performance when aggregation is
* enabled, this should be set to TRUE if the stylesheet is present on every
* page of the website for users for whom it is present at all. This
* defaults to FALSE. It is set to TRUE for stylesheets added via module and
* theme .info files. Modules that add stylesheets within hook_init()
* implementations, or from other code that ensures that the stylesheet is
* added to all website pages, should also set this flag to TRUE. All
* stylesheets within the same group that have the 'every_page' flag set to
* TRUE and do not have 'preprocess' set to FALSE are aggregated together
* into a single aggregate file, and that aggregate file can be reused
* across a user's entire site visit, leading to faster navigation between
* pages. However, stylesheets that are only needed on pages less frequently
* visited, can be added by code that only runs for those particular pages,
* and that code should not set the 'every_page' flag. This minimizes the
* size of the aggregate file that the user needs to download when first
* visiting the website. Stylesheets without the 'every_page' flag are
* aggregated into a separate aggregate file. This other aggregate file is
* likely to change from page to page, and each new aggregate file needs to
* be downloaded when first encountered, so it should be kept relatively
* small by ensuring that most commonly needed stylesheets are added to
* every page.
* - 'weight': The weight of the stylesheet specifies the order in which the
* CSS will appear relative to other stylesheets with the same group and
* 'every_page' flag. The exact ordering of stylesheets is as follows:
* - First by group.
* - Then by the 'every_page' flag, with TRUE coming before FALSE.
* - Then by weight.
* - Then by the order in which the CSS was added. For example, all else
* being the same, a stylesheet added by a call to drupal_add_css() that
* happened later in the page request gets added to the page after one for
* which drupal_add_css() happened earlier in the page request.
* - 'media': The media type for the stylesheet, e.g., all, print, screen.
* Defaults to 'all'.
* - 'preprocess': If TRUE and CSS aggregation/compression is enabled, the
* styles will be aggregated and compressed. Defaults to TRUE.
* - 'browsers': An array containing information specifying which browsers
* should load the CSS item. See drupal_pre_render_conditional_comments()
* for details.
*
* @return
* An array of queued cascading stylesheets.
*
* @see drupal_get_css()
*/
function drupal_add_css($data = NULL, $options = NULL) {
$css = &drupal_static(__FUNCTION__, array());
$count = &drupal_static(__FUNCTION__ . '_count', 0);
// If the $css variable has been reset with drupal_static_reset(), there is
// no longer any CSS being tracked, so set the counter back to 0 also.
if (count($css) === 0) {
$count = 0;
}
// Construct the options, taking the defaults into consideration.
if (isset($options)) {
if (!is_array($options)) {
$options = array('type' => $options);
}
}
else {
$options = array();
}
// Create an array of CSS files for each media type first, since each type needs to be served
// to the browser differently.
if (isset($data)) {
$options += array(
'type' => 'file',
'group' => CSS_DEFAULT,
'weight' => 0,
'every_page' => FALSE,
'media' => 'all',
'preprocess' => TRUE,
'data' => $data,
'browsers' => array(),
);
$options['browsers'] += array(
'IE' => TRUE,
'!IE' => TRUE,
);
// Files with a query string cannot be preprocessed.
if ($options['type'] === 'file' && $options['preprocess'] && strpos($options['data'], '?') !== FALSE) {
$options['preprocess'] = FALSE;
}
// Always add a tiny value to the weight, to conserve the insertion order.
$options['weight'] += $count / 1000;
$count++;
// Add the data to the CSS array depending on the type.
switch ($options['type']) {
case 'inline':
// For inline stylesheets, we don't want to use the $data as the array
// key as $data could be a very long string of CSS.
$css[] = $options;
break;
default:
// Local and external files must keep their name as the associative key
// so the same CSS file is not be added twice.
$css[$data] = $options;
}
}
return $css;
}
/**
* Returns a themed representation of all stylesheets to attach to the page.
*
* It loads the CSS in order, with 'module' first, then 'theme' afterwards.
* This ensures proper cascading of styles so themes can easily override
* module styles through CSS selectors.
*
* Themes may replace module-defined CSS files by adding a stylesheet with the
* same filename. For example, themes/bartik/system-menus.css would replace
* modules/system/system-menus.css. This allows themes to override complete
* CSS files, rather than specific selectors, when necessary.
*
* If the original CSS file is being overridden by a theme, the theme is
* responsible for supplying an accompanying RTL CSS file to replace the
* module's.
*
* @param $css
* (optional) An array of CSS files. If no array is provided, the default
* stylesheets array is used instead.
* @param $skip_alter
* (optional) If set to TRUE, this function skips calling drupal_alter() on
* $css, useful when the calling function passes a $css array that has already
* been altered.
*
* @return
* A string of XHTML CSS tags.
*
* @see drupal_add_css()
*/
function drupal_get_css($css = NULL, $skip_alter = FALSE) {
if (!isset($css)) {
$css = drupal_add_css();
}
// Allow modules and themes to alter the CSS items.
if (!$skip_alter) {
drupal_alter('css', $css);
}
// Sort CSS items, so that they appear in the correct order.
uasort($css, 'drupal_sort_css_js');
// Provide the page with information about the individual CSS files used,
// information not otherwise available when CSS aggregation is enabled. The
// setting is attached later in this function, but is set here, so that CSS
// files removed below are still considered "used" and prevented from being
// added in a later AJAX request.
// Skip if no files were added to the page or jQuery.extend() will overwrite
// the Drupal.settings.ajaxPageState.css object with an empty array.
if (!empty($css)) {
// Cast the array to an object to be on the safe side even if not empty.
$setting['ajaxPageState']['css'] = (object) array_fill_keys(array_keys($css), 1);
}
// Remove the overridden CSS files. Later CSS files override former ones.
$previous_item = array();
foreach ($css as $key => $item) {
if ($item['type'] == 'file') {
// If defined, force a unique basename for this file.
$basename = isset($item['basename']) ? $item['basename'] : drupal_basename($item['data']);
if (isset($previous_item[$basename])) {
// Remove the previous item that shared the same base name.
unset($css[$previous_item[$basename]]);
}
$previous_item[$basename] = $key;
}
}
// Render the HTML needed to load the CSS.
$styles = array(
'#type' => 'styles',
'#items' => $css,
);
if (!empty($setting)) {
$styles['#attached']['js'][] = array('type' => 'setting', 'data' => $setting);
}
return drupal_render($styles);
}
/**
* Sorts CSS and JavaScript resources.
*
* Callback for uasort() within:
* - drupal_get_css()
* - drupal_get_js()
*
* This sort order helps optimize front-end performance while providing modules
* and themes with the necessary control for ordering the CSS and JavaScript
* appearing on a page.
*
* @param $a
* First item for comparison. The compared items should be associative arrays
* of member items from drupal_add_css() or drupal_add_js().
* @param $b
* Second item for comparison.
*
* @see drupal_add_css()
* @see drupal_add_js()
*/
function drupal_sort_css_js($a, $b) {
// First order by group, so that, for example, all items in the CSS_SYSTEM
// group appear before items in the CSS_DEFAULT group, which appear before
// all items in the CSS_THEME group. Modules may create additional groups by
// defining their own constants.
if ($a['group'] < $b['group']) {
return -1;
}
elseif ($a['group'] > $b['group']) {
return 1;
}
// Within a group, order all infrequently needed, page-specific files after
// common files needed throughout the website. Separating this way allows for
// the aggregate file generated for all of the common files to be reused
// across a site visit without being cut by a page using a less common file.
elseif ($a['every_page'] && !$b['every_page']) {
return -1;
}
elseif (!$a['every_page'] && $b['every_page']) {
return 1;
}
// Finally, order by weight.
elseif ($a['weight'] < $b['weight']) {
return -1;
}
elseif ($a['weight'] > $b['weight']) {
return 1;
}
else {
return 0;
}
}
/**
* Default callback to group CSS items.
*
* This function arranges the CSS items that are in the #items property of the
* styles element into groups. Arranging the CSS items into groups serves two
* purposes. When aggregation is enabled, files within a group are aggregated
* into a single file, significantly improving page loading performance by
* minimizing network traffic overhead. When aggregation is disabled, grouping
* allows multiple files to be loaded from a single STYLE tag, enabling sites
* with many modules enabled or a complex theme being used to stay within IE's
* 31 CSS inclusion tag limit: http://drupal.org/node/228818.
*
* This function puts multiple items into the same group if they are groupable
* and if they are for the same 'media' and 'browsers'. Items of the 'file' type
* are groupable if their 'preprocess' flag is TRUE, items of the 'inline' type
* are always groupable, and items of the 'external' type are never groupable.
* This function also ensures that the process of grouping items does not change
* their relative order. This requirement may result in multiple groups for the
* same type, media, and browsers, if needed to accommodate other items in
* between.
*
* @param $css
* An array of CSS items, as returned by drupal_add_css(), but after
* alteration performed by drupal_get_css().
*
* @return
* An array of CSS groups. Each group contains the same keys (e.g., 'media',
* 'data', etc.) as a CSS item from the $css parameter, with the value of
* each key applying to the group as a whole. Each group also contains an
* 'items' key, which is the subset of items from $css that are in the group.
*
* @see drupal_pre_render_styles()
* @see system_element_info()
*/
function drupal_group_css($css) {
$groups = array();
// If a group can contain multiple items, we track the information that must
// be the same for each item in the group, so that when we iterate the next
// item, we can determine if it can be put into the current group, or if a
// new group needs to be made for it.
$current_group_keys = NULL;
// When creating a new group, we pre-increment $i, so by initializing it to
// -1, the first group will have index 0.
$i = -1;
foreach ($css as $item) {
// The browsers for which the CSS item needs to be loaded is part of the
// information that determines when a new group is needed, but the order of
// keys in the array doesn't matter, and we don't want a new group if all
// that's different is that order.
ksort($item['browsers']);
// If the item can be grouped with other items, set $group_keys to an array
// of information that must be the same for all items in its group. If the
// item can't be grouped with other items, set $group_keys to FALSE. We
// put items into a group that can be aggregated together: whether they will
// be aggregated is up to the _drupal_css_aggregate() function or an
// override of that function specified in hook_css_alter(), but regardless
// of the details of that function, a group represents items that can be
// aggregated. Since a group may be rendered with a single HTML tag, all
// items in the group must share the same information that would need to be
// part of that HTML tag.
switch ($item['type']) {
case 'file':
// Group file items if their 'preprocess' flag is TRUE.
// Help ensure maximum reuse of aggregate files by only grouping
// together items that share the same 'group' value and 'every_page'
// flag. See drupal_add_css() for details about that.
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['every_page'], $item['media'], $item['browsers']) : FALSE;
break;
case 'inline':
// Always group inline items.
$group_keys = array($item['type'], $item['media'], $item['browsers']);
break;
case 'external':
// Do not group external items.
$group_keys = FALSE;
break;
}
// If the group keys don't match the most recent group we're working with,
// then a new group must be made.
if ($group_keys !== $current_group_keys) {
$i++;
// Initialize the new group with the same properties as the first item
// being placed into it. The item's 'data' and 'weight' properties are
// unique to the item and should not be carried over to the group.
$groups[$i] = $item;
unset($groups[$i]['data'], $groups[$i]['weight']);
$groups[$i]['items'] = array();
$current_group_keys = $group_keys ? $group_keys : NULL;
}
// Add the item to the current group.
$groups[$i]['items'][] = $item;
}
return $groups;
}
/**
* Default callback to aggregate CSS files and inline content.
*
* Having the browser load fewer CSS files results in much faster page loads
* than when it loads many small files. This function aggregates files within
* the same group into a single file unless the site-wide setting to do so is
* disabled (commonly the case during site development). To optimize download,
* it also compresses the aggregate files by removing comments, whitespace, and
* other unnecessary content. Additionally, this functions aggregates inline
* content together, regardless of the site-wide aggregation setting.
*
* @param $css_groups
* An array of CSS groups as returned by drupal_group_css(). This function
* modifies the group's 'data' property for each group that is aggregated.
*
* @see drupal_group_css()
* @see drupal_pre_render_styles()
* @see system_element_info()
*/
function drupal_aggregate_css(&$css_groups) {
$preprocess_css = (variable_get('preprocess_css', FALSE) && !defined('MAINTENANCE_MODE'));
// For each group that needs aggregation, aggregate its items.
foreach ($css_groups as $key => $group) {
switch ($group['type']) {
// If a file group can be aggregated into a single file, do so, and set
// the group's data property to the file path of the aggregate file.
case 'file':
if ($group['preprocess'] && $preprocess_css) {
$css_groups[$key]['data'] = drupal_build_css_cache($group['items']);
}
break;
// Aggregate all inline CSS content into the group's data property.
case 'inline':
$css_groups[$key]['data'] = '';
foreach ($group['items'] as $item) {
$css_groups[$key]['data'] .= drupal_load_stylesheet_content($item['data'], $item['preprocess']);
}
break;
}
}
}
/**
* #pre_render callback to add the elements needed for CSS tags to be rendered.
*
* For production websites, LINK tags are preferable to STYLE tags with @import
* statements, because:
* - They are the standard tag intended for linking to a resource.
* - On Firefox 2 and perhaps other browsers, CSS files included with @import
* statements don't get saved when saving the complete web page for offline
* use: http://drupal.org/node/145218.
* - On IE, if only LINK tags and no @import statements are used, all the CSS
* files are downloaded in parallel, resulting in faster page load, but if
* @import statements are used and span across multiple STYLE tags, all the
* ones from one STYLE tag must be downloaded before downloading begins for
* the next STYLE tag. Furthermore, IE7 does not support media declaration on
* the @import statement, so multiple STYLE tags must be used when different
* files are for different media types. Non-IE browsers always download in
* parallel, so this is an IE-specific performance quirk:
* http://www.stevesouders.com/blog/2009/04/09/dont-use-import/.
*
* However, IE has an annoying limit of 31 total CSS inclusion tags
* (http://drupal.org/node/228818) and LINK tags are limited to one file per
* tag, whereas STYLE tags can contain multiple @import statements allowing
* multiple files to be loaded per tag. When CSS aggregation is disabled, a
* Drupal site can easily have more than 31 CSS files that need to be loaded, so
* using LINK tags exclusively would result in a site that would display
* incorrectly in IE. Depending on different needs, different strategies can be
* employed to decide when to use LINK tags and when to use STYLE tags.
*
* The strategy employed by this function is to use LINK tags for all aggregate
* files and for all files that cannot be aggregated (e.g., if 'preprocess' is
* set to FALSE or the type is 'external'), and to use STYLE tags for groups
* of files that could be aggregated together but aren't (e.g., if the site-wide
* aggregation setting is disabled). This results in all LINK tags when
* aggregation is enabled, a guarantee that as many or only slightly more tags
* are used with aggregation disabled than enabled (so that if the limit were to
* be crossed with aggregation enabled, the site developer would also notice the
* problem while aggregation is disabled), and an easy way for a developer to
* view HTML source while aggregation is disabled and know what files will be
* aggregated together when aggregation becomes enabled.
*
* This function evaluates the aggregation enabled/disabled condition on a group
* by group basis by testing whether an aggregate file has been made for the
* group rather than by testing the site-wide aggregation setting. This allows
* this function to work correctly even if modules have implemented custom
* logic for grouping and aggregating files.
*
* @param $element
* A render array containing:
* - '#items': The CSS items as returned by drupal_add_css() and altered by
* drupal_get_css().
* - '#group_callback': A function to call to group #items to enable the use
* of fewer tags by aggregating files and/or using multiple @import
* statements within a single tag.
* - '#aggregate_callback': A function to call to aggregate the items within
* the groups arranged by the #group_callback function.
*
* @return
* A render array that will render to a string of XHTML CSS tags.
*
* @see drupal_get_css()
*/
function drupal_pre_render_styles($elements) {
// Group and aggregate the items.
if (isset($elements['#group_callback'])) {
$elements['#groups'] = $elements['#group_callback']($elements['#items']);
}
if (isset($elements['#aggregate_callback'])) {
$elements['#aggregate_callback']($elements['#groups']);
}
// A dummy query-string is added to filenames, to gain control over
// browser-caching. The string changes on every update or full cache
// flush, forcing browsers to load a new copy of the files, as the
// URL changed.
$query_string = variable_get('css_js_query_string', '0');
// For inline CSS to validate as XHTML, all CSS containing XHTML needs to be
// wrapped in CDATA. To make that backwards compatible with HTML 4, we need to
// comment out the CDATA-tag.
$embed_prefix = "\n\n";
// Defaults for LINK and STYLE elements.
$link_element_defaults = array(
'#type' => 'html_tag',
'#tag' => 'link',
'#attributes' => array(
'type' => 'text/css',
'rel' => 'stylesheet',
),
);
$style_element_defaults = array(
'#type' => 'html_tag',
'#tag' => 'style',
'#attributes' => array(
'type' => 'text/css',
),
);
// Loop through each group.
foreach ($elements['#groups'] as $group) {
switch ($group['type']) {
// For file items, there are three possibilites.
// - The group has been aggregated: in this case, output a LINK tag for
// the aggregate file.
// - The group can be aggregated but has not been (most likely because
// the site administrator disabled the site-wide setting): in this case,
// output as few STYLE tags for the group as possible, using @import
// statement for each file in the group. This enables us to stay within
// IE's limit of 31 total CSS inclusion tags.
// - The group contains items not eligible for aggregation (their
// 'preprocess' flag has been set to FALSE): in this case, output a LINK
// tag for each file.
case 'file':
// The group has been aggregated into a single file: output a LINK tag
// for the aggregate file.
if (isset($group['data'])) {
$element = $link_element_defaults;
$element['#attributes']['href'] = file_create_url($group['data']);
$element['#attributes']['media'] = $group['media'];
$element['#browsers'] = $group['browsers'];
$elements[] = $element;
}
// The group can be aggregated, but hasn't been: combine multiple items
// into as few STYLE tags as possible.
elseif ($group['preprocess']) {
$import = array();
foreach ($group['items'] as $item) {
// A theme's .info file may have an entry for a file that doesn't
// exist as a way of overriding a module or base theme CSS file from
// being added to the page. Normally, file_exists() calls that need
// to run for every page request should be minimized, but this one
// is okay, because it only runs when CSS aggregation is disabled.
// On a server under heavy enough load that file_exists() calls need
// to be minimized, CSS aggregation should be enabled, in which case
// this code is not run. When aggregation is enabled,
// drupal_load_stylesheet() checks file_exists(), but only when
// building the aggregate file, which is then reused for many page
// requests.
if (file_exists($item['data'])) {
// The dummy query string needs to be added to the URL to control
// browser-caching. IE7 does not support a media type on the
// @import statement, so we instead specify the media for the
// group on the STYLE tag.
$import[] = '@import url("' . check_plain(file_create_url($item['data']) . '?' . $query_string) . '");';
}
}
// In addition to IE's limit of 31 total CSS inclusion tags, it also
// has a limit of 31 @import statements per STYLE tag.
while (!empty($import)) {
$import_batch = array_slice($import, 0, 31);
$import = array_slice($import, 31);
$element = $style_element_defaults;
// This simplifies the JavaScript regex, allowing each line
// (separated by \n) to be treated as a completely different string.
// This means that we can use ^ and $ on one line at a time, and not
// worry about style tags since they'll never match the regex.
$element['#value'] = "\n" . implode("\n", $import_batch) . "\n";
$element['#attributes']['media'] = $group['media'];
$element['#browsers'] = $group['browsers'];
$elements[] = $element;
}
}
// The group contains items ineligible for aggregation: output a LINK
// tag for each file.
else {
foreach ($group['items'] as $item) {
$element = $link_element_defaults;
// We do not check file_exists() here, because this code runs for
// files whose 'preprocess' is set to FALSE, and therefore, even
// when aggregation is enabled, and we want to avoid needlessly
// taxing a server that may be under heavy load. The file_exists()
// performed above for files whose 'preprocess' is TRUE is done for
// the benefit of theme .info files, but code that deals with files
// whose 'preprocess' is FALSE is responsible for ensuring the file
// exists.
// The dummy query string needs to be added to the URL to control
// browser-caching.
$query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?';
$element['#attributes']['href'] = file_create_url($item['data']) . $query_string_separator . $query_string;
$element['#attributes']['media'] = $item['media'];
$element['#browsers'] = $group['browsers'];
$elements[] = $element;
}
}
break;
// For inline content, the 'data' property contains the CSS content. If
// the group's 'data' property is set, then output it in a single STYLE
// tag. Otherwise, output a separate STYLE tag for each item.
case 'inline':
if (isset($group['data'])) {
$element = $style_element_defaults;
$element['#value'] = $group['data'];
$element['#value_prefix'] = $embed_prefix;
$element['#value_suffix'] = $embed_suffix;
$element['#attributes']['media'] = $group['media'];
$element['#browsers'] = $group['browsers'];
$elements[] = $element;
}
else {
foreach ($group['items'] as $item) {
$element = $style_element_defaults;
$element['#value'] = $item['data'];
$element['#value_prefix'] = $embed_prefix;
$element['#value_suffix'] = $embed_suffix;
$element['#attributes']['media'] = $item['media'];
$element['#browsers'] = $group['browsers'];
$elements[] = $element;
}
}
break;
// Output a LINK tag for each external item. The item's 'data' property
// contains the full URL.
case 'external':
foreach ($group['items'] as $item) {
$element = $link_element_defaults;
$element['#attributes']['href'] = $item['data'];
$element['#attributes']['media'] = $item['media'];
$element['#browsers'] = $group['browsers'];
$elements[] = $element;
}
break;
}
}
return $elements;
}
/**
* Aggregates and optimizes CSS files into a cache file in the files directory.
*
* The file name for the CSS cache file is generated from the hash of the
* aggregated contents of the files in $css. This forces proxies and browsers
* to download new CSS when the CSS changes.
*
* The cache file name is retrieved on a page load via a lookup variable that
* contains an associative array. The array key is the hash of the file names
* in $css while the value is the cache file name. The cache file is generated
* in two cases. First, if there is no file name value for the key, which will
* happen if a new file name has been added to $css or after the lookup
* variable is emptied to force a rebuild of the cache. Second, the cache file
* is generated if it is missing on disk. Old cache files are not deleted
* immediately when the lookup variable is emptied, but are deleted after a set
* period by drupal_delete_file_if_stale(). This ensures that files referenced
* by a cached page will still be available.
*
* @param $css
* An array of CSS files to aggregate and compress into one file.
*
* @return
* The URI of the CSS cache file, or FALSE if the file could not be saved.
*/
function drupal_build_css_cache($css) {
$data = '';
$uri = '';
$map = variable_get('drupal_css_cache_files', array());
// Create a new array so that only the file names are used to create the hash.
// This prevents new aggregates from being created unnecessarily.
$css_data = array();
foreach ($css as $css_file) {
$css_data[] = $css_file['data'];
}
$key = hash('sha256', serialize($css_data));
if (isset($map[$key])) {
$uri = $map[$key];
}
if (empty($uri) || !file_exists($uri)) {
// Build aggregate CSS file.
foreach ($css as $stylesheet) {
// Only 'file' stylesheets can be aggregated.
if ($stylesheet['type'] == 'file') {
$contents = drupal_load_stylesheet($stylesheet['data'], TRUE);
// Build the base URL of this CSS file: start with the full URL.
$css_base_url = file_create_url($stylesheet['data']);
// Move to the parent.
$css_base_url = substr($css_base_url, 0, strrpos($css_base_url, '/'));
// Simplify to a relative URL if the stylesheet URL starts with the
// base URL of the website.
if (substr($css_base_url, 0, strlen($GLOBALS['base_root'])) == $GLOBALS['base_root']) {
$css_base_url = substr($css_base_url, strlen($GLOBALS['base_root']));
}
_drupal_build_css_path(NULL, $css_base_url . '/');
// Anchor all paths in the CSS with its base URL, ignoring external and absolute paths.
$data .= preg_replace_callback('/url\(\s*[\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\s*\)/i', '_drupal_build_css_path', $contents);
}
}
// Per the W3C specification at http://www.w3.org/TR/REC-CSS2/cascade.html#at-import,
// @import rules must proceed any other style, so we move those to the top.
$regexp = '/@import[^;]+;/i';
preg_match_all($regexp, $data, $matches);
$data = preg_replace($regexp, '', $data);
$data = implode('', $matches[0]) . $data;
// Prefix filename to prevent blocking by firewalls which reject files
// starting with "ad*".
$filename = 'css_' . drupal_hash_base64($data) . '.css';
// Create the css/ within the files folder.
$csspath = 'public://css';
$uri = $csspath . '/' . $filename;
// Create the CSS file.
file_prepare_directory($csspath, FILE_CREATE_DIRECTORY);
if (!file_exists($uri) && !file_unmanaged_save_data($data, $uri, FILE_EXISTS_REPLACE)) {
return FALSE;
}
// If CSS gzip compression is enabled, clean URLs are enabled (which means
// that rewrite rules are working) and the zlib extension is available then
// create a gzipped version of this file. This file is served conditionally
// to browsers that accept gzip using .htaccess rules.
if (variable_get('css_gzip_compression', TRUE) && variable_get('clean_url', 0) && extension_loaded('zlib')) {
if (!file_exists($uri . '.gz') && !file_unmanaged_save_data(gzencode($data, 9, FORCE_GZIP), $uri . '.gz', FILE_EXISTS_REPLACE)) {
return FALSE;
}
}
// Save the updated map.
$map[$key] = $uri;
variable_set('drupal_css_cache_files', $map);
}
return $uri;
}
/**
* Prefixes all paths within a CSS file for drupal_build_css_cache().
*/
function _drupal_build_css_path($matches, $base = NULL) {
$_base = &drupal_static(__FUNCTION__);
// Store base path for preg_replace_callback.
if (isset($base)) {
$_base = $base;
}
// Prefix with base and remove '../' segments where possible.
$path = $_base . (isset($matches[1]) ? $matches[1] : '');
$last = '';
while ($path != $last) {
$last = $path;
$path = preg_replace('`(^|/)(?!\.\./)([^/]+)/\.\./`', '$1', $path);
}
return 'url(' . $path . ')';
}
/**
* Loads the stylesheet and resolves all @import commands.
*
* Loads a stylesheet and replaces @import commands with the contents of the
* imported file. Use this instead of file_get_contents when processing
* stylesheets.
*
* The returned contents are compressed removing white space and comments only
* when CSS aggregation is enabled. This optimization will not apply for
* color.module enabled themes with CSS aggregation turned off.
*
* @param $file
* Name of the stylesheet to be processed.
* @param $optimize
* Defines if CSS contents should be compressed or not.
* @param $reset_basepath
* Used internally to facilitate recursive resolution of @import commands.
*
* @return
* Contents of the stylesheet, including any resolved @import commands.
*/
function drupal_load_stylesheet($file, $optimize = NULL, $reset_basepath = TRUE) {
// These statics are not cache variables, so we don't use drupal_static().
static $_optimize, $basepath;
if ($reset_basepath) {
$basepath = '';
}
// Store the value of $optimize for preg_replace_callback with nested
// @import loops.
if (isset($optimize)) {
$_optimize = $optimize;
}
// Stylesheets are relative one to each other. Start by adding a base path
// prefix provided by the parent stylesheet (if necessary).
if ($basepath && !file_uri_scheme($file)) {
$file = $basepath . '/' . $file;
}
// Store the parent base path to restore it later.
$parent_base_path = $basepath;
// Set the current base path to process possible child imports.
$basepath = dirname($file);
// Load the CSS stylesheet. We suppress errors because themes may specify
// stylesheets in their .info file that don't exist in the theme's path,
// but are merely there to disable certain module CSS files.
$content = '';
if ($contents = @file_get_contents($file)) {
// Return the processed stylesheet.
$content = drupal_load_stylesheet_content($contents, $_optimize);
}
// Restore the parent base path as the file and its childen are processed.
$basepath = $parent_base_path;
return $content;
}
/**
* Processes the contents of a stylesheet for aggregation.
*
* @param $contents
* The contents of the stylesheet.
* @param $optimize
* (optional) Boolean whether CSS contents should be minified. Defaults to
* FALSE.
*
* @return
* Contents of the stylesheet including the imported stylesheets.
*/
function drupal_load_stylesheet_content($contents, $optimize = FALSE) {
// Remove multiple charset declarations for standards compliance (and fixing Safari problems).
$contents = preg_replace('/^@charset\s+[\'"](\S*?)\b[\'"];/i', '', $contents);
if ($optimize) {
// Perform some safe CSS optimizations.
// Regexp to match comment blocks.
$comment = '/\*[^*]*\*+(?:[^/*][^*]*\*+)*/';
// Regexp to match double quoted strings.
$double_quot = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"';
// Regexp to match single quoted strings.
$single_quot = "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'";
// Strip all comment blocks, but keep double/single quoted strings.
$contents = preg_replace(
"<($double_quot|$single_quot)|$comment>Ss",
"$1",
$contents
);
// Remove certain whitespace.
// There are different conditions for removing leading and trailing
// whitespace.
// @see http://php.net/manual/regexp.reference.subpatterns.php
$contents = preg_replace('<
# Do not strip any space from within single or double quotes
(' . $double_quot . '|' . $single_quot . ')
# Strip leading and trailing whitespace.
| \s*([@{};,])\s*
# Strip only leading whitespace from:
# - Closing parenthesis: Retain "@media (bar) and foo".
| \s+([\)])
# Strip only trailing whitespace from:
# - Opening parenthesis: Retain "@media (bar) and foo".
# - Colon: Retain :pseudo-selectors.
| ([\(:])\s+
>xSs',
// Only one of the four capturing groups will match, so its reference
// will contain the wanted value and the references for the
// two non-matching groups will be replaced with empty strings.
'$1$2$3$4',
$contents
);
// End the file with a new line.
$contents = trim($contents);
$contents .= "\n";
}
// Replaces @import commands with the actual stylesheet content.
// This happens recursively but omits external files.
$contents = preg_replace_callback('/@import\s*(?:url\(\s*)?[\'"]?(?![a-z]+:)(?!\/\/)([^\'"\()]+)[\'"]?\s*\)?\s*;/', '_drupal_load_stylesheet', $contents);
return $contents;
}
/**
* Loads stylesheets recursively and returns contents with corrected paths.
*
* This function is used for recursive loading of stylesheets and
* returns the stylesheet content with all url() paths corrected.
*/
function _drupal_load_stylesheet($matches) {
$filename = $matches[1];
// Load the imported stylesheet and replace @import commands in there as well.
$file = drupal_load_stylesheet($filename, NULL, FALSE);
// Determine the file's directory.
$directory = dirname($filename);
// If the file is in the current directory, make sure '.' doesn't appear in
// the url() path.
$directory = $directory == '.' ? '' : $directory .'/';
// Alter all internal url() paths. Leave external paths alone. We don't need
// to normalize absolute paths here (i.e. remove folder/... segments) because
// that will be done later.
return preg_replace('/url\(\s*([\'"]?)(?![a-z]+:|\/+)([^\'")]+)([\'"]?)\s*\)/i', 'url(\1' . $directory . '\2\3)', $file);
}
/**
* Deletes old cached CSS files.
*/
function drupal_clear_css_cache() {
variable_del('drupal_css_cache_files');
file_scan_directory('public://css', '/.*/', array('callback' => 'drupal_delete_file_if_stale'));
}
/**
* Callback to delete files modified more than a set time ago.
*/
function drupal_delete_file_if_stale($uri) {
// Default stale file threshold is 30 days.
if (REQUEST_TIME - filemtime($uri) > variable_get('drupal_stale_file_threshold', 2592000)) {
file_unmanaged_delete($uri);
}
}
/**
* Prepares a string for use as a CSS identifier (element, class, or ID name).
*
* http://www.w3.org/TR/CSS21/syndata.html#characters shows the syntax for valid
* CSS identifiers (including element names, classes, and IDs in selectors.)
*
* @param $identifier
* The identifier to clean.
* @param $filter
* An array of string replacements to use on the identifier.
*
* @return
* The cleaned identifier.
*/
function drupal_clean_css_identifier($identifier, $filter = array(' ' => '-', '_' => '-', '/' => '-', '[' => '-', ']' => '')) {
// Use the advanced drupal_static() pattern, since this is called very often.
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast['allow_css_double_underscores'] = &drupal_static(__FUNCTION__ . ':allow_css_double_underscores');
}
$allow_css_double_underscores = &$drupal_static_fast['allow_css_double_underscores'];
if (!isset($allow_css_double_underscores)) {
$allow_css_double_underscores = variable_get('allow_css_double_underscores', FALSE);
}
// Preserve BEM-style double-underscores depending on custom setting.
if ($allow_css_double_underscores) {
$filter['__'] = '__';
}
// By default, we filter using Drupal's coding standards.
$identifier = strtr($identifier, $filter);
// Valid characters in a CSS identifier are:
// - the hyphen (U+002D)
// - a-z (U+0030 - U+0039)
// - A-Z (U+0041 - U+005A)
// - the underscore (U+005F)
// - 0-9 (U+0061 - U+007A)
// - ISO 10646 characters U+00A1 and higher
// We strip out any character not in the above list.
$identifier = preg_replace('/[^\x{002D}\x{0030}-\x{0039}\x{0041}-\x{005A}\x{005F}\x{0061}-\x{007A}\x{00A1}-\x{FFFF}]/u', '', $identifier);
return $identifier;
}
/**
* Prepares a string for use as a valid class name.
*
* Do not pass one string containing multiple classes as they will be
* incorrectly concatenated with dashes, i.e. "one two" will become "one-two".
*
* @param $class
* The class name to clean.
*
* @return
* The cleaned class name.
*/
function drupal_html_class($class) {
// The output of this function will never change, so this uses a normal
// static instead of drupal_static().
static $classes = array();
if (!isset($classes[$class])) {
$classes[$class] = drupal_clean_css_identifier(drupal_strtolower($class));
}
return $classes[$class];
}
/**
* Prepares a string for use as a valid HTML ID and guarantees uniqueness.
*
* This function ensures that each passed HTML ID value only exists once on the
* page. By tracking the already returned ids, this function enables forms,
* blocks, and other content to be output multiple times on the same page,
* without breaking (X)HTML validation.
*
* For already existing IDs, a counter is appended to the ID string. Therefore,
* JavaScript and CSS code should not rely on any value that was generated by
* this function and instead should rely on manually added CSS classes or
* similarly reliable constructs.
*
* Two consecutive hyphens separate the counter from the original ID. To manage
* uniqueness across multiple Ajax requests on the same page, Ajax requests
* POST an array of all IDs currently present on the page, which are used to
* prime this function's cache upon first invocation.
*
* To allow reverse-parsing of IDs submitted via Ajax, any multiple consecutive
* hyphens in the originally passed $id are replaced with a single hyphen.
*
* @param $id
* The ID to clean.
*
* @return
* The cleaned ID.
*/
function drupal_html_id($id) {
// If this is an Ajax request, then content returned by this page request will
// be merged with content already on the base page. The HTML IDs must be
// unique for the fully merged content. Therefore, initialize $seen_ids to
// take into account IDs that are already in use on the base page.
static $drupal_static_fast;
if (!isset($drupal_static_fast['seen_ids_init'])) {
$drupal_static_fast['seen_ids_init'] = &drupal_static(__FUNCTION__ . ':init');
}
$seen_ids_init = &$drupal_static_fast['seen_ids_init'];
if (!isset($seen_ids_init)) {
// Ideally, Drupal would provide an API to persist state information about
// prior page requests in the database, and we'd be able to add this
// function's $seen_ids static variable to that state information in order
// to have it properly initialized for this page request. However, no such
// page state API exists, so instead, ajax.js adds all of the in-use HTML
// IDs to the POST data of Ajax submissions. Direct use of $_POST is
// normally not recommended as it could open up security risks, but because
// the raw POST data is cast to a number before being returned by this
// function, this usage is safe.
if (empty($_POST['ajax_html_ids'])) {
$seen_ids_init = array();
}
else {
// This function ensures uniqueness by appending a counter to the base id
// requested by the calling function after the first occurrence of that
// requested id. $_POST['ajax_html_ids'] contains the ids as they were
// returned by this function, potentially with the appended counter, so
// we parse that to reconstruct the $seen_ids array.
if (isset($_POST['ajax_html_ids'][0]) && strpos($_POST['ajax_html_ids'][0], ',') === FALSE) {
$ajax_html_ids = $_POST['ajax_html_ids'];
}
else {
// jquery.form.js may send the server a comma-separated string as the
// first element of an array (see http://drupal.org/node/1575060), so
// we need to convert it to an array in that case.
$ajax_html_ids = explode(',', $_POST['ajax_html_ids'][0]);
}
foreach ($ajax_html_ids as $seen_id) {
// We rely on '--' being used solely for separating a base id from the
// counter, which this function ensures when returning an id.
$parts = explode('--', $seen_id, 2);
if (!empty($parts[1]) && is_numeric($parts[1])) {
list($seen_id, $i) = $parts;
}
else {
$i = 1;
}
if (!isset($seen_ids_init[$seen_id]) || ($i > $seen_ids_init[$seen_id])) {
$seen_ids_init[$seen_id] = $i;
}
}
}
}
if (!isset($drupal_static_fast['seen_ids'])) {
$drupal_static_fast['seen_ids'] = &drupal_static(__FUNCTION__, $seen_ids_init);
}
$seen_ids = &$drupal_static_fast['seen_ids'];
$id = strtr(drupal_strtolower($id), array(' ' => '-', '_' => '-', '[' => '-', ']' => ''));
// As defined in http://www.w3.org/TR/html4/types.html#type-name, HTML IDs can
// only contain letters, digits ([0-9]), hyphens ("-"), underscores ("_"),
// colons (":"), and periods ("."). We strip out any character not in that
// list. Note that the CSS spec doesn't allow colons or periods in identifiers
// (http://www.w3.org/TR/CSS21/syndata.html#characters), so we strip those two
// characters as well.
$id = preg_replace('/[^A-Za-z0-9\-_]/', '', $id);
// Removing multiple consecutive hyphens.
$id = preg_replace('/\-+/', '-', $id);
// Ensure IDs are unique by appending a counter after the first occurrence.
// The counter needs to be appended with a delimiter that does not exist in
// the base ID. Requiring a unique delimiter helps ensure that we really do
// return unique IDs and also helps us re-create the $seen_ids array during
// Ajax requests.
if (isset($seen_ids[$id])) {
$id = $id . '--' . ++$seen_ids[$id];
}
else {
$seen_ids[$id] = 1;
}
return $id;
}
/**
* Provides a standard HTML class name that identifies a page region.
*
* It is recommended that template preprocess functions apply this class to any
* page region that is output by the theme (Drupal core already handles this in
* the standard template preprocess implementation). Standardizing the class
* names in this way allows modules to implement certain features, such as
* drag-and-drop or dynamic Ajax loading, in a theme-independent way.
*
* @param $region
* The name of the page region (for example, 'page_top' or 'content').
*
* @return
* An HTML class that identifies the region (for example, 'region-page-top'
* or 'region-content').
*
* @see template_preprocess_region()
*/
function drupal_region_class($region) {
return drupal_html_class("region-$region");
}
/**
* Adds a JavaScript file, setting, or inline code to the page.
*
* The behavior of this function depends on the parameters it is called with.
* Generally, it handles the addition of JavaScript to the page, either as
* reference to an existing file or as inline code. The following actions can be
* performed using this function:
* - Add a file ('file'): Adds a reference to a JavaScript file to the page.
* - Add inline JavaScript code ('inline'): Executes a piece of JavaScript code
* on the current page by placing the code directly in the page (for example,
* to tell the user that a new message arrived, by opening a pop up, alert
* box, etc.). This should only be used for JavaScript that cannot be executed
* from a file. When adding inline code, make sure that you are not relying on
* $() being the jQuery function. Wrap your code in
* @code (function ($) {... })(jQuery); @endcode
* or use jQuery() instead of $().
* - Add external JavaScript ('external'): Allows the inclusion of external
* JavaScript files that are not hosted on the local server. Note that these
* external JavaScript references do not get aggregated when preprocessing is
* on.
* - Add settings ('setting'): Adds settings to Drupal's global storage of
* JavaScript settings. Per-page settings are required by some modules to
* function properly. All settings will be accessible at Drupal.settings.
*
* Examples:
* @code
* drupal_add_js('misc/collapse.js');
* drupal_add_js('misc/collapse.js', 'file');
* drupal_add_js('jQuery(document).ready(function () { alert("Hello!"); });', 'inline');
* drupal_add_js('jQuery(document).ready(function () { alert("Hello!"); });',
* array('type' => 'inline', 'scope' => 'footer', 'weight' => 5)
* );
* drupal_add_js('http://example.com/example.js', 'external');
* drupal_add_js(array('myModule' => array('key' => 'value')), 'setting');
* @endcode
*
* Calling drupal_static_reset('drupal_add_js') will clear all JavaScript added
* so far.
*
* If JavaScript aggregation is enabled, all JavaScript files added with
* $options['preprocess'] set to TRUE will be merged into one aggregate file.
* Preprocessed inline JavaScript will not be aggregated into this single file.
* Externally hosted JavaScripts are never aggregated.
*
* The reason for aggregating the files is outlined quite thoroughly here:
* http://www.die.net/musings/page_load_time/ "Load fewer external objects. Due
* to request overhead, one bigger file just loads faster than two smaller ones
* half its size."
*
* $options['preprocess'] should be only set to TRUE when a file is required for
* all typical visitors and most pages of a site. It is critical that all
* preprocessed files are added unconditionally on every page, even if the
* files are not needed on a page. This is normally done by calling
* drupal_add_js() in a hook_init() implementation.
*
* Non-preprocessed files should only be added to the page when they are
* actually needed.
*
* @param $data
* (optional) If given, the value depends on the $options parameter, or
* $options['type'] if $options is passed as an associative array:
* - 'file': Path to the file relative to base_path().
* - 'inline': The JavaScript code that should be placed in the given scope.
* - 'external': The absolute path to an external JavaScript file that is not
* hosted on the local server. These files will not be aggregated if
* JavaScript aggregation is enabled.
* - 'setting': An associative array with configuration options. The array is
* merged directly into Drupal.settings. All modules should wrap their
* actual configuration settings in another variable to prevent conflicts in
* the Drupal.settings namespace. Items added with a string key will replace
* existing settings with that key; items with numeric array keys will be
* added to the existing settings array.
* @param $options
* (optional) A string defining the type of JavaScript that is being added in
* the $data parameter ('file'/'setting'/'inline'/'external'), or an
* associative array. JavaScript settings should always pass the string
* 'setting' only. Other types can have the following elements in the array:
* - type: The type of JavaScript that is to be added to the page. Allowed
* values are 'file', 'inline', 'external' or 'setting'. Defaults
* to 'file'.
* - scope: The location in which you want to place the script. Possible
* values are 'header' or 'footer'. If your theme implements different
* regions, you can also use these. Defaults to 'header'.
* - group: A number identifying the group in which to add the JavaScript.
* Available constants are:
* - JS_LIBRARY: Any libraries, settings, or jQuery plugins.
* - JS_DEFAULT: Any module-layer JavaScript.
* - JS_THEME: Any theme-layer JavaScript.
* The group number serves as a weight: JavaScript within a lower weight
* group is presented on the page before JavaScript within a higher weight
* group.
* - every_page: For optimal front-end performance when aggregation is
* enabled, this should be set to TRUE if the JavaScript is present on every
* page of the website for users for whom it is present at all. This
* defaults to FALSE. It is set to TRUE for JavaScript files that are added
* via module and theme .info files. Modules that add JavaScript within
* hook_init() implementations, or from other code that ensures that the
* JavaScript is added to all website pages, should also set this flag to
* TRUE. All JavaScript files within the same group and that have the
* 'every_page' flag set to TRUE and do not have 'preprocess' set to FALSE
* are aggregated together into a single aggregate file, and that aggregate
* file can be reused across a user's entire site visit, leading to faster
* navigation between pages. However, JavaScript that is only needed on
* pages less frequently visited, can be added by code that only runs for
* those particular pages, and that code should not set the 'every_page'
* flag. This minimizes the size of the aggregate file that the user needs
* to download when first visiting the website. JavaScript without the
* 'every_page' flag is aggregated into a separate aggregate file. This
* other aggregate file is likely to change from page to page, and each new
* aggregate file needs to be downloaded when first encountered, so it
* should be kept relatively small by ensuring that most commonly needed
* JavaScript is added to every page.
* - weight: A number defining the order in which the JavaScript is added to
* the page relative to other JavaScript with the same 'scope', 'group',
* and 'every_page' value. In some cases, the order in which the JavaScript
* is presented on the page is very important. jQuery, for example, must be
* added to the page before any jQuery code is run, so jquery.js uses the
* JS_LIBRARY group and a weight of -20, jquery.once.js (a library drupal.js
* depends on) uses the JS_LIBRARY group and a weight of -19, drupal.js uses
* the JS_LIBRARY group and a weight of -1, other libraries use the
* JS_LIBRARY group and a weight of 0 or higher, and all other scripts use
* one of the other group constants. The exact ordering of JavaScript is as
* follows:
* - First by scope, with 'header' first, 'footer' last, and any other
* scopes provided by a custom theme coming in between, as determined by
* the theme.
* - Then by group.
* - Then by the 'every_page' flag, with TRUE coming before FALSE.
* - Then by weight.
* - Then by the order in which the JavaScript was added. For example, all
* else being the same, JavaScript added by a call to drupal_add_js() that
* happened later in the page request gets added to the page after one for
* which drupal_add_js() happened earlier in the page request.
* - requires_jquery: Set this to FALSE if the JavaScript you are adding does
* not have a dependency on jQuery. Defaults to TRUE, except for JavaScript
* settings where it defaults to FALSE. This is used on sites that have the
* 'javascript_always_use_jquery' variable set to FALSE; on those sites, if
* all the JavaScript added to the page by drupal_add_js() does not have a
* dependency on jQuery, then for improved front-end performance Drupal
* will not add jQuery and related libraries and settings to the page.
* - defer: If set to TRUE, the defer attribute is set on the Lorem ipsum ",
'variables' => NULL,
'severity' => WATCHDOG_NOTICE,
'link' => 'foo/bar',
'request_uri' => 'http://example.com?dblog=1',
'referer' => 'http://example.org?dblog=2',
'ip' => '0.0.1.0',
'timestamp' => REQUEST_TIME,
);
dblog_watchdog($log);
$wid = db_query('SELECT MAX(wid) FROM {watchdog}')->fetchField();
$this->drupalGet('admin/reports/event/' . $wid);
$this->assertResponse(200);
$this->assertNoRaw("");
$this->assertRaw("alert('foo'); Lorem ipsum ");
}
}
array(
'choice_wrapper' => array(
'label' => t('Poll choices'),
'description' => t('Poll choices'),
'weight' => -4,
),
'settings' => array(
'label' => t('Poll settings'),
'description' => t('Poll module settings'),
'weight' => -3,
),
),
'display' => array(
'poll_view_voting' => array(
'label' => t('Poll vote'),
'description' => t('Poll vote'),
'weight' => 0,
),
'poll_view_results' => array(
'label' => t('Poll results'),
'description' => t('Poll results'),
'weight' => 0,
),
)
);
return $extra;
}
/**
* Alter "pseudo-field" components on fieldable entities.
*
* @param $info
* The associative array of 'pseudo-field' components.
*
* @see hook_field_extra_fields()
*
* @ingroup field_types
*/
function hook_field_extra_fields_alter(&$info) {
// Force node title to always be at the top of the list by default.
foreach (node_type_get_types() as $bundle) {
if (isset($info['node'][$bundle->type]['form']['title'])) {
$info['node'][$bundle->type]['form']['title']['weight'] = -20;
}
}
}
/**
* @defgroup field_types Field Types API
* @{
* Define field types.
*
* In the Field API, each field has a type, which determines what kind of data
* (integer, string, date, etc.) the field can hold, which settings it provides,
* and so on. The data type(s) accepted by a field are defined in
* hook_field_schema(); other basic properties of a field are defined in
* hook_field_info(). The other hooks below are called by the Field Attach API
* to perform field-type-specific actions.
*
* The Field Types API also defines two kinds of pluggable handlers: widgets
* and formatters. @link field_widget Widgets @endlink specify how the field
* appears in edit forms, while @link field_formatter formatters @endlink
* specify how the field appears in displayed entities.
*
* A third kind of pluggable handlers, storage backends, is defined by the
* @link field_storage Field Storage API @endlink.
*
* See @link field Field API @endlink for information about the other parts of
* the Field API.
*/
/**
* Define Field API field types.
*
* Along with this hook, you also need to implement other hooks. See
* @link field_types Field Types API @endlink for more information.
*
* @return
* An array whose keys are field type names and whose values are arrays
* describing the field type, with the following key/value pairs:
* - label: The human-readable name of the field type.
* - description: A short description for the field type.
* - settings: An array whose keys are the names of the settings available
* for the field type, and whose values are the default values for those
* settings.
* - instance_settings: An array whose keys are the names of the settings
* available for instances of the field type, and whose values are the
* default values for those settings. Instance-level settings can have
* different values on each field instance, and thus allow greater
* flexibility than field-level settings. It is recommended to put settings
* at the instance level whenever possible. Notable exceptions: settings
* acting on the schema definition, or settings that Views needs to use
* across field instances (for example, the list of allowed values).
* - default_widget: The machine name of the default widget to be used by
* instances of this field type, when no widget is specified in the
* instance definition. This widget must be available whenever the field
* type is available (i.e. provided by the field type module, or by a module
* the field type module depends on).
* - default_formatter: The machine name of the default formatter to be used
* by instances of this field type, when no formatter is specified in the
* instance definition. This formatter must be available whenever the field
* type is available (i.e. provided by the field type module, or by a module
* the field type module depends on).
* - no_ui: (optional) A boolean specifying that users should not be allowed
* to create fields and instances of this field type through the UI. Such
* fields can only be created programmatically with field_create_field()
* and field_create_instance(). Defaults to FALSE.
*
* @see hook_field_info_alter()
*/
function hook_field_info() {
return array(
'text' => array(
'label' => t('Text'),
'description' => t('This field stores varchar text in the database.'),
'settings' => array('max_length' => 255),
'instance_settings' => array('text_processing' => 0),
'default_widget' => 'text_textfield',
'default_formatter' => 'text_default',
),
'text_long' => array(
'label' => t('Long text'),
'description' => t('This field stores long text in the database.'),
'settings' => array('max_length' => ''),
'instance_settings' => array('text_processing' => 0),
'default_widget' => 'text_textarea',
'default_formatter' => 'text_default',
),
'text_with_summary' => array(
'label' => t('Long text and summary'),
'description' => t('This field stores long text in the database along with optional summary text.'),
'settings' => array('max_length' => ''),
'instance_settings' => array('text_processing' => 1, 'display_summary' => 0),
'default_widget' => 'text_textarea_with_summary',
'default_formatter' => 'text_summary_or_trimmed',
),
);
}
/**
* Perform alterations on Field API field types.
*
* @param $info
* Array of information on field types exposed by hook_field_info()
* implementations.
*/
function hook_field_info_alter(&$info) {
// Add a setting to all field types.
foreach ($info as $field_type => $field_type_info) {
$info[$field_type]['settings'] += array(
'mymodule_additional_setting' => 'default value',
);
}
// Change the default widget for fields of type 'foo'.
if (isset($info['foo'])) {
$info['foo']['default widget'] = 'mymodule_widget';
}
}
/**
* Define the Field API schema for a field structure.
*
* This is invoked when a field is created, in order to obtain the database
* schema from the module that defines the field's type.
*
* This hook must be defined in the module's .install file for it to be detected
* during installation and upgrade.
*
* @param $field
* A field structure.
*
* @return
* An associative array with the following keys:
* - columns: An array of Schema API column specifications, keyed by column
* name. This specifies what comprises a value for a given field. For
* example, a value for a number field is simply 'value', while a value for
* a formatted text field is the combination of 'value' and 'format'. It is
* recommended to avoid having the column definitions depend on field
* settings when possible. No assumptions should be made on how storage
* engines internally use the original column name to structure their
* storage.
* - indexes: (optional) An array of Schema API indexes definitions. Only
* columns that appear in the 'columns' array are allowed. Those indexes
* will be used as default indexes. Callers of field_create_field() can
* specify additional indexes, or, at their own risk, modify the default
* indexes specified by the field-type module. Some storage engines might
* not support indexes.
* - foreign keys: (optional) An array of Schema API foreign keys
* definitions.
*/
function hook_field_schema($field) {
if ($field['type'] == 'text_long') {
$columns = array(
'value' => array(
'type' => 'text',
'size' => 'big',
'not null' => FALSE,
),
);
}
else {
$columns = array(
'value' => array(
'type' => 'varchar',
'length' => $field['settings']['max_length'],
'not null' => FALSE,
),
);
}
$columns += array(
'format' => array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
),
);
return array(
'columns' => $columns,
'indexes' => array(
'format' => array('format'),
),
'foreign keys' => array(
'format' => array(
'table' => 'filter_format',
'columns' => array('format' => 'format'),
),
),
);
}
/**
* Define custom load behavior for this module's field types.
*
* Unlike most other field hooks, this hook operates on multiple entities. The
* $entities, $instances and $items parameters are arrays keyed by entity ID.
* For performance reasons, information for all available entity should be
* loaded in a single query where possible.
*
* Note that the changes made to the field values get cached by the field cache
* for subsequent loads. You should never use this hook to load fieldable
* entities, since this is likely to cause infinite recursions when
* hook_field_load() is run on those as well. Use
* hook_field_formatter_prepare_view() instead.
*
* Make changes or additions to field values by altering the $items parameter by
* reference. There is no return value.
*
* @param $entity_type
* The type of $entity.
* @param $entities
* Array of entities being loaded, keyed by entity ID.
* @param $field
* The field structure for the operation.
* @param $instances
* Array of instance structures for $field for each entity, keyed by entity
* ID.
* @param $langcode
* The language code associated with $items.
* @param $items
* Array of field values already loaded for the entities, keyed by entity ID.
* Store your changes in this parameter (passed by reference).
* @param $age
* FIELD_LOAD_CURRENT to load the most recent revision for all fields, or
* FIELD_LOAD_REVISION to load the version indicated by each entity.
*/
function hook_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
// Sample code from text.module: precompute sanitized strings so they are
// stored in the field cache.
foreach ($entities as $id => $entity) {
foreach ($items[$id] as $delta => $item) {
// Only process items with a cacheable format, the rest will be handled
// by formatters if needed.
if (empty($instances[$id]['settings']['text_processing']) || filter_format_allowcache($item['format'])) {
$items[$id][$delta]['safe_value'] = isset($item['value']) ? _text_sanitize($instances[$id], $langcode, $item, 'value') : '';
if ($field['type'] == 'text_with_summary') {
$items[$id][$delta]['safe_summary'] = isset($item['summary']) ? _text_sanitize($instances[$id], $langcode, $item, 'summary') : '';
}
}
}
}
}
/**
* Prepare field values prior to display.
*
* This hook is invoked before the field values are handed to formatters
* for display, and runs before the formatters' own
* hook_field_formatter_prepare_view().
*
* Unlike most other field hooks, this hook operates on multiple entities. The
* $entities, $instances and $items parameters are arrays keyed by entity ID.
* For performance reasons, information for all available entities should be
* loaded in a single query where possible.
*
* Make changes or additions to field values by altering the $items parameter by
* reference. There is no return value.
*
* @param $entity_type
* The type of $entity.
* @param $entities
* Array of entities being displayed, keyed by entity ID.
* @param $field
* The field structure for the operation.
* @param $instances
* Array of instance structures for $field for each entity, keyed by entity
* ID.
* @param $langcode
* The language associated to $items.
* @param $items
* $entity->{$field['field_name']}, or an empty array if unset.
*/
function hook_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
// Sample code from image.module: if there are no images specified at all,
// use the default image.
foreach ($entities as $id => $entity) {
if (empty($items[$id]) && $field['settings']['default_image']) {
if ($file = file_load($field['settings']['default_image'])) {
$items[$id][0] = (array) $file + array(
'is_default' => TRUE,
'alt' => '',
'title' => '',
);
}
}
}
}
/**
* Validate this module's field data.
*
* If there are validation problems, add to the $errors array (passed by
* reference). There is no return value.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity for the operation.
* @param $field
* The field structure for the operation.
* @param $instance
* The instance structure for $field on $entity's bundle.
* @param $langcode
* The language associated with $items.
* @param $items
* $entity->{$field['field_name']}[$langcode], or an empty array if unset.
* @param $errors
* The array of errors (keyed by field name, language code, and delta) that
* have already been reported for the entity. The function should add its
* errors to this array. Each error is an associative array with the following
* keys and values:
* - error: An error code (should be a string prefixed with the module name).
* - message: The human readable message to be displayed.
*/
function hook_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
foreach ($items as $delta => $item) {
if (!empty($item['value'])) {
if (!empty($field['settings']['max_length']) && drupal_strlen($item['value']) > $field['settings']['max_length']) {
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => 'text_max_length',
'message' => t('%name: the value may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length'])),
);
}
}
}
}
/**
* Define custom presave behavior for this module's field types.
*
* Make changes or additions to field values by altering the $items parameter by
* reference. There is no return value.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity for the operation.
* @param $field
* The field structure for the operation.
* @param $instance
* The instance structure for $field on $entity's bundle.
* @param $langcode
* The language associated with $items.
* @param $items
* $entity->{$field['field_name']}[$langcode], or an empty array if unset.
*/
function hook_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
if ($field['type'] == 'number_decimal') {
// Let PHP round the value to ensure consistent behavior across storage
// backends.
foreach ($items as $delta => $item) {
if (isset($item['value'])) {
$items[$delta]['value'] = round($item['value'], $field['settings']['scale']);
}
}
}
}
/**
* Define custom insert behavior for this module's field data.
*
* This hook is invoked from field_attach_insert() on the module that defines a
* field, during the process of inserting an entity object (node, taxonomy term,
* etc.). It is invoked just before the data for this field on the particular
* entity object is inserted into field storage. Only field modules that are
* storing or tracking information outside the standard field storage mechanism
* need to implement this hook.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity for the operation.
* @param $field
* The field structure for the operation.
* @param $instance
* The instance structure for $field on $entity's bundle.
* @param $langcode
* The language associated with $items.
* @param $items
* $entity->{$field['field_name']}[$langcode], or an empty array if unset.
*
* @see hook_field_update()
* @see hook_field_delete()
*/
function hook_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) {
if (variable_get('taxonomy_maintain_index_table', TRUE) && $field['storage']['type'] == 'field_sql_storage' && $entity_type == 'node' && $entity->status) {
$query = db_insert('taxonomy_index')->fields(array('nid', 'tid', 'sticky', 'created', ));
foreach ($items as $item) {
$query->values(array(
'nid' => $entity->nid,
'tid' => $item['tid'],
'sticky' => $entity->sticky,
'created' => $entity->created,
));
}
$query->execute();
}
}
/**
* Define custom update behavior for this module's field data.
*
* This hook is invoked from field_attach_update() on the module that defines a
* field, during the process of updating an entity object (node, taxonomy term,
* etc.). It is invoked just before the data for this field on the particular
* entity object is updated into field storage. Only field modules that are
* storing or tracking information outside the standard field storage mechanism
* need to implement this hook.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity for the operation.
* @param $field
* The field structure for the operation.
* @param $instance
* The instance structure for $field on $entity's bundle.
* @param $langcode
* The language associated with $items.
* @param $items
* $entity->{$field['field_name']}[$langcode], or an empty array if unset.
*
* @see hook_field_insert()
* @see hook_field_delete()
*/
function hook_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) {
if (variable_get('taxonomy_maintain_index_table', TRUE) && $field['storage']['type'] == 'field_sql_storage' && $entity_type == 'node') {
$first_call = &drupal_static(__FUNCTION__, array());
// We don't maintain data for old revisions, so clear all previous values
// from the table. Since this hook runs once per field, per object, make
// sure we only wipe values once.
if (!isset($first_call[$entity->nid])) {
$first_call[$entity->nid] = FALSE;
db_delete('taxonomy_index')->condition('nid', $entity->nid)->execute();
}
// Only save data to the table if the node is published.
if ($entity->status) {
$query = db_insert('taxonomy_index')->fields(array('nid', 'tid', 'sticky', 'created'));
foreach ($items as $item) {
$query->values(array(
'nid' => $entity->nid,
'tid' => $item['tid'],
'sticky' => $entity->sticky,
'created' => $entity->created,
));
}
$query->execute();
}
}
}
/**
* Update the storage information for a field.
*
* This is invoked on the field's storage module from field_update_field(),
* before the new field information is saved to the database. The field storage
* module should update its storage tables to agree with the new field
* information. If there is a problem, the field storage module should throw an
* exception.
*
* @param $field
* The updated field structure to be saved.
* @param $prior_field
* The previously-saved field structure.
* @param $has_data
* TRUE if the field has data in storage currently.
*/
function hook_field_storage_update_field($field, $prior_field, $has_data) {
if (!$has_data) {
// There is no data. Re-create the tables completely.
$prior_schema = _field_sql_storage_schema($prior_field);
foreach ($prior_schema as $name => $table) {
db_drop_table($name, $table);
}
$schema = _field_sql_storage_schema($field);
foreach ($schema as $name => $table) {
db_create_table($name, $table);
}
}
else {
// There is data. See field_sql_storage_field_storage_update_field() for
// an example of what to do to modify the schema in place, preserving the
// old data as much as possible.
}
drupal_get_schema(NULL, TRUE);
}
/**
* Define custom delete behavior for this module's field data.
*
* This hook is invoked from field_attach_delete() on the module that defines a
* field, during the process of deleting an entity object (node, taxonomy term,
* etc.). It is invoked just before the data for this field on the particular
* entity object is deleted from field storage. Only field modules that are
* storing or tracking information outside the standard field storage mechanism
* need to implement this hook.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity for the operation.
* @param $field
* The field structure for the operation.
* @param $instance
* The instance structure for $field on $entity's bundle.
* @param $langcode
* The language associated with $items.
* @param $items
* $entity->{$field['field_name']}[$langcode], or an empty array if unset.
*
* @see hook_field_insert()
* @see hook_field_update()
*/
function hook_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
foreach ($items as $delta => $item) {
// For hook_file_references(), remember that this is being deleted.
$item['file_field_name'] = $field['field_name'];
// Pass in the ID of the object that is being removed so all references can
// be counted in hook_file_references().
$item['file_field_type'] = $entity_type;
$item['file_field_id'] = $id;
file_field_delete_file($item, $field, $entity_type, $id);
}
}
/**
* Define custom revision delete behavior for this module's field types.
*
* This hook is invoked just before the data is deleted from field storage
* in field_attach_delete_revision(), and will only be called for fieldable
* types that are versioned.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity for the operation.
* @param $field
* The field structure for the operation.
* @param $instance
* The instance structure for $field on $entity's bundle.
* @param $langcode
* The language associated with $items.
* @param $items
* $entity->{$field['field_name']}[$langcode], or an empty array if unset.
*/
function hook_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, &$items) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
foreach ($items as $delta => $item) {
// For hook_file_references, remember that this file is being deleted.
$item['file_field_name'] = $field['field_name'];
if (file_field_delete_file($item, $field, $entity_type, $id)) {
$items[$delta] = NULL;
}
}
}
/**
* Define custom prepare_translation behavior for this module's field types.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity for the operation.
* @param $field
* The field structure for the operation.
* @param $instance
* The instance structure for $field on $entity's bundle.
* @param $langcode
* The language associated to $items.
* @param $items
* $entity->{$field['field_name']}[$langcode], or an empty array if unset.
* @param $source_entity
* The source entity from which field values are being copied.
* @param $source_langcode
* The source language from which field values are being copied.
*
* @ingroup field_language
*/
function hook_field_prepare_translation($entity_type, $entity, $field, $instance, $langcode, &$items, $source_entity, $source_langcode) {
// If the translating user is not permitted to use the assigned text format,
// we must not expose the source values.
$field_name = $field['field_name'];
$formats = filter_formats();
$format_id = $source_entity->{$field_name}[$source_langcode][0]['format'];
if (!filter_access($formats[$format_id])) {
$items = array();
}
}
/**
* Define what constitutes an empty item for a field type.
*
* @param $item
* An item that may or may not be empty.
* @param $field
* The field to which $item belongs.
*
* @return
* TRUE if $field's type considers $item not to contain any data;
* FALSE otherwise.
*/
function hook_field_is_empty($item, $field) {
if (empty($item['value']) && (string) $item['value'] !== '0') {
return TRUE;
}
return FALSE;
}
/**
* @} End of "defgroup field_types".
*/
/**
* @defgroup field_widget Field Widget API
* @{
* Define Field API widget types.
*
* Field API widgets specify how fields are displayed in edit forms. Fields of a
* given @link field_types field type @endlink may be edited using more than one
* widget. In this case, the Field UI module allows the site builder to choose
* which widget to use. Widget types are defined by implementing
* hook_field_widget_info().
*
* Widgets are @link forms_api_reference.html Form API @endlink elements with
* additional processing capabilities. Widget hooks are typically called by the
* Field Attach API during the creation of the field form structure with
* field_attach_form().
*
* @see field
* @see field_types
* @see field_formatter
*/
/**
* Expose Field API widget types.
*
* @return
* An array describing the widget types implemented by the module.
* The keys are widget type names. To avoid name clashes, widget type
* names should be prefixed with the name of the module that exposes them.
* The values are arrays describing the widget type, with the following
* key/value pairs:
* - label: The human-readable name of the widget type.
* - description: A short description for the widget type.
* - field types: An array of field types the widget supports.
* - settings: An array whose keys are the names of the settings available
* for the widget type, and whose values are the default values for those
* settings.
* - behaviors: (optional) An array describing behaviors of the widget, with
* the following elements:
* - multiple values: One of the following constants:
* - FIELD_BEHAVIOR_DEFAULT: (default) If the widget allows the input of
* one single field value (most common case). The widget will be
* repeated for each value input.
* - FIELD_BEHAVIOR_CUSTOM: If one single copy of the widget can receive
* several field values. Examples: checkboxes, multiple select,
* comma-separated textfield.
* - default value: One of the following constants:
* - FIELD_BEHAVIOR_DEFAULT: (default) If the widget accepts default
* values.
* - FIELD_BEHAVIOR_NONE: if the widget does not support default values.
* - weight: (optional) An integer to determine the weight of this widget
* relative to other widgets in the Field UI when selecting a widget for a
* given field instance.
*
* @see hook_field_widget_info_alter()
* @see hook_field_widget_form()
* @see hook_field_widget_form_alter()
* @see hook_field_widget_WIDGET_TYPE_form_alter()
* @see hook_field_widget_error()
* @see hook_field_widget_settings_form()
*/
function hook_field_widget_info() {
return array(
'text_textfield' => array(
'label' => t('Text field'),
'field types' => array('text'),
'settings' => array('size' => 60),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_DEFAULT,
'default value' => FIELD_BEHAVIOR_DEFAULT,
),
),
'text_textarea' => array(
'label' => t('Text area (multiple rows)'),
'field types' => array('text_long'),
'settings' => array('rows' => 5),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_DEFAULT,
'default value' => FIELD_BEHAVIOR_DEFAULT,
),
),
'text_textarea_with_summary' => array(
'label' => t('Text area with a summary'),
'field types' => array('text_with_summary'),
'settings' => array('rows' => 20, 'summary_rows' => 5),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_DEFAULT,
'default value' => FIELD_BEHAVIOR_DEFAULT,
),
// As an advanced widget, force it to sink to the bottom of the choices.
'weight' => 2,
),
);
}
/**
* Perform alterations on Field API widget types.
*
* @param $info
* Array of informations on widget types exposed by hook_field_widget_info()
* implementations.
*/
function hook_field_widget_info_alter(&$info) {
// Add a setting to a widget type.
$info['text_textfield']['settings'] += array(
'mymodule_additional_setting' => 'default value',
);
// Let a new field type re-use an existing widget.
$info['options_select']['field types'][] = 'my_field_type';
}
/**
* Return the form for a single field widget.
*
* Field widget form elements should be based on the passed-in $element, which
* contains the base form element properties derived from the field
* configuration.
*
* Field API will set the weight, field name and delta values for each form
* element. If there are multiple values for this field, the Field API will
* invoke this hook as many times as needed.
*
* Note that, depending on the context in which the widget is being included
* (regular entity form, field configuration form, advanced search form...),
* the values for $field and $instance might be different from the "official"
* definitions returned by field_info_field() and field_info_instance().
* Examples: mono-value widget even if the field is multi-valued, non-required
* widget even if the field is 'required'...
*
* Therefore, the FAPI element callbacks (such as #process, #element_validate,
* #value_callback...) used by the widget cannot use the field_info_field()
* or field_info_instance() functions to retrieve the $field or $instance
* definitions they should operate on. The field_widget_field() and
* field_widget_instance() functions should be used instead to fetch the
* current working definitions from $form_state, where Field API stores them.
*
* Alternatively, hook_field_widget_form() can extract the needed specific
* properties from $field and $instance and set them as ad-hoc
* $element['#custom'] properties, for later use by its element callbacks.
*
* Other modules may alter the form element provided by this function using
* hook_field_widget_form_alter().
*
* @param $form
* The form structure where widgets are being attached to. This might be a
* full form structure, or a sub-element of a larger form.
* @param $form_state
* An associative array containing the current state of the form.
* @param $field
* The field structure.
* @param $instance
* The field instance.
* @param $langcode
* The language associated with $items.
* @param $items
* Array of default values for this field.
* @param $delta
* The order of this item in the array of subelements (0, 1, 2, etc).
* @param $element
* A form element array containing basic properties for the widget:
* - #entity_type: The name of the entity the field is attached to.
* - #bundle: The name of the field bundle the field is contained in.
* - #field_name: The name of the field.
* - #language: The language the field is being edited in.
* - #field_parents: The 'parents' space for the field in the form. Most
* widgets can simply overlook this property. This identifies the
* location where the field values are placed within
* $form_state['values'], and is used to access processing information
* for the field through the field_form_get_state() and
* field_form_set_state() functions.
* - #columns: A list of field storage columns of the field.
* - #title: The sanitized element label for the field instance, ready for
* output.
* - #description: The sanitized element description for the field instance,
* ready for output.
* - #required: A Boolean indicating whether the element value is required;
* for required multiple value fields, only the first widget's values are
* required.
* - #delta: The order of this item in the array of subelements; see $delta
* above.
*
* @return
* The form elements for a single widget for this field.
*
* @see field_widget_field()
* @see field_widget_instance()
* @see hook_field_widget_form_alter()
* @see hook_field_widget_WIDGET_TYPE_form_alter()
*/
function hook_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
$element += array(
'#type' => $instance['widget']['type'],
'#default_value' => isset($items[$delta]) ? $items[$delta] : '',
);
return array('value' => $element);
}
/**
* Alter forms for field widgets provided by other modules.
*
* @param $element
* The field widget form element as constructed by hook_field_widget_form().
* @param $form_state
* An associative array containing the current state of the form.
* @param $context
* An associative array containing the following key-value pairs, matching the
* arguments received by hook_field_widget_form():
* - form: The form structure to which widgets are being attached. This may be
* a full form structure, or a sub-element of a larger form.
* - field: The field structure.
* - instance: The field instance structure.
* - langcode: The language associated with $items.
* - items: Array of default values for this field.
* - delta: The order of this item in the array of subelements (0, 1, 2, etc).
*
* @see hook_field_widget_form()
* @see hook_field_widget_WIDGET_TYPE_form_alter()
*/
function hook_field_widget_form_alter(&$element, &$form_state, $context) {
// Add a css class to widget form elements for all fields of type mytype.
if ($context['field']['type'] == 'mytype') {
// Be sure not to overwrite existing attributes.
$element['#attributes']['class'][] = 'myclass';
}
}
/**
* Alter widget forms for a specific widget provided by another module.
*
* Modules can implement hook_field_widget_WIDGET_TYPE_form_alter() to modify a
* specific widget form, rather than using hook_field_widget_form_alter() and
* checking the widget type.
*
* @param $element
* The field widget form element as constructed by hook_field_widget_form().
* @param $form_state
* An associative array containing the current state of the form.
* @param $context
* An associative array containing the following key-value pairs, matching the
* arguments received by hook_field_widget_form():
* - "form": The form structure where widgets are being attached to. This
* might be a full form structure, or a sub-element of a larger form.
* - "field": The field structure.
* - "instance": The field instance structure.
* - "langcode": The language associated with $items.
* - "items": Array of default values for this field.
* - "delta": The order of this item in the array of subelements (0, 1, 2,
* etc).
*
* @see hook_field_widget_form()
* @see hook_field_widget_form_alter()
*/
function hook_field_widget_WIDGET_TYPE_form_alter(&$element, &$form_state, $context) {
// Code here will only act on widgets of type WIDGET_TYPE. For example,
// hook_field_widget_mymodule_autocomplete_form_alter() will only act on
// widgets of type 'mymodule_autocomplete'.
$element['#autocomplete_path'] = 'mymodule/autocomplete_path';
}
/**
* Alters the widget properties of a field instance before it gets displayed.
*
* Note that instead of hook_field_widget_properties_alter(), which is called
* for all fields on all entity types,
* hook_field_widget_properties_ENTITY_TYPE_alter() may be used to alter widget
* properties for fields on a specific entity type only.
*
* This hook is called once per field per added or edit entity. If the result
* of the hook involves reading from the database, it is highly recommended to
* statically cache the information.
*
* @param $widget
* The instance's widget properties.
* @param $context
* An associative array containing:
* - entity_type: The entity type; e.g., 'node' or 'user'.
* - entity: The entity object.
* - field: The field that the widget belongs to.
* - instance: The instance of the field.
*
* @see hook_field_widget_properties_ENTITY_TYPE_alter()
*/
function hook_field_widget_properties_alter(&$widget, $context) {
// Change a widget's type according to the time of day.
$field = $context['field'];
if ($context['entity_type'] == 'node' && $field['field_name'] == 'field_foo') {
$time = date('H');
$widget['type'] = $time < 12 ? 'widget_am' : 'widget_pm';
}
}
/**
* Flag a field-level validation error.
*
* @param $element
* An array containing the form element for the widget. The error needs to be
* flagged on the right sub-element, according to the widget's internal
* structure.
* @param $error
* An associative array with the following key-value pairs, as returned by
* hook_field_validate():
* - error: the error code. Complex widgets might need to report different
* errors to different form elements inside the widget.
* - message: the human readable message to be displayed.
* @param $form
* The form structure where field elements are attached to. This might be a
* full form structure, or a sub-element of a larger form.
* @param $form_state
* An associative array containing the current state of the form.
*/
function hook_field_widget_error($element, $error, $form, &$form_state) {
form_error($element, $error['message']);
}
/**
* @} End of "defgroup field_widget".
*/
/**
* @defgroup field_formatter Field Formatter API
* @{
* Define Field API formatter types.
*
* Field API formatters specify how fields are displayed when the entity to
* which the field is attached is displayed. Fields of a given
* @link field_types field type @endlink may be displayed using more than one
* formatter. In this case, the Field UI module allows the site builder to
* choose which formatter to use. Field formatters are defined by implementing
* hook_field_formatter_info().
*
* @see field
* @see field_types
* @see field_widget
*/
/**
* Expose Field API formatter types.
*
* Formatters handle the display of field values. Formatter hooks are typically
* called by the Field Attach API field_attach_prepare_view() and
* field_attach_view() functions.
*
* @return
* An array describing the formatter types implemented by the module.
* The keys are formatter type names. To avoid name clashes, formatter type
* names should be prefixed with the name of the module that exposes them.
* The values are arrays describing the formatter type, with the following
* key/value pairs:
* - label: The human-readable name of the formatter type.
* - description: A short description for the formatter type.
* - field types: An array of field types the formatter supports.
* - settings: An array whose keys are the names of the settings available
* for the formatter type, and whose values are the default values for
* those settings.
*
* @see hook_field_formatter_info_alter()
* @see hook_field_formatter_view()
* @see hook_field_formatter_prepare_view()
*/
function hook_field_formatter_info() {
return array(
'text_default' => array(
'label' => t('Default'),
'field types' => array('text', 'text_long', 'text_with_summary'),
),
'text_plain' => array(
'label' => t('Plain text'),
'field types' => array('text', 'text_long', 'text_with_summary'),
),
// The text_trimmed formatter displays the trimmed version of the
// full element of the field. It is intended to be used with text
// and text_long fields. It also works with text_with_summary
// fields though the text_summary_or_trimmed formatter makes more
// sense for that field type.
'text_trimmed' => array(
'label' => t('Trimmed'),
'field types' => array('text', 'text_long', 'text_with_summary'),
),
// The 'summary or trimmed' field formatter for text_with_summary
// fields displays returns the summary element of the field or, if
// the summary is empty, the trimmed version of the full element
// of the field.
'text_summary_or_trimmed' => array(
'label' => t('Summary or trimmed'),
'field types' => array('text_with_summary'),
),
);
}
/**
* Perform alterations on Field API formatter types.
*
* @param $info
* An array of information on formatter types exposed by
* hook_field_formatter_info() implementations.
*/
function hook_field_formatter_info_alter(&$info) {
// Add a setting to a formatter type.
$info['text_default']['settings'] += array(
'mymodule_additional_setting' => 'default value',
);
// Let a new field type re-use an existing formatter.
$info['text_default']['field types'][] = 'my_field_type';
}
/**
* Allow formatters to load information for field values being displayed.
*
* This should be used when a formatter needs to load additional information
* from the database in order to render a field, for example a reference field
* which displays properties of the referenced entities such as name or type.
*
* This hook is called after the field type's own hook_field_prepare_view().
*
* Unlike most other field hooks, this hook operates on multiple entities. The
* $entities, $instances and $items parameters are arrays keyed by entity ID.
* For performance reasons, information for all available entities should be
* loaded in a single query where possible.
*
* @param $entity_type
* The type of $entity.
* @param $entities
* Array of entities being displayed, keyed by entity ID.
* @param $field
* The field structure for the operation.
* @param $instances
* Array of instance structures for $field for each entity, keyed by entity
* ID.
* @param $langcode
* The language the field values are to be shown in. If no language is
* provided the current language is used.
* @param $items
* Array of field values for the entities, keyed by entity ID.
* @param $displays
* Array of display settings to use for each entity, keyed by entity ID.
*
* @return
* Changes or additions to field values are done by altering the $items
* parameter by reference.
*/
function hook_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
$tids = array();
// Collect every possible term attached to any of the fieldable entities.
foreach ($entities as $id => $entity) {
foreach ($items[$id] as $delta => $item) {
// Force the array key to prevent duplicates.
$tids[$item['tid']] = $item['tid'];
}
}
if ($tids) {
$terms = taxonomy_term_load_multiple($tids);
// Iterate through the fieldable entities again to attach the loaded term
// data.
foreach ($entities as $id => $entity) {
$rekey = FALSE;
foreach ($items[$id] as $delta => $item) {
// Check whether the taxonomy term field instance value could be loaded.
if (isset($terms[$item['tid']])) {
// Replace the instance value with the term data.
$items[$id][$delta]['taxonomy_term'] = $terms[$item['tid']];
}
// Otherwise, unset the instance value, since the term does not exist.
else {
unset($items[$id][$delta]);
$rekey = TRUE;
}
}
if ($rekey) {
// Rekey the items array.
$items[$id] = array_values($items[$id]);
}
}
}
}
/**
* Build a renderable array for a field value.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity being displayed.
* @param $field
* The field structure.
* @param $instance
* The field instance.
* @param $langcode
* The language associated with $items.
* @param $items
* Array of values for this field.
* @param $display
* The display settings to use, as found in the 'display' entry of instance
* definitions. The array notably contains the following keys and values;
* - type: The name of the formatter to use.
* - settings: The array of formatter settings.
*
* @return
* A renderable array for the $items, as an array of child elements keyed
* by numeric indexes starting from 0.
*/
function hook_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$element = array();
$settings = $display['settings'];
switch ($display['type']) {
case 'sample_field_formatter_simple':
// Common case: each value is displayed individually in a sub-element
// keyed by delta. The field.tpl.php template specifies the markup
// wrapping each value.
foreach ($items as $delta => $item) {
$element[$delta] = array('#markup' => $settings['some_setting'] . $item['value']);
}
break;
case 'sample_field_formatter_themeable':
// More elaborate formatters can defer to a theme function for easier
// customization.
foreach ($items as $delta => $item) {
$element[$delta] = array(
'#theme' => 'mymodule_theme_sample_field_formatter_themeable',
'#data' => $item['value'],
'#some_setting' => $settings['some_setting'],
);
}
break;
case 'sample_field_formatter_combined':
// Some formatters might need to display all values within a single piece
// of markup.
$rows = array();
foreach ($items as $delta => $item) {
$rows[] = array($delta, $item['value']);
}
$element[0] = array(
'#theme' => 'table',
'#header' => array(t('Delta'), t('Value')),
'#rows' => $rows,
);
break;
}
return $element;
}
/**
* @} End of "defgroup field_formatter".
*/
/**
* @addtogroup field_attach
* @{
*/
/**
* Act on field_attach_form().
*
* This hook is invoked after the field module has performed the operation.
* Implementing modules should alter the $form or $form_state parameters.
*
* @param $entity_type
* The type of $entity; for example, 'node' or 'user'.
* @param $entity
* The entity for which an edit form is being built.
* @param $form
* The form structure where field elements are attached to. This might be a
* full form structure, or a sub-element of a larger form. The
* $form['#parents'] property can be used to identify the corresponding part
* of $form_state['values']. Hook implementations that need to act on the
* top-level properties of the global form (like #submit, #validate...) can
* add a #process callback to the array received in the $form parameter, and
* act on the $complete_form parameter in the process callback.
* @param $form_state
* An associative array containing the current state of the form.
* @param $langcode
* The language the field values are going to be entered in. If no language
* is provided the default site language will be used.
*/
function hook_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) {
// Add a checkbox allowing a given field to be emptied.
// See hook_field_attach_submit() for the corresponding processing code.
$form['empty_field_foo'] = array(
'#type' => 'checkbox',
'#title' => t("Empty the 'field_foo' field"),
);
}
/**
* Act on field_attach_load().
*
* This hook is invoked after the field module has performed the operation.
*
* Unlike other field_attach hooks, this hook accounts for 'multiple loads'.
* Instead of the usual $entity parameter, it accepts an array of entities,
* indexed by entity ID. For performance reasons, information for all available
* entities should be loaded in a single query where possible.
*
* The changes made to the entities' field values get cached by the field cache
* for subsequent loads.
*
* See field_attach_load() for details and arguments.
*/
function hook_field_attach_load($entity_type, $entities, $age, $options) {
// @todo Needs function body.
}
/**
* Act on field_attach_validate().
*
* This hook is invoked after the field module has performed the operation.
*
* See field_attach_validate() for details and arguments.
*
* @param $entity_type
* The type of $entity; e.g., 'node' or 'user'.
* @param $entity
* The entity with fields to validate.
* @param array $errors
* The array of errors (keyed by field name, language code, and delta) that
* have already been reported for the entity. The function should add its
* errors to this array. Each error is an associative array with the following
* keys and values:
* - error: An error code (should be a string prefixed with the module name).
* - message: The human readable message to be displayed.
*/
function hook_field_attach_validate($entity_type, $entity, &$errors) {
// Make sure any images in article nodes have an alt text.
if ($entity_type == 'node' && $entity->type == 'article' && !empty($entity->field_image)) {
foreach ($entity->field_image as $langcode => $items) {
foreach ($items as $delta => $item) {
if (!empty($item['fid']) && empty($item['alt'])) {
$errors['field_image'][$langcode][$delta][] = array(
'error' => 'field_example_invalid',
'message' => t('All images in articles need to have an alternative text set.'),
);
}
}
}
}
}
/**
* Act on field_attach_submit().
*
* This hook is invoked after the field module has performed the operation.
*
* @param $entity_type
* The type of $entity; for example, 'node' or 'user'.
* @param $entity
* The entity for which an edit form is being submitted. The incoming form
* values have been extracted as field values of the $entity object.
* @param $form
* The form structure where field elements are attached to. This might be a
* full form structure, or a sub-part of a larger form. The $form['#parents']
* property can be used to identify the corresponding part of
* $form_state['values'].
* @param $form_state
* An associative array containing the current state of the form.
*/
function hook_field_attach_submit($entity_type, $entity, $form, &$form_state) {
// Sample case of an 'Empty the field' checkbox added on the form, allowing
// a given field to be emptied.
$values = drupal_array_get_nested_value($form_state['values'], $form['#parents']);
if (!empty($values['empty_field_foo'])) {
unset($entity->field_foo);
}
}
/**
* Act on field_attach_presave().
*
* This hook is invoked after the field module has performed the operation.
*
* See field_attach_presave() for details and arguments.
*/
function hook_field_attach_presave($entity_type, $entity) {
// @todo Needs function body.
}
/**
* Act on field_attach_insert().
*
* This hook is invoked after the field module has performed the operation.
*
* See field_attach_insert() for details and arguments.
*/
function hook_field_attach_insert($entity_type, $entity) {
// @todo Needs function body.
}
/**
* Act on field_attach_update().
*
* This hook is invoked after the field module has performed the operation.
*
* See field_attach_update() for details and arguments.
*/
function hook_field_attach_update($entity_type, $entity) {
// @todo Needs function body.
}
/**
* Alter field_attach_preprocess() variables.
*
* This hook is invoked while preprocessing the field.tpl.php template file
* in field_attach_preprocess().
*
* @param $variables
* The variables array is passed by reference and will be populated with field
* values.
* @param $context
* An associative array containing:
* - entity_type: The type of $entity; for example, 'node' or 'user'.
* - entity: The entity with fields to render.
* - element: The structured array containing the values ready for rendering.
*/
function hook_field_attach_preprocess_alter(&$variables, $context) {
// @todo Needs function body.
}
/**
* Act on field_attach_delete().
*
* This hook is invoked after the field module has performed the operation.
*
* See field_attach_delete() for details and arguments.
*/
function hook_field_attach_delete($entity_type, $entity) {
// @todo Needs function body.
}
/**
* Act on field_attach_delete_revision().
*
* This hook is invoked after the field module has performed the operation.
*
* See field_attach_delete_revision() for details and arguments.
*/
function hook_field_attach_delete_revision($entity_type, $entity) {
// @todo Needs function body.
}
/**
* Act on field_purge_data().
*
* This hook is invoked in field_purge_data() and allows modules to act on
* purging data from a single field pseudo-entity. For example, if a module
* relates data in the field with its own data, it may purge its own data
* during this process as well.
*
* @param $entity_type
* The type of $entity; for example, 'node' or 'user'.
* @param $entity
* The pseudo-entity whose field data is being purged.
* @param $field
* The (possibly deleted) field whose data is being purged.
* @param $instance
* The deleted field instance whose data is being purged.
*
* @see @link field_purge Field API bulk data deletion @endlink
* @see field_purge_data()
*/
function hook_field_attach_purge($entity_type, $entity, $field, $instance) {
// find the corresponding data in mymodule and purge it
if ($entity_type == 'node' && $field->field_name == 'my_field_name') {
mymodule_remove_mydata($entity->nid);
}
}
/**
* Perform alterations on field_attach_view() or field_view_field().
*
* This hook is invoked after the field module has performed the operation.
*
* @param $output
* The structured content array tree for all of the entity's fields.
* @param $context
* An associative array containing:
* - entity_type: The type of $entity; for example, 'node' or 'user'.
* - entity: The entity with fields to render.
* - view_mode: View mode; for example, 'full' or 'teaser'.
* - display: Either a view mode string or an array of display settings. If
* this hook is being invoked from field_attach_view(), the 'display'
* element is set to the view mode string. If this hook is being invoked
* from field_view_field(), this element is set to the $display argument
* and the view_mode element is set to '_custom'. See field_view_field()
* for more information on what its $display argument contains.
* - language: The language code used for rendering.
*/
function hook_field_attach_view_alter(&$output, $context) {
// Append RDF term mappings on displayed taxonomy links.
foreach (element_children($output) as $field_name) {
$element = &$output[$field_name];
if ($element['#field_type'] == 'taxonomy_term_reference' && $element['#formatter'] == 'taxonomy_term_reference_link') {
foreach ($element['#items'] as $delta => $item) {
$term = $item['taxonomy_term'];
if (!empty($term->rdf_mapping['rdftype'])) {
$element[$delta]['#options']['attributes']['typeof'] = $term->rdf_mapping['rdftype'];
}
if (!empty($term->rdf_mapping['name']['predicates'])) {
$element[$delta]['#options']['attributes']['property'] = $term->rdf_mapping['name']['predicates'];
}
}
}
}
}
/**
* Perform alterations on field_attach_prepare_translation().
*
* This hook is invoked after the field module has performed the operation.
*
* @param $entity
* The entity being prepared for translation.
* @param $context
* An associative array containing:
* - entity_type: The type of $entity; e.g. 'node' or 'user'.
* - langcode: The language the entity has to be translated in.
* - source_entity: The entity holding the field values to be translated.
* - source_langcode: The source language from which translate.
*/
function hook_field_attach_prepare_translation_alter(&$entity, $context) {
if ($context['entity_type'] == 'custom_entity_type') {
$entity->custom_field = $context['source_entity']->custom_field;
}
}
/**
* Perform alterations on field_language() values.
*
* This hook is invoked to alter the array of display languages for the given
* entity.
*
* @param $display_language
* A reference to an array of language codes keyed by field name.
* @param $context
* An associative array containing:
* - entity_type: The type of the entity to be displayed.
* - entity: The entity with fields to render.
* - langcode: The language code $entity has to be displayed in.
*
* @ingroup field_language
*/
function hook_field_language_alter(&$display_language, $context) {
// Do not apply core language fallback rules if they are disabled or if Locale
// is not registered as a translation handler.
if (variable_get('locale_field_language_fallback', TRUE) && field_has_translation_handler($context['entity_type'], 'locale')) {
locale_field_language_fallback($display_language, $context['entity'], $context['language']);
}
}
/**
* Alter field_available_languages() values.
*
* This hook is invoked from field_available_languages() to allow modules to
* alter the array of available languages for the given field.
*
* @param $languages
* A reference to an array of language codes to be made available.
* @param $context
* An associative array containing:
* - entity_type: The type of the entity the field is attached to.
* - field: A field data structure.
*
* @ingroup field_language
*/
function hook_field_available_languages_alter(&$languages, $context) {
// Add an unavailable language.
$languages[] = 'xx';
// Remove an available language.
$index = array_search('yy', $languages);
unset($languages[$index]);
}
/**
* Act on field_attach_create_bundle().
*
* This hook is invoked after the field module has performed the operation.
*
* See field_attach_create_bundle() for details and arguments.
*/
function hook_field_attach_create_bundle($entity_type, $bundle) {
// When a new bundle is created, the menu needs to be rebuilt to add the
// Field UI menu item tabs.
variable_set('menu_rebuild_needed', TRUE);
}
/**
* Act on field_attach_rename_bundle().
*
* This hook is invoked after the field module has performed the operation.
*
* See field_attach_rename_bundle() for details and arguments.
*/
function hook_field_attach_rename_bundle($entity_type, $bundle_old, $bundle_new) {
// Update the extra weights variable with new information.
if ($bundle_old !== $bundle_new) {
$extra_weights = variable_get('field_extra_weights', array());
if (isset($info[$entity_type][$bundle_old])) {
$extra_weights[$entity_type][$bundle_new] = $extra_weights[$entity_type][$bundle_old];
unset($extra_weights[$entity_type][$bundle_old]);
variable_set('field_extra_weights', $extra_weights);
}
}
}
/**
* Act on field_attach_delete_bundle.
*
* This hook is invoked after the field module has performed the operation.
*
* @param $entity_type
* The type of entity; for example, 'node' or 'user'.
* @param $bundle
* The name of the bundle that was just deleted.
* @param $instances
* An array of all instances that existed for the bundle before it was
* deleted.
*/
function hook_field_attach_delete_bundle($entity_type, $bundle, $instances) {
// Remove the extra weights variable information for this bundle.
$extra_weights = variable_get('field_extra_weights', array());
if (isset($extra_weights[$entity_type][$bundle])) {
unset($extra_weights[$entity_type][$bundle]);
variable_set('field_extra_weights', $extra_weights);
}
}
/**
* @} End of "addtogroup field_attach".
*/
/**
* @addtogroup field_storage
* @{
*/
/**
* Expose Field API storage backends.
*
* @return
* An array describing the storage backends implemented by the module.
* The keys are storage backend names. To avoid name clashes, storage backend
* names should be prefixed with the name of the module that exposes them.
* The values are arrays describing the storage backend, with the following
* key/value pairs:
* - label: The human-readable name of the storage backend.
* - description: A short description for the storage backend.
* - settings: An array whose keys are the names of the settings available
* for the storage backend, and whose values are the default values for
* those settings.
*/
function hook_field_storage_info() {
return array(
'field_sql_storage' => array(
'label' => t('Default SQL storage'),
'description' => t('Stores fields in the local SQL database, using per-field tables.'),
'settings' => array(),
),
);
}
/**
* Perform alterations on Field API storage types.
*
* @param $info
* An array with information on storage types returned by
* hook_field_storage_info() implementations.
*/
function hook_field_storage_info_alter(&$info) {
// Add a setting to a storage type.
$info['field_sql_storage']['settings'] += array(
'mymodule_additional_setting' => 'default value',
);
}
/**
* Reveal the internal details about the storage for a field.
*
* For example, an SQL storage module might return the Schema API structure for
* the table. A key/value storage module might return the server name,
* authentication credentials, and bin name.
*
* Field storage modules are not obligated to implement this hook. Modules
* that rely on these details must only use them for read operations.
*
* @param $field
* A field structure.
*
* @return
* An array of details.
* - The first dimension is a store type (sql, solr, etc).
* - The second dimension indicates the age of the values in the store
* FIELD_LOAD_CURRENT or FIELD_LOAD_REVISION.
* - Other dimensions are specific to the field storage module.
*
* @see hook_field_storage_details_alter()
*/
function hook_field_storage_details($field) {
$details = array();
// Add field columns.
foreach ((array) $field['columns'] as $column_name => $attributes) {
$real_name = _field_sql_storage_columnname($field['field_name'], $column_name);
$columns[$column_name] = $real_name;
}
return array(
'sql' => array(
FIELD_LOAD_CURRENT => array(
_field_sql_storage_tablename($field) => $columns,
),
FIELD_LOAD_REVISION => array(
_field_sql_storage_revision_tablename($field) => $columns,
),
),
);
}
/**
* Perform alterations on Field API storage details.
*
* @param $details
* An array of storage details for fields as exposed by
* hook_field_storage_details() implementations.
* @param $field
* A field structure.
*
* @see hook_field_storage_details()
*/
function hook_field_storage_details_alter(&$details, $field) {
if ($field['field_name'] == 'field_of_interest') {
$columns = array();
foreach ((array) $field['columns'] as $column_name => $attributes) {
$columns[$column_name] = $column_name;
}
$details['drupal_variables'] = array(
FIELD_LOAD_CURRENT => array(
'moon' => $columns,
),
FIELD_LOAD_REVISION => array(
'mars' => $columns,
),
);
}
}
/**
* Load field data for a set of entities.
*
* This hook is invoked from field_attach_load() to ask the field storage
* module to load field data.
*
* Modules implementing this hook should load field values and add them to
* objects in $entities. Fields with no values should be added as empty
* arrays.
*
* @param $entity_type
* The type of entity, such as 'node' or 'user'.
* @param $entities
* The array of entity objects to add fields to, keyed by entity ID.
* @param $age
* FIELD_LOAD_CURRENT to load the most recent revision for all fields, or
* FIELD_LOAD_REVISION to load the version indicated by each entity.
* @param $fields
* An array listing the fields to be loaded. The keys of the array are field
* IDs, and the values of the array are the entity IDs (or revision IDs,
* depending on the $age parameter) to add each field to.
* @param $options
* An associative array of additional options, with the following keys:
* - deleted: If TRUE, deleted fields should be loaded as well as
* non-deleted fields. If unset or FALSE, only non-deleted fields should be
* loaded.
*/
function hook_field_storage_load($entity_type, $entities, $age, $fields, $options) {
$load_current = $age == FIELD_LOAD_CURRENT;
foreach ($fields as $field_id => $ids) {
// By the time this hook runs, the relevant field definitions have been
// populated and cached in FieldInfo, so calling field_info_field_by_id()
// on each field individually is more efficient than loading all fields in
// memory upfront with field_info_field_by_ids().
$field = field_info_field_by_id($field_id);
$field_name = $field['field_name'];
$table = $load_current ? _field_sql_storage_tablename($field) : _field_sql_storage_revision_tablename($field);
$query = db_select($table, 't')
->fields('t')
->condition('entity_type', $entity_type)
->condition($load_current ? 'entity_id' : 'revision_id', $ids, 'IN')
->condition('language', field_available_languages($entity_type, $field), 'IN')
->orderBy('delta');
if (empty($options['deleted'])) {
$query->condition('deleted', 0);
}
$results = $query->execute();
$delta_count = array();
foreach ($results as $row) {
if (!isset($delta_count[$row->entity_id][$row->language])) {
$delta_count[$row->entity_id][$row->language] = 0;
}
if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$row->language] < $field['cardinality']) {
$item = array();
// For each column declared by the field, populate the item
// from the prefixed database column.
foreach ($field['columns'] as $column => $attributes) {
$column_name = _field_sql_storage_columnname($field_name, $column);
$item[$column] = $row->$column_name;
}
// Add the item to the field values for the entity.
$entities[$row->entity_id]->{$field_name}[$row->language][] = $item;
$delta_count[$row->entity_id][$row->language]++;
}
}
}
}
/**
* Write field data for an entity.
*
* This hook is invoked from field_attach_insert() and field_attach_update(),
* to ask the field storage module to save field data.
*
* @param $entity_type
* The entity type of entity, such as 'node' or 'user'.
* @param $entity
* The entity on which to operate.
* @param $op
* FIELD_STORAGE_UPDATE when updating an existing entity,
* FIELD_STORAGE_INSERT when inserting a new entity.
* @param $fields
* An array listing the fields to be written. The keys and values of the
* array are field IDs.
*/
function hook_field_storage_write($entity_type, $entity, $op, $fields) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
if (!isset($vid)) {
$vid = $id;
}
foreach ($fields as $field_id) {
$field = field_info_field_by_id($field_id);
$field_name = $field['field_name'];
$table_name = _field_sql_storage_tablename($field);
$revision_name = _field_sql_storage_revision_tablename($field);
$all_languages = field_available_languages($entity_type, $field);
$field_languages = array_intersect($all_languages, array_keys((array) $entity->$field_name));
// Delete and insert, rather than update, in case a value was added.
if ($op == FIELD_STORAGE_UPDATE) {
// Delete languages present in the incoming $entity->$field_name.
// Delete all languages if $entity->$field_name is empty.
$languages = !empty($entity->$field_name) ? $field_languages : $all_languages;
if ($languages) {
db_delete($table_name)
->condition('entity_type', $entity_type)
->condition('entity_id', $id)
->condition('language', $languages, 'IN')
->execute();
db_delete($revision_name)
->condition('entity_type', $entity_type)
->condition('entity_id', $id)
->condition('revision_id', $vid)
->condition('language', $languages, 'IN')
->execute();
}
}
// Prepare the multi-insert query.
$do_insert = FALSE;
$columns = array('entity_type', 'entity_id', 'revision_id', 'bundle', 'delta', 'language');
foreach ($field['columns'] as $column => $attributes) {
$columns[] = _field_sql_storage_columnname($field_name, $column);
}
$query = db_insert($table_name)->fields($columns);
$revision_query = db_insert($revision_name)->fields($columns);
foreach ($field_languages as $langcode) {
$items = (array) $entity->{$field_name}[$langcode];
$delta_count = 0;
foreach ($items as $delta => $item) {
// We now know we have something to insert.
$do_insert = TRUE;
$record = array(
'entity_type' => $entity_type,
'entity_id' => $id,
'revision_id' => $vid,
'bundle' => $bundle,
'delta' => $delta,
'language' => $langcode,
);
foreach ($field['columns'] as $column => $attributes) {
$record[_field_sql_storage_columnname($field_name, $column)] = isset($item[$column]) ? $item[$column] : NULL;
}
$query->values($record);
if (isset($vid)) {
$revision_query->values($record);
}
if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) {
break;
}
}
}
// Execute the query if we have values to insert.
if ($do_insert) {
$query->execute();
$revision_query->execute();
}
}
}
/**
* Delete all field data for an entity.
*
* This hook is invoked from field_attach_delete() to ask the field storage
* module to delete field data.
*
* @param $entity_type
* The entity type of entity, such as 'node' or 'user'.
* @param $entity
* The entity on which to operate.
* @param $fields
* An array listing the fields to delete. The keys and values of the
* array are field IDs.
*/
function hook_field_storage_delete($entity_type, $entity, $fields) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
foreach (field_info_instances($entity_type, $bundle) as $instance) {
if (isset($fields[$instance['field_id']])) {
$field = field_info_field_by_id($instance['field_id']);
field_sql_storage_field_storage_purge($entity_type, $entity, $field, $instance);
}
}
}
/**
* Delete a single revision of field data for an entity.
*
* This hook is invoked from field_attach_delete_revision() to ask the field
* storage module to delete field revision data.
*
* Deleting the current (most recently written) revision is not
* allowed as has undefined results.
*
* @param $entity_type
* The entity type of entity, such as 'node' or 'user'.
* @param $entity
* The entity on which to operate. The revision to delete is
* indicated by the entity's revision ID property, as identified by
* hook_fieldable_info() for $entity_type.
* @param $fields
* An array listing the fields to delete. The keys and values of the
* array are field IDs.
*/
function hook_field_storage_delete_revision($entity_type, $entity, $fields) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
if (isset($vid)) {
foreach ($fields as $field_id) {
$field = field_info_field_by_id($field_id);
$revision_name = _field_sql_storage_revision_tablename($field);
db_delete($revision_name)
->condition('entity_type', $entity_type)
->condition('entity_id', $id)
->condition('revision_id', $vid)
->execute();
}
}
}
/**
* Execute an EntityFieldQuery.
*
* This hook is called to find the entities having certain entity and field
* conditions and sort them in the given field order. If the field storage
* engine also handles property sorts and orders, it should unset those
* properties in the called object to signal that those have been handled.
*
* @param EntityFieldQuery $query
* An EntityFieldQuery.
*
* @return
* See EntityFieldQuery::execute() for the return values.
*/
function hook_field_storage_query($query) {
$groups = array();
if ($query->age == FIELD_LOAD_CURRENT) {
$tablename_function = '_field_sql_storage_tablename';
$id_key = 'entity_id';
}
else {
$tablename_function = '_field_sql_storage_revision_tablename';
$id_key = 'revision_id';
}
$table_aliases = array();
// Add tables for the fields used.
foreach ($query->fields as $key => $field) {
$tablename = $tablename_function($field);
// Every field needs a new table.
$table_alias = $tablename . $key;
$table_aliases[$key] = $table_alias;
if ($key) {
$select_query->join($tablename, $table_alias, "$table_alias.entity_type = $field_base_table.entity_type AND $table_alias.$id_key = $field_base_table.$id_key");
}
else {
$select_query = db_select($tablename, $table_alias);
$select_query->addTag('entity_field_access');
$select_query->addMetaData('base_table', $tablename);
$select_query->fields($table_alias, array('entity_type', 'entity_id', 'revision_id', 'bundle'));
$field_base_table = $table_alias;
}
if ($field['cardinality'] != 1) {
$select_query->distinct();
}
}
// Add field conditions.
foreach ($query->fieldConditions as $key => $condition) {
$table_alias = $table_aliases[$key];
$field = $condition['field'];
// Add the specified condition.
$sql_field = "$table_alias." . _field_sql_storage_columnname($field['field_name'], $condition['column']);
$query->addCondition($select_query, $sql_field, $condition);
// Add delta / language group conditions.
foreach (array('delta', 'language') as $column) {
if (isset($condition[$column . '_group'])) {
$group_name = $condition[$column . '_group'];
if (!isset($groups[$column][$group_name])) {
$groups[$column][$group_name] = $table_alias;
}
else {
$select_query->where("$table_alias.$column = " . $groups[$column][$group_name] . ".$column");
}
}
}
}
if (isset($query->deleted)) {
$select_query->condition("$field_base_table.deleted", (int) $query->deleted);
}
// Is there a need to sort the query by property?
$has_property_order = FALSE;
foreach ($query->order as $order) {
if ($order['type'] == 'property') {
$has_property_order = TRUE;
}
}
if ($query->propertyConditions || $has_property_order) {
if (empty($query->entityConditions['entity_type']['value'])) {
throw new EntityFieldQueryException('Property conditions and orders must have an entity type defined.');
}
$entity_type = $query->entityConditions['entity_type']['value'];
$entity_base_table = _field_sql_storage_query_join_entity($select_query, $entity_type, $field_base_table);
$query->entityConditions['entity_type']['operator'] = '=';
foreach ($query->propertyConditions as $property_condition) {
$query->addCondition($select_query, "$entity_base_table." . $property_condition['column'], $property_condition);
}
}
foreach ($query->entityConditions as $key => $condition) {
$query->addCondition($select_query, "$field_base_table.$key", $condition);
}
// Order the query.
foreach ($query->order as $order) {
if ($order['type'] == 'entity') {
$key = $order['specifier'];
$select_query->orderBy("$field_base_table.$key", $order['direction']);
}
elseif ($order['type'] == 'field') {
$specifier = $order['specifier'];
$field = $specifier['field'];
$table_alias = $table_aliases[$specifier['index']];
$sql_field = "$table_alias." . _field_sql_storage_columnname($field['field_name'], $specifier['column']);
$select_query->orderBy($sql_field, $order['direction']);
}
elseif ($order['type'] == 'property') {
$select_query->orderBy("$entity_base_table." . $order['specifier'], $order['direction']);
}
}
return $query->finishQuery($select_query, $id_key);
}
/**
* Act on creation of a new field.
*
* This hook is invoked from field_create_field() to ask the field storage
* module to save field information and prepare for storing field instances.
* If there is a problem, the field storage module should throw an exception.
*
* @param $field
* The field structure being created.
*/
function hook_field_storage_create_field($field) {
$schema = _field_sql_storage_schema($field);
foreach ($schema as $name => $table) {
db_create_table($name, $table);
}
drupal_get_schema(NULL, TRUE);
}
/**
* Act on deletion of a field.
*
* This hook is invoked from field_delete_field() to ask the field storage
* module to mark all information stored in the field for deletion.
*
* @param $field
* The field being deleted.
*/
function hook_field_storage_delete_field($field) {
// Mark all data associated with the field for deletion.
$field['deleted'] = 0;
$table = _field_sql_storage_tablename($field);
$revision_table = _field_sql_storage_revision_tablename($field);
db_update($table)
->fields(array('deleted' => 1))
->execute();
// Move the table to a unique name while the table contents are being deleted.
$field['deleted'] = 1;
$new_table = _field_sql_storage_tablename($field);
$revision_new_table = _field_sql_storage_revision_tablename($field);
db_rename_table($table, $new_table);
db_rename_table($revision_table, $revision_new_table);
drupal_get_schema(NULL, TRUE);
}
/**
* Act on deletion of a field instance.
*
* This hook is invoked from field_delete_instance() to ask the field storage
* module to mark all information stored for the field instance for deletion.
*
* @param $instance
* The instance being deleted.
*/
function hook_field_storage_delete_instance($instance) {
$field = field_info_field($instance['field_name']);
$table_name = _field_sql_storage_tablename($field);
$revision_name = _field_sql_storage_revision_tablename($field);
db_update($table_name)
->fields(array('deleted' => 1))
->condition('entity_type', $instance['entity_type'])
->condition('bundle', $instance['bundle'])
->execute();
db_update($revision_name)
->fields(array('deleted' => 1))
->condition('entity_type', $instance['entity_type'])
->condition('bundle', $instance['bundle'])
->execute();
}
/**
* Act before the storage backends load field data.
*
* This hook allows modules to load data before the Field Storage API,
* optionally preventing the field storage module from doing so.
*
* This lets 3rd party modules override, mirror, shard, or otherwise store a
* subset of fields in a different way than the current storage engine.
* Possible use cases include per-bundle storage, per-combo-field storage, etc.
*
* Modules implementing this hook should load field values and add them to
* objects in $entities. Fields with no values should be added as empty
* arrays. In addition, fields loaded should be added as keys to $skip_fields.
*
* @param $entity_type
* The type of entity, such as 'node' or 'user'.
* @param $entities
* The array of entity objects to add fields to, keyed by entity ID.
* @param $age
* FIELD_LOAD_CURRENT to load the most recent revision for all fields, or
* FIELD_LOAD_REVISION to load the version indicated by each entity.
* @param $skip_fields
* An array keyed by field IDs whose data has already been loaded and
* therefore should not be loaded again. Add a key to this array to indicate
* that your module has already loaded a field.
* @param $options
* An associative array of additional options, with the following keys:
* - field_id: The field ID that should be loaded. If unset, all fields
* should be loaded.
* - deleted: If TRUE, deleted fields should be loaded as well as
* non-deleted fields. If unset or FALSE, only non-deleted fields should be
* loaded.
*/
function hook_field_storage_pre_load($entity_type, $entities, $age, &$skip_fields, $options) {
// @todo Needs function body.
}
/**
* Act before the storage backends insert field data.
*
* This hook allows modules to store data before the Field Storage API,
* optionally preventing the field storage module from doing so.
*
* @param $entity_type
* The type of $entity; for example, 'node' or 'user'.
* @param $entity
* The entity with fields to save.
* @param $skip_fields
* An array keyed by field IDs whose data has already been written and
* therefore should not be written again. The values associated with these
* keys are not specified.
* @return
* Saved field IDs are set set as keys in $skip_fields.
*/
function hook_field_storage_pre_insert($entity_type, $entity, &$skip_fields) {
if ($entity_type == 'node' && $entity->status && _forum_node_check_node_type($entity)) {
$query = db_insert('forum_index')->fields(array('nid', 'title', 'tid', 'sticky', 'created', 'comment_count', 'last_comment_timestamp'));
foreach ($entity->taxonomy_forums as $language) {
foreach ($language as $delta) {
$query->values(array(
'nid' => $entity->nid,
'title' => $entity->title,
'tid' => $delta['value'],
'sticky' => $entity->sticky,
'created' => $entity->created,
'comment_count' => 0,
'last_comment_timestamp' => $entity->created,
));
}
}
$query->execute();
}
}
/**
* Act before the storage backends update field data.
*
* This hook allows modules to store data before the Field Storage API,
* optionally preventing the field storage module from doing so.
*
* @param $entity_type
* The type of $entity; for example, 'node' or 'user'.
* @param $entity
* The entity with fields to save.
* @param $skip_fields
* An array keyed by field IDs whose data has already been written and
* therefore should not be written again. The values associated with these
* keys are not specified.
* @return
* Saved field IDs are set set as keys in $skip_fields.
*/
function hook_field_storage_pre_update($entity_type, $entity, &$skip_fields) {
$first_call = &drupal_static(__FUNCTION__, array());
if ($entity_type == 'node' && $entity->status && _forum_node_check_node_type($entity)) {
// We don't maintain data for old revisions, so clear all previous values
// from the table. Since this hook runs once per field, per entity, make
// sure we only wipe values once.
if (!isset($first_call[$entity->nid])) {
$first_call[$entity->nid] = FALSE;
db_delete('forum_index')->condition('nid', $entity->nid)->execute();
}
// Only save data to the table if the node is published.
if ($entity->status) {
$query = db_insert('forum_index')->fields(array('nid', 'title', 'tid', 'sticky', 'created', 'comment_count', 'last_comment_timestamp'));
foreach ($entity->taxonomy_forums as $language) {
foreach ($language as $delta) {
$query->values(array(
'nid' => $entity->nid,
'title' => $entity->title,
'tid' => $delta['value'],
'sticky' => $entity->sticky,
'created' => $entity->created,
'comment_count' => 0,
'last_comment_timestamp' => $entity->created,
));
}
}
$query->execute();
// The logic for determining last_comment_count is fairly complex, so
// call _forum_update_forum_index() too.
_forum_update_forum_index($entity->nid);
}
}
}
/**
* @} End of "addtogroup field_storage
*/
/**
* Returns the maximum weight for the entity components handled by the module.
*
* Field API takes care of fields and 'extra_fields'. This hook is intended for
* third-party modules adding other entity components (e.g. field_group).
*
* @param $entity_type
* The type of entity; e.g. 'node' or 'user'.
* @param $bundle
* The bundle name.
* @param $context
* The context for which the maximum weight is requested. Either 'form', or
* the name of a view mode.
*
* @return
* The maximum weight of the entity's components, or NULL if no components
* were found.
*
* @ingroup field_info
*/
function hook_field_info_max_weight($entity_type, $bundle, $context) {
$weights = array();
foreach (my_module_entity_additions($entity_type, $bundle, $context) as $addition) {
$weights[] = $addition['weight'];
}
return $weights ? max($weights) : NULL;
}
/**
* @addtogroup field_types
* @{
*/
/**
* Alters the display settings of a field before it gets displayed.
*
* Note that instead of hook_field_display_alter(), which is called for all
* fields on all entity types, hook_field_display_ENTITY_TYPE_alter() may be
* used to alter display settings for fields on a specific entity type only.
*
* This hook is called once per field per displayed entity. If the result of the
* hook involves reading from the database, it is highly recommended to
* statically cache the information.
*
* @param $display
* The display settings that will be used to display the field values, as
* found in the 'display' key of $instance definitions.
* @param $context
* An associative array containing:
* - entity_type: The entity type; e.g., 'node' or 'user'.
* - field: The field being rendered.
* - instance: The instance being rendered.
* - entity: The entity being rendered.
* - view_mode: The view mode, e.g. 'full', 'teaser'...
*
* @see hook_field_display_ENTITY_TYPE_alter()
*/
function hook_field_display_alter(&$display, $context) {
// Leave field labels out of the search index.
// Note: The check against $context['entity_type'] == 'node' could be avoided
// by using hook_field_display_node_alter() instead of
// hook_field_display_alter(), resulting in less function calls when
// rendering non-node entities.
if ($context['entity_type'] == 'node' && $context['view_mode'] == 'search_index') {
$display['label'] = 'hidden';
}
}
/**
* Alters the display settings of a field on a given entity type before it gets displayed.
*
* Modules can implement hook_field_display_ENTITY_TYPE_alter() to alter display
* settings for fields on a specific entity type, rather than implementing
* hook_field_display_alter().
*
* This hook is called once per field per displayed entity. If the result of the
* hook involves reading from the database, it is highly recommended to
* statically cache the information.
*
* @param $display
* The display settings that will be used to display the field values, as
* found in the 'display' key of $instance definitions.
* @param $context
* An associative array containing:
* - entity_type: The entity type; e.g., 'node' or 'user'.
* - field: The field being rendered.
* - instance: The instance being rendered.
* - entity: The entity being rendered.
* - view_mode: The view mode, e.g. 'full', 'teaser'...
*
* @see hook_field_display_alter()
*/
function hook_field_display_ENTITY_TYPE_alter(&$display, $context) {
// Leave field labels out of the search index.
if ($context['view_mode'] == 'search_index') {
$display['label'] = 'hidden';
}
}
/**
* @} End of "addtogroup field_types
*/
/**
* Alters the display settings of pseudo-fields before an entity is displayed.
*
* This hook is called once per displayed entity. If the result of the hook
* involves reading from the database, it is highly recommended to statically
* cache the information.
*
* @param $displays
* An array of display settings for the pseudo-fields in the entity, keyed
* by pseudo-field names.
* @param $context
* An associative array containing:
* - entity_type: The entity type; e.g., 'node' or 'user'.
* - bundle: The bundle name.
* - view_mode: The view mode, e.g. 'full', 'teaser'...
*
* @ingroup field_types
*/
function hook_field_extra_fields_display_alter(&$displays, $context) {
if ($context['entity_type'] == 'taxonomy_term' && $context['view_mode'] == 'full') {
$displays['description']['visible'] = FALSE;
}
}
/**
* Alters the widget properties of a field instance on a given entity type
* before it gets displayed.
*
* Modules can implement hook_field_widget_properties_ENTITY_TYPE_alter() to
* alter the widget properties for fields on a specific entity type, rather than
* implementing hook_field_widget_properties_alter().
*
* This hook is called once per field per displayed widget entity. If the result
* of the hook involves reading from the database, it is highly recommended to
* statically cache the information.
*
* @param $widget
* The instance's widget properties.
* @param $context
* An associative array containing:
* - entity_type: The entity type; e.g., 'node' or 'user'.
* - entity: The entity object.
* - field: The field that the widget belongs to.
* - instance: The instance of the field.
*
* @see hook_field_widget_properties_alter()
*
* @ingroup field_widget
*/
function hook_field_widget_properties_ENTITY_TYPE_alter(&$widget, $context) {
// Change a widget's type according to the time of day.
$field = $context['field'];
if ($field['field_name'] == 'field_foo') {
$time = date('H');
$widget['type'] = $time < 12 ? 'widget_am' : 'widget_pm';
}
}
/**
* @addtogroup field_crud
* @{
*/
/**
* Act on a field being created.
*
* This hook is invoked from field_create_field() after the field is created, to
* allow modules to act on field creation.
*
* @param $field
* The field just created.
*/
function hook_field_create_field($field) {
// @todo Needs function body.
}
/**
* Act on a field instance being created.
*
* This hook is invoked from field_create_instance() after the instance record
* is saved, so it cannot be used to modify the instance itself.
*
* @param $instance
* The instance just created.
*/
function hook_field_create_instance($instance) {
// @todo Needs function body.
}
/**
* Forbid a field update from occurring.
*
* Any module may forbid any update for any reason. For example, the
* field's storage module might forbid an update if it would change
* the storage schema while data for the field exists. A field type
* module might forbid an update if it would change existing data's
* semantics, or if there are external dependencies on field settings
* that cannot be updated.
*
* To forbid the update from occurring, throw a FieldUpdateForbiddenException.
*
* @param $field
* The field as it will be post-update.
* @param $prior_field
* The field as it is pre-update.
* @param $has_data
* Whether any data already exists for this field.
*/
function hook_field_update_forbid($field, $prior_field, $has_data) {
// A 'list' field stores integer keys mapped to display values. If
// the new field will have fewer values, and any data exists for the
// abandoned keys, the field will have no way to display them. So,
// forbid such an update.
if ($has_data && count($field['settings']['allowed_values']) < count($prior_field['settings']['allowed_values'])) {
// Identify the keys that will be lost.
$lost_keys = array_diff(array_keys($field['settings']['allowed_values']), array_keys($prior_field['settings']['allowed_values']));
// If any data exist for those keys, forbid the update.
$query = new EntityFieldQuery();
$found = $query
->fieldCondition($prior_field['field_name'], 'value', $lost_keys)
->range(0, 1)
->execute();
if ($found) {
throw new FieldUpdateForbiddenException("Cannot update a list field not to include keys with existing data");
}
}
}
/**
* Act on a field being updated.
*
* This hook is invoked just after field is updated in field_update_field().
*
* @param $field
* The field as it is post-update.
* @param $prior_field
* The field as it was pre-update.
* @param $has_data
* Whether any data already exists for this field.
*/
function hook_field_update_field($field, $prior_field, $has_data) {
// Reset the static value that keeps track of allowed values for list fields.
drupal_static_reset('list_allowed_values');
}
/**
* Act on a field being deleted.
*
* This hook is invoked just after a field is deleted by field_delete_field().
*
* @param $field
* The field just deleted.
*/
function hook_field_delete_field($field) {
// @todo Needs function body.
}
/**
* Act on a field instance being updated.
*
* This hook is invoked from field_update_instance() after the instance record
* is saved, so it cannot be used by a module to modify the instance itself.
*
* @param $instance
* The instance as it is post-update.
* @param $prior_instance
* The instance as it was pre-update.
*/
function hook_field_update_instance($instance, $prior_instance) {
// @todo Needs function body.
}
/**
* Act on a field instance being deleted.
*
* This hook is invoked from field_delete_instance() after the instance is
* deleted.
*
* @param $instance
* The instance just deleted.
*/
function hook_field_delete_instance($instance) {
// @todo Needs function body.
}
/**
* Act on field records being read from the database.
*
* This hook is invoked from field_read_fields() on each field being read.
*
* @param $field
* The field record just read from the database.
*/
function hook_field_read_field($field) {
// @todo Needs function body.
}
/**
* Act on a field record being read from the database.
*
* This hook is invoked from field_read_instances() on each instance being read.
*
* @param $instance
* The instance record just read from the database.
*/
function hook_field_read_instance($instance) {
// @todo Needs function body.
}
/**
* Acts when a field record is being purged.
*
* In field_purge_field(), after the field configuration has been
* removed from the database, the field storage module has had a chance to
* run its hook_field_storage_purge_field(), and the field info cache
* has been cleared, this hook is invoked on all modules to allow them to
* respond to the field being purged.
*
* @param $field
* The field being purged.
*/
function hook_field_purge_field($field) {
db_delete('my_module_field_info')
->condition('id', $field['id'])
->execute();
}
/**
* Acts when a field instance is being purged.
*
* In field_purge_instance(), after the field instance has been
* removed from the database, the field storage module has had a chance to
* run its hook_field_storage_purge_instance(), and the field info cache
* has been cleared, this hook is invoked on all modules to allow them to
* respond to the field instance being purged.
*
* @param $instance
* The instance being purged.
*/
function hook_field_purge_instance($instance) {
db_delete('my_module_field_instance_info')
->condition('id', $instance['id'])
->execute();
}
/**
* Remove field storage information when a field record is purged.
*
* Called from field_purge_field() to allow the field storage module
* to remove field information when a field is being purged.
*
* @param $field
* The field being purged.
*
* @ingroup field_storage
*/
function hook_field_storage_purge_field($field) {
$table_name = _field_sql_storage_tablename($field);
$revision_name = _field_sql_storage_revision_tablename($field);
db_drop_table($table_name);
db_drop_table($revision_name);
}
/**
* Remove field storage information when a field instance is purged.
*
* Called from field_purge_instance() to allow the field storage module
* to remove field instance information when a field instance is being
* purged.
*
* @param $instance
* The instance being purged.
*
* @ingroup field_storage
*/
function hook_field_storage_purge_field_instance($instance) {
db_delete('my_module_field_instance_info')
->condition('id', $instance['id'])
->execute();
}
/**
* Remove field storage information when field data is purged.
*
* Called from field_purge_data() to allow the field storage
* module to delete field data information.
*
* @param $entity_type
* The type of $entity; for example, 'node' or 'user'.
* @param $entity
* The pseudo-entity whose field data to delete.
* @param $field
* The (possibly deleted) field whose data is being purged.
* @param $instance
* The deleted field instance whose data is being purged.
*
* @ingroup field_storage
*/
function hook_field_storage_purge($entity_type, $entity, $field, $instance) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
$table_name = _field_sql_storage_tablename($field);
$revision_name = _field_sql_storage_revision_tablename($field);
db_delete($table_name)
->condition('entity_type', $entity_type)
->condition('entity_id', $id)
->execute();
db_delete($revision_name)
->condition('entity_type', $entity_type)
->condition('entity_id', $id)
->execute();
}
/**
* @} End of "addtogroup field_crud".
*/
/**
* Determine whether the user has access to a given field.
*
* This hook is invoked from field_access() to let modules block access to
* operations on fields. If no module returns FALSE, the operation is allowed.
*
* @param $op
* The operation to be performed. Possible values: 'edit', 'view'.
* @param $field
* The field on which the operation is to be performed.
* @param $entity_type
* The type of $entity; for example, 'node' or 'user'.
* @param $entity
* (optional) The entity for the operation.
* @param $account
* (optional) The account to check; if not given use currently logged in user.
*
* @return
* TRUE if the operation is allowed, and FALSE if the operation is denied.
*
* @ingroup field_types
*/
function hook_field_access($op, $field, $entity_type, $entity, $account) {
if ($field['field_name'] == 'field_of_interest' && $op == 'edit') {
return user_access('edit field of interest', $account);
}
return TRUE;
}
/**
* @} End of "addtogroup hooks".
*/
errors = $errors;
parent::__construct(t('Field validation errors'));
}
}
/**
* @defgroup field_storage Field Storage API
* @{
* Implement a storage engine for Field API data.
*
* The Field Attach API uses the Field Storage API to perform all "database
* access". Each Field Storage API hook function defines a primitive database
* operation such as read, write, or delete. The default field storage module,
* field_sql_storage.module, uses the local SQL database to implement these
* operations, but alternative field storage backends can choose to represent
* the data in SQL differently or use a completely different storage mechanism
* such as a cloud-based database.
*
* Each field defines which storage backend it uses. The Drupal system variable
* 'field_storage_default' identifies the storage backend used by default.
*
* See @link field Field API @endlink for information about the other parts of
* the Field API.
*/
/**
* Argument for an update operation.
*
* This is used in hook_field_storage_write when updating an
* existing entity.
*/
define('FIELD_STORAGE_UPDATE', 'update');
/**
* Argument for an insert operation.
*
* This is used in hook_field_storage_write when inserting a new entity.
*/
define('FIELD_STORAGE_INSERT', 'insert');
/**
* @} End of "defgroup field_storage".
*/
/**
* @defgroup field_attach Field Attach API
* @{
* Operate on Field API data attached to Drupal entities.
*
* Field Attach API functions load, store, display, generate Field API
* structures, and perform a variety of other functions for field data attached
* to individual entities.
*
* Field Attach API functions generally take $entity_type and $entity arguments
* along with additional function-specific arguments. $entity_type is the type
* of the fieldable entity, such as 'node' or 'user', and $entity is the entity
* itself.
*
* hook_entity_info() is the central place for entity types to define if and
* how Field API should operate on their entity objects. Notably, the
* 'fieldable' property needs to be set to TRUE.
*
* The Field Attach API uses the concept of bundles: the set of fields for a
* given entity is defined on a per-bundle basis. The collection of bundles for
* an entity type is defined its hook_entity_info() implementation. For
* instance, node_entity_info() exposes each node type as its own bundle. This
* means that the set of fields of a node is determined by the node type. The
* Field API reads the bundle name for a given entity from a particular
* property of the entity object, and hook_entity_info() defines which property
* to use. For instance, node_entity_info() specifies:
* @code $info['entity keys']['bundle'] = 'type'@endcode
* This indicates that for a particular node object, the bundle name can be
* found in $node->type. This property can be omitted if the entity type only
* exposes a single bundle (all entities of this type have the same collection
* of fields). This is the case for the 'user' entity type.
*
* Most Field Attach API functions define a corresponding hook function that
* allows any module to act on Field Attach operations for any entity after the
* operation is complete, and access or modify all the field, form, or display
* data for that entity and operation. For example, field_attach_view() invokes
* hook_field_attach_view_alter(). These all-module hooks are distinct from
* those of the Field Types API, such as hook_field_load(), that are only
* invoked for the module that defines a specific field type.
*
* field_attach_load(), field_attach_insert(), and field_attach_update() also
* define pre-operation hooks, e.g. hook_field_attach_pre_load(). These hooks
* run before the corresponding Field Storage API and Field Type API
* operations. They allow modules to define additional storage locations (e.g.
* denormalizing, mirroring) for field data on a per-field basis. They also
* allow modules to take over field storage completely by instructing other
* implementations of the same hook and the Field Storage API itself not to
* operate on specified fields.
*
* The pre-operation hooks do not make the Field Storage API irrelevant. The
* Field Storage API is essentially the "fallback mechanism" for any fields
* that aren't being intercepted explicitly by pre-operation hooks.
*
* @link field_language Field Language API @endlink provides information about
* the structure of field objects.
*
* See @link field Field API @endlink for information about the other parts of
* the Field API.
*/
/**
* Invoke a field hook.
*
* @param $op
* Possible operations include:
* - form
* - validate
* - presave
* - insert
* - update
* - delete
* - delete revision
* - view
* - prepare translation
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The fully formed $entity_type entity.
* @param $a
* - The $form in the 'form' operation.
* - The value of $view_mode in the 'view' operation.
* - Otherwise NULL.
* @param $b
* - The $form_state in the 'submit' operation.
* - Otherwise NULL.
* @param $options
* An associative array of additional options, with the following keys:
* - 'field_name': The name of the field whose operation should be
* invoked. By default, the operation is invoked on all the fields
* in the entity's bundle. NOTE: This option is not compatible with
* the 'deleted' option; the 'field_id' option should be used
* instead.
* - 'field_id': The id of the field whose operation should be
* invoked. By default, the operation is invoked on all the fields
* in the entity's' bundles.
* - 'default': A boolean value, specifying which implementation of
* the operation should be invoked.
* - if FALSE (default), the field types implementation of the operation
* will be invoked (hook_field_[op])
* - If TRUE, the default field implementation of the field operation
* will be invoked (field_default_[op])
* Internal use only. Do not explicitely set to TRUE, but use
* _field_invoke_default() instead.
* - 'deleted': If TRUE, the function will operate on deleted fields
* as well as non-deleted fields. If unset or FALSE, only
* non-deleted fields are operated on.
* - 'language': A language code or an array of language codes keyed by field
* name. It will be used to narrow down to a single value the available
* languages to act on.
*/
function _field_invoke($op, $entity_type, $entity, &$a = NULL, &$b = NULL, $options = array()) {
// Merge default options.
$default_options = array(
'default' => FALSE,
'deleted' => FALSE,
'language' => NULL,
);
$options += $default_options;
// Determine the list of instances to iterate on.
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
$instances = _field_invoke_get_instances($entity_type, $bundle, $options);
// Iterate through the instances and collect results.
$return = array();
foreach ($instances as $instance) {
// field_info_field() is not available for deleted fields, so use
// field_info_field_by_id().
$field = field_info_field_by_id($instance['field_id']);
$field_name = $field['field_name'];
$function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op;
if (function_exists($function)) {
// Determine the list of languages to iterate on.
$available_languages = field_available_languages($entity_type, $field);
$languages = _field_language_suggestion($available_languages, $options['language'], $field_name);
foreach ($languages as $langcode) {
$items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
$result = $function($entity_type, $entity, $field, $instance, $langcode, $items, $a, $b);
if (isset($result)) {
// For hooks with array results, we merge results together.
// For hooks with scalar results, we collect results in an array.
if (is_array($result)) {
$return = array_merge($return, $result);
}
else {
$return[] = $result;
}
}
// Populate $items back in the field values, but avoid replacing missing
// fields with an empty array (those are not equivalent on update).
if ($items !== array() || isset($entity->{$field_name}[$langcode])) {
$entity->{$field_name}[$langcode] = $items;
}
}
}
}
return $return;
}
/**
* Invoke a field hook across fields on multiple entities.
*
* @param $op
* Possible operations include:
* - load
* - prepare_view
* For all other operations, use _field_invoke() / field_invoke_default()
* instead.
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entities
* An array of entities, keyed by entity id.
* @param $a
* - The $age parameter in the 'load' operation.
* - Otherwise NULL.
* @param $b
* Currently always NULL.
* @param $options
* An associative array of additional options, with the following keys:
* - 'field_name': The name of the field whose operation should be
* invoked. By default, the operation is invoked on all the fields
* in the entity's bundle. NOTE: This option is not compatible with
* the 'deleted' option; the 'field_id' option should be used instead.
* - 'field_id': The id of the field whose operation should be
* invoked. By default, the operation is invoked on all the fields
* in the entity's' bundles.
* - 'default': A boolean value, specifying which implementation of
* the operation should be invoked.
* - if FALSE (default), the field types implementation of the operation
* will be invoked (hook_field_[op])
* - If TRUE, the default field implementation of the field operation
* will be invoked (field_default_[op])
* Internal use only. Do not explicitely set to TRUE, but use
* _field_invoke_multiple_default() instead.
* - 'deleted': If TRUE, the function will operate on deleted fields
* as well as non-deleted fields. If unset or FALSE, only
* non-deleted fields are operated on.
* - 'language': A language code or an array of arrays of language codes keyed
* by entity id and field name. It will be used to narrow down to a single
* value the available languages to act on.
*
* @return
* An array of returned values keyed by entity id.
*/
function _field_invoke_multiple($op, $entity_type, $entities, &$a = NULL, &$b = NULL, $options = array()) {
// Merge default options.
$default_options = array(
'default' => FALSE,
'deleted' => FALSE,
'language' => NULL,
);
$options += $default_options;
$fields = array();
$grouped_instances = array();
$grouped_entities = array();
$grouped_items = array();
$return = array();
// Go through the entities and collect the fields on which the hook should be
// invoked.
//
// We group fields by id, not by name, because this function can operate on
// deleted fields which may have non-unique names. However, entities can only
// contain data for a single field for each name, even if that field
// is deleted, so we reference field data via the
// $entity->$field_name property.
foreach ($entities as $entity) {
// Determine the list of instances to iterate on.
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
$instances = _field_invoke_get_instances($entity_type, $bundle, $options);
foreach ($instances as $instance) {
$field_id = $instance['field_id'];
$field_name = $instance['field_name'];
$field = field_info_field_by_id($field_id);
$function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op;
if (function_exists($function)) {
// Add the field to the list of fields to invoke the hook on.
if (!isset($fields[$field_id])) {
$fields[$field_id] = $field;
}
// Extract the field values into a separate variable, easily accessed
// by hook implementations.
// Unless a language suggestion is provided we iterate on all the
// available languages.
$available_languages = field_available_languages($entity_type, $field);
$language = is_array($options['language']) && !empty($options['language'][$id]) ? $options['language'][$id] : $options['language'];
$languages = _field_language_suggestion($available_languages, $language, $field_name);
foreach ($languages as $langcode) {
$grouped_items[$field_id][$langcode][$id] = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
// Group the instances and entities corresponding to the current
// field.
$grouped_instances[$field_id][$langcode][$id] = $instance;
$grouped_entities[$field_id][$langcode][$id] = $entities[$id];
}
}
}
// Initialize the return value for each entity.
$return[$id] = array();
}
// For each field, invoke the field hook and collect results.
foreach ($fields as $field_id => $field) {
$field_name = $field['field_name'];
$function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op;
// Iterate over all the field translations.
foreach ($grouped_items[$field_id] as $langcode => &$items) {
$entities = $grouped_entities[$field_id][$langcode];
$instances = $grouped_instances[$field_id][$langcode];
$results = $function($entity_type, $entities, $field, $instances, $langcode, $items, $a, $b);
if (isset($results)) {
// Collect results by entity.
// For hooks with array results, we merge results together.
// For hooks with scalar results, we collect results in an array.
foreach ($results as $id => $result) {
if (is_array($result)) {
$return[$id] = array_merge($return[$id], $result);
}
else {
$return[$id][] = $result;
}
}
}
}
// Populate field values back in the entities, but avoid replacing missing
// fields with an empty array (those are not equivalent on update).
foreach ($grouped_entities[$field_id] as $langcode => $entities) {
foreach ($entities as $id => $entity) {
if ($grouped_items[$field_id][$langcode][$id] !== array() || isset($entity->{$field_name}[$langcode])) {
$entity->{$field_name}[$langcode] = $grouped_items[$field_id][$langcode][$id];
}
}
}
}
return $return;
}
/**
* Invoke field.module's version of a field hook.
*
* This function invokes the field_default_[op]() function.
* Use _field_invoke() to invoke the field type implementation,
* hook_field_[op]().
*
* @see _field_invoke()
*/
function _field_invoke_default($op, $entity_type, $entity, &$a = NULL, &$b = NULL, $options = array()) {
$options['default'] = TRUE;
return _field_invoke($op, $entity_type, $entity, $a, $b, $options);
}
/**
* Invoke field.module's version of a field hook on multiple entities.
*
* This function invokes the field_default_[op]() function.
* Use _field_invoke_multiple() to invoke the field type implementation,
* hook_field_[op]().
*
* @see _field_invoke_multiple()
*/
function _field_invoke_multiple_default($op, $entity_type, $entities, &$a = NULL, &$b = NULL, $options = array()) {
$options['default'] = TRUE;
return _field_invoke_multiple($op, $entity_type, $entities, $a, $b, $options);
}
/**
* Helper for _field_invoke(): retrieves a list of instances to operate on.
*
* @param $entity_type
* The entity type.
* @param $bundle
* The bundle name.
* @param $options
* An associative array of options, as provided to _field_invoke(). Only the
* following keys are considered :
* - deleted
* - field_name
* - field_id
* See _field_invoke() for details.
*
* @return
* The array of selected instance definitions.
*/
function _field_invoke_get_instances($entity_type, $bundle, $options) {
if ($options['deleted']) {
// Deleted fields are not included in field_info_instances(), and need to
// be fetched from the database with field_read_instances().
$params = array('entity_type' => $entity_type, 'bundle' => $bundle);
if (isset($options['field_id'])) {
// Single-field mode by field id: field_read_instances() does the filtering.
// Single-field mode by field name is not compatible with the 'deleted'
// option.
$params['field_id'] = $options['field_id'];
}
$instances = field_read_instances($params, array('include_deleted' => TRUE));
}
elseif (isset($options['field_name'])) {
// Single-field mode by field name: field_info_instance() does the
// filtering.
$instances = array(field_info_instance($entity_type, $options['field_name'], $bundle));
}
else {
$instances = field_info_instances($entity_type, $bundle);
if (isset($options['field_id'])) {
// Single-field mode by field id: we need to loop on each instance to
// find the right one.
foreach ($instances as $instance) {
if ($instance['field_id'] == $options['field_id']) {
$instances = array($instance);
break;
}
}
}
}
return $instances;
}
/**
* Add form elements for all fields for an entity to a form structure.
*
* The form elements for the entity's fields are added by reference as direct
* children in the $form parameter. This parameter can be a full form structure
* (most common case for entity edit forms), or a sub-element of a larger form.
*
* By default, submitted field values appear at the top-level of
* $form_state['values']. A different location within $form_state['values'] can
* be specified by setting the '#parents' property on the incoming $form
* parameter. Because of name clashes, two instances of the same field cannot
* appear within the same $form element, or within the same '#parents' space.
*
* For each call to field_attach_form(), field values are processed by calling
* field_attach_form_validate() and field_attach_submit() on the same $form
* element.
*
* Sample resulting structure in $form:
* @code
* '#parents' => The location of field values in $form_state['values'],
* '#entity_type' => The name of the entity type,
* '#bundle' => The name of the bundle,
* // One sub-array per field appearing in the entity, keyed by field name.
* // The structure of the array differs slightly depending on whether the
* // widget is 'single-value' (provides the input for one field value,
* // most common case), and will therefore be repeated as many times as
* // needed, or 'multiple-values' (one single widget allows the input of
* // several values, e.g checkboxes, select box...).
* // The sub-array is nested into a $langcode key where $langcode has the
* // same value of the $langcode parameter above.
* // The '#language' key holds the same value of $langcode and it is used
* // to access the field sub-array when $langcode is unknown.
* 'field_foo' => array(
* '#tree' => TRUE,
* '#field_name' => The name of the field,
* '#language' => $langcode,
* $langcode => array(
* '#field_name' => The name of the field,
* '#language' => $langcode,
* '#field_parents' => The 'parents' space for the field in the form,
* equal to the #parents property of the $form parameter received by
* field_attach_form(),
* '#required' => Whether or not the field is required,
* '#title' => The label of the field instance,
* '#description' => The description text for the field instance,
*
* // Only for 'single' widgets:
* '#theme' => 'field_multiple_value_form',
* '#cardinality' => The field cardinality,
* // One sub-array per copy of the widget, keyed by delta.
* 0 => array(
* '#entity_type' => The name of the entity type,
* '#bundle' => The name of the bundle,
* '#field_name' => The name of the field,
* '#field_parents' => The 'parents' space for the field in the form,
* equal to the #parents property of the $form parameter received by
* field_attach_form(),
* '#title' => The title to be displayed by the widget,
* '#default_value' => The field value for delta 0,
* '#required' => Whether the widget should be marked required,
* '#delta' => 0,
* '#columns' => The array of field columns,
* // The remaining elements in the sub-array depend on the widget.
* '#type' => The type of the widget,
* ...
* ),
* 1 => array(
* ...
* ),
*
* // Only for multiple widgets:
* '#entity_type' => The name of the entity type,
* '#bundle' => $instance['bundle'],
* '#columns' => array_keys($field['columns']),
* // The remaining elements in the sub-array depend on the widget.
* '#type' => The type of the widget,
* ...
* ),
* ...
* ),
* )
* @endcode
*
* Additionally, some processing data is placed in $form_state, and can be
* accessed by field_form_get_state() and field_form_set_state().
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The entity for which to load form elements, used to initialize
* default form values.
* @param $form
* The form structure to fill in. This can be a full form structure, or a
* sub-element of a larger form. The #parents property can be set to control
* the location of submitted field values within $form_state['values']. If
* not specified, $form['#parents'] is set to an empty array, placing field
* values at the top-level of $form_state['values'].
* @param $form_state
* An associative array containing the current state of the form.
* @param $langcode
* The language the field values are going to be entered, if no language
* is provided the default site language will be used.
* @param array $options
* An associative array of additional options. See _field_invoke() for
* details.
*
* @see field_form_get_state()
* @see field_form_set_state()
*/
function field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode = NULL, $options = array()) {
// Validate $options since this is a new parameter added after Drupal 7 was
// released.
$options = is_array($options) ? $options : array();
// Set #parents to 'top-level' by default.
$form += array('#parents' => array());
// If no language is provided use the default site language.
$options['language'] = field_valid_language($langcode);
$form += (array) _field_invoke_default('form', $entity_type, $entity, $form, $form_state, $options);
// Add custom weight handling.
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
$form['#pre_render'][] = '_field_extra_fields_pre_render';
$form['#entity_type'] = $entity_type;
$form['#bundle'] = $bundle;
// Let other modules make changes to the form.
// Avoid module_invoke_all() to let parameters be taken by reference.
foreach (module_implements('field_attach_form') as $module) {
$function = $module . '_field_attach_form';
$function($entity_type, $entity, $form, $form_state, $langcode);
}
}
/**
* Loads fields for the current revisions of a group of entities.
*
* Loads all fields for each entity object in a group of a single entity type.
* The loaded field values are added directly to the entity objects.
*
* field_attach_load() is automatically called by the default entity controller
* class, and thus, in most cases, doesn't need to be explicitly called by the
* entity type module.
*
* @param $entity_type
* The type of $entity; e.g., 'node' or 'user'.
* @param $entities
* An array of entities for which to load fields, keyed by entity ID.
* Each entity needs to have its 'bundle', 'id' and (if applicable)
* 'revision' keys filled in. The function adds the loaded field data
* directly in the entity objects of the $entities array.
* @param $age
* FIELD_LOAD_CURRENT to load the most recent revision for all
* fields, or FIELD_LOAD_REVISION to load the version indicated by
* each entity. Defaults to FIELD_LOAD_CURRENT; use
* field_attach_load_revision() instead of passing FIELD_LOAD_REVISION.
* @param $options
* An associative array of additional options, with the following keys:
* - 'field_id': The field ID that should be loaded, instead of
* loading all fields, for each entity. Note that returned entities
* may contain data for other fields, for example if they are read
* from a cache.
* - 'deleted': If TRUE, the function will operate on deleted fields
* as well as non-deleted fields. If unset or FALSE, only
* non-deleted fields are operated on.
*/
function field_attach_load($entity_type, $entities, $age = FIELD_LOAD_CURRENT, $options = array()) {
$load_current = $age == FIELD_LOAD_CURRENT;
// Merge default options.
$default_options = array(
'deleted' => FALSE,
);
$options += $default_options;
$info = entity_get_info($entity_type);
// Only the most current revision of non-deleted fields for cacheable entity
// types can be cached.
$cache_read = $load_current && $info['field cache'] && empty($options['deleted']);
// In addition, do not write to the cache when loading a single field.
$cache_write = $cache_read && !isset($options['field_id']);
if (empty($entities)) {
return;
}
// Assume all entities will need to be queried. Entities found in the cache
// will be removed from the list.
$queried_entities = $entities;
// Fetch available entities from cache, if applicable.
if ($cache_read) {
// Build the list of cache entries to retrieve.
$cids = array();
foreach ($entities as $id => $entity) {
$cids[] = "field:$entity_type:$id";
}
$cache = cache_get_multiple($cids, 'cache_field');
// Put the cached field values back into the entities and remove them from
// the list of entities to query.
foreach ($entities as $id => $entity) {
$cid = "field:$entity_type:$id";
if (isset($cache[$cid])) {
unset($queried_entities[$id]);
foreach ($cache[$cid]->data as $field_name => $values) {
$entity->$field_name = $values;
}
}
}
}
// Fetch other entities from their storage location.
if ($queried_entities) {
// The invoke order is:
// - hook_field_storage_pre_load()
// - storage backend's hook_field_storage_load()
// - field-type module's hook_field_load()
// - hook_field_attach_load()
// Invoke hook_field_storage_pre_load(): let any module load field
// data before the storage engine, accumulating along the way.
$skip_fields = array();
foreach (module_implements('field_storage_pre_load') as $module) {
$function = $module . '_field_storage_pre_load';
$function($entity_type, $queried_entities, $age, $skip_fields, $options);
}
$instances = array();
// Collect the storage backends used by the remaining fields in the entities.
$storages = array();
foreach ($queried_entities as $entity) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
$instances = _field_invoke_get_instances($entity_type, $bundle, $options);
foreach ($instances as $instance) {
$field_name = $instance['field_name'];
$field_id = $instance['field_id'];
// Make sure all fields are present at least as empty arrays.
if (!isset($queried_entities[$id]->{$field_name})) {
$queried_entities[$id]->{$field_name} = array();
}
// Collect the storage backend if the field has not been loaded yet.
if (!isset($skip_fields[$field_id])) {
$field = field_info_field_by_id($field_id);
$storages[$field['storage']['type']][$field_id][] = $load_current ? $id : $vid;
}
}
}
// Invoke hook_field_storage_load() on the relevant storage backends.
foreach ($storages as $storage => $fields) {
$storage_info = field_info_storage_types($storage);
module_invoke($storage_info['module'], 'field_storage_load', $entity_type, $queried_entities, $age, $fields, $options);
}
// Invoke field-type module's hook_field_load().
$null = NULL;
_field_invoke_multiple('load', $entity_type, $queried_entities, $age, $null, $options);
// Invoke hook_field_attach_load(): let other modules act on loading the
// entity.
module_invoke_all('field_attach_load', $entity_type, $queried_entities, $age, $options);
// Build cache data.
if ($cache_write) {
foreach ($queried_entities as $id => $entity) {
$data = array();
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
$instances = field_info_instances($entity_type, $bundle);
foreach ($instances as $instance) {
$data[$instance['field_name']] = $queried_entities[$id]->{$instance['field_name']};
}
$cid = "field:$entity_type:$id";
cache_set($cid, $data, 'cache_field');
}
}
}
}
/**
* Load all fields for previous versions of a group of entities.
*
* Loading different versions of the same entities is not supported, and should
* be done by separate calls to the function.
*
* field_attach_load_revision() is automatically called by the default entity
* controller class, and thus, in most cases, doesn't need to be explicitly
* called by the entity type module.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entities
* An array of entities for which to load fields, keyed by entity ID. Each
* entity needs to have its 'bundle', 'id' and (if applicable) 'revision'
* keys filled. The function adds the loaded field data directly in the
* entity objects of the $entities array.
* @param $options
* An associative array of additional options. See field_attach_load() for
* details.
*/
function field_attach_load_revision($entity_type, $entities, $options = array()) {
return field_attach_load($entity_type, $entities, FIELD_LOAD_REVISION, $options);
}
/**
* Perform field validation against the field data in an entity.
*
* This function does not perform field widget validation on form
* submissions. It is intended to be called during API save
* operations. Use field_attach_form_validate() to validate form
* submissions.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The entity with fields to validate.
* @throws FieldValidationException
* If validation errors are found, a FieldValidationException is thrown. The
* 'errors' property contains the array of errors, keyed by field name,
* language and delta.
* @param array $options
* An associative array of additional options. See _field_invoke() for
* details.
*/
function field_attach_validate($entity_type, $entity, $options = array()) {
// Validate $options since this is a new parameter added after Drupal 7 was
// released.
$options = is_array($options) ? $options : array();
$errors = array();
// Check generic, field-type-agnostic errors first.
$null = NULL;
_field_invoke_default('validate', $entity_type, $entity, $errors, $null, $options);
// Check field-type specific errors.
_field_invoke('validate', $entity_type, $entity, $errors, $null, $options);
// Let other modules validate the entity.
// Avoid module_invoke_all() to let $errors be taken by reference.
foreach (module_implements('field_attach_validate') as $module) {
$function = $module . '_field_attach_validate';
$function($entity_type, $entity, $errors);
}
if ($errors) {
throw new FieldValidationException($errors);
}
}
/**
* Perform field validation against form-submitted field values.
*
* There are two levels of validation for fields in forms: widget
* validation, and field validation.
* - Widget validation steps are specific to a given widget's own form
* structure and UI metaphors. They are executed through FAPI's
* #element_validate property during normal form validation.
* - Field validation steps are common to a given field type, independently of
* the specific widget being used in a given form. They are defined in the
* field type's implementation of hook_field_validate().
*
* This function performs field validation in the context of a form
* submission. It converts field validation errors into form errors
* on the correct form elements. Fieldable entity types should call
* this function during their own form validation function.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The entity being submitted. The 'bundle', 'id' and (if applicable)
* 'revision' keys should be present. The actual field values will be read
* from $form_state['values'].
* @param $form
* The form structure where field elements are attached to. This might be a
* full form structure, or a sub-element of a larger form.
* @param $form_state
* An associative array containing the current state of the form.
* @param array $options
* An associative array of additional options. See _field_invoke() for
* details.
*/
function field_attach_form_validate($entity_type, $entity, $form, &$form_state, $options = array()) {
// Validate $options since this is a new parameter added after Drupal 7 was
// released.
$options = is_array($options) ? $options : array();
// Extract field values from submitted values.
_field_invoke_default('extract_form_values', $entity_type, $entity, $form, $form_state);
// Perform field_level validation.
try {
field_attach_validate($entity_type, $entity, $options);
}
catch (FieldValidationException $e) {
// Pass field-level validation errors back to widgets for accurate error
// flagging.
foreach ($e->errors as $field_name => $field_errors) {
foreach ($field_errors as $langcode => $errors) {
$field_state = field_form_get_state($form['#parents'], $field_name, $langcode, $form_state);
$field_state['errors'] = $errors;
field_form_set_state($form['#parents'], $field_name, $langcode, $form_state, $field_state);
}
}
_field_invoke_default('form_errors', $entity_type, $entity, $form, $form_state, $options);
}
}
/**
* Perform necessary operations on field data submitted by a form.
*
* Currently, this accounts for drag-and-drop reordering of
* field values, and filtering of empty values.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The entity being submitted. The 'bundle', 'id' and (if applicable)
* 'revision' keys should be present. The actual field values will be read
* from $form_state['values'].
* @param $form
* The form structure where field elements are attached to. This might be a
* full form structure, or a sub-element of a larger form.
* @param $form_state
* An associative array containing the current state of the form.
* @param array $options
* An associative array of additional options. See _field_invoke() for
* details.
*/
function field_attach_submit($entity_type, $entity, $form, &$form_state, $options = array()) {
// Validate $options since this is a new parameter added after Drupal 7 was
// released.
$options = is_array($options) ? $options : array();
// Extract field values from submitted values.
_field_invoke_default('extract_form_values', $entity_type, $entity, $form, $form_state, $options);
_field_invoke_default('submit', $entity_type, $entity, $form, $form_state, $options);
// Let other modules act on submitting the entity.
// Avoid module_invoke_all() to let $form_state be taken by reference.
foreach (module_implements('field_attach_submit') as $module) {
$function = $module . '_field_attach_submit';
$function($entity_type, $entity, $form, $form_state);
}
}
/**
* Perform necessary operations just before fields data get saved.
*
* We take no specific action here, we just give other
* modules the opportunity to act.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The entity with fields to process.
*/
function field_attach_presave($entity_type, $entity) {
_field_invoke('presave', $entity_type, $entity);
// Let other modules act on presaving the entity.
module_invoke_all('field_attach_presave', $entity_type, $entity);
}
/**
* Save field data for a new entity.
*
* The passed-in entity must already contain its id and (if applicable)
* revision id attributes.
* Default values (if any) will be saved for fields not present in the
* $entity.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The entity with fields to save.
* @return
* Default values (if any) will be added to the $entity parameter for fields
* it leaves unspecified.
*/
function field_attach_insert($entity_type, $entity) {
_field_invoke_default('insert', $entity_type, $entity);
_field_invoke('insert', $entity_type, $entity);
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
// Let any module insert field data before the storage engine, accumulating
// saved fields along the way.
$skip_fields = array();
foreach (module_implements('field_storage_pre_insert') as $module) {
$function = $module . '_field_storage_pre_insert';
$function($entity_type, $entity, $skip_fields);
}
// Collect the storage backends used by the remaining fields in the entities.
$storages = array();
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$field = field_info_field_by_id($instance['field_id']);
$field_id = $field['id'];
$field_name = $field['field_name'];
if (!empty($entity->$field_name)) {
// Collect the storage backend if the field has not been written yet.
if (!isset($skip_fields[$field_id])) {
$storages[$field['storage']['type']][$field_id] = $field_id;
}
}
}
// Field storage backends save any remaining unsaved fields.
foreach ($storages as $storage => $fields) {
$storage_info = field_info_storage_types($storage);
module_invoke($storage_info['module'], 'field_storage_write', $entity_type, $entity, FIELD_STORAGE_INSERT, $fields);
}
// Let other modules act on inserting the entity.
module_invoke_all('field_attach_insert', $entity_type, $entity);
$entity_info = entity_get_info($entity_type);
}
/**
* Save field data for an existing entity.
*
* When calling this function outside an entity save operation be sure to
* clear caches for the entity:
* @code
* entity_get_controller($entity_type)->resetCache(array($entity_id))
* @endcode
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The entity with fields to save.
*/
function field_attach_update($entity_type, $entity) {
_field_invoke('update', $entity_type, $entity);
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
// Let any module update field data before the storage engine, accumulating
// saved fields along the way.
$skip_fields = array();
foreach (module_implements('field_storage_pre_update') as $module) {
$function = $module . '_field_storage_pre_update';
$function($entity_type, $entity, $skip_fields);
}
// Collect the storage backends used by the remaining fields in the entities.
$storages = array();
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$field = field_info_field_by_id($instance['field_id']);
$field_id = $field['id'];
$field_name = $field['field_name'];
// Leave the field untouched if $entity comes with no $field_name property,
// but empty the field if it comes as a NULL value or an empty array.
// Function property_exists() is slower, so we catch the more frequent
// cases where it's an empty array with the faster isset().
if (isset($entity->$field_name) || property_exists($entity, $field_name)) {
// Collect the storage backend if the field has not been written yet.
if (!isset($skip_fields[$field_id])) {
$storages[$field['storage']['type']][$field_id] = $field_id;
}
}
}
// Field storage backends save any remaining unsaved fields.
foreach ($storages as $storage => $fields) {
$storage_info = field_info_storage_types($storage);
module_invoke($storage_info['module'], 'field_storage_write', $entity_type, $entity, FIELD_STORAGE_UPDATE, $fields);
}
// Let other modules act on updating the entity.
module_invoke_all('field_attach_update', $entity_type, $entity);
$entity_info = entity_get_info($entity_type);
if ($entity_info['field cache']) {
cache_clear_all("field:$entity_type:$id", 'cache_field');
}
}
/**
* Delete field data for an existing entity. This deletes all
* revisions of field data for the entity.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The entity whose field data to delete.
*/
function field_attach_delete($entity_type, $entity) {
_field_invoke('delete', $entity_type, $entity);
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
// Collect the storage backends used by the fields in the entities.
$storages = array();
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$field = field_info_field_by_id($instance['field_id']);
$field_id = $field['id'];
$storages[$field['storage']['type']][$field_id] = $field_id;
}
// Field storage backends delete their data.
foreach ($storages as $storage => $fields) {
$storage_info = field_info_storage_types($storage);
module_invoke($storage_info['module'], 'field_storage_delete', $entity_type, $entity, $fields);
}
// Let other modules act on deleting the entity.
module_invoke_all('field_attach_delete', $entity_type, $entity);
$entity_info = entity_get_info($entity_type);
if ($entity_info['field cache']) {
cache_clear_all("field:$entity_type:$id", 'cache_field');
}
}
/**
* Delete field data for a single revision of an existing entity. The
* passed entity must have a revision id attribute.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The entity with fields to save.
*/
function field_attach_delete_revision($entity_type, $entity) {
_field_invoke('delete_revision', $entity_type, $entity);
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
// Collect the storage backends used by the fields in the entities.
$storages = array();
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$field = field_info_field_by_id($instance['field_id']);
$field_id = $field['id'];
$storages[$field['storage']['type']][$field_id] = $field_id;
}
// Field storage backends delete their data.
foreach ($storages as $storage => $fields) {
$storage_info = field_info_storage_types($storage);
module_invoke($storage_info['module'], 'field_storage_delete_revision', $entity_type, $entity, $fields);
}
// Let other modules act on deleting the revision.
module_invoke_all('field_attach_delete_revision', $entity_type, $entity);
}
/**
* Prepare field data prior to display.
*
* This function lets field types and formatters load additional data
* needed for display that is not automatically loaded during
* field_attach_load(). It accepts an array of entities to allow query
* optimisation when displaying lists of entities.
*
* field_attach_prepare_view() and field_attach_view() are two halves
* of the same operation. It is safe to call
* field_attach_prepare_view() multiple times on the same entity
* before calling field_attach_view() on it, but calling any Field
* API operation on an entity between passing that entity to these two
* functions may yield incorrect results.
*
* @param $entity_type
* The type of $entities; e.g. 'node' or 'user'.
* @param $entities
* An array of entities, keyed by entity id.
* @param $view_mode
* View mode, e.g. 'full', 'teaser'...
* @param $langcode
* (Optional) The language the field values are to be shown in. If no language
* is provided the current language is used.
* @param array $options
* An associative array of additional options. See _field_invoke() for
* details.
*/
function field_attach_prepare_view($entity_type, $entities, $view_mode, $langcode = NULL, $options = array()) {
// Validate $options since this is a new parameter added after Drupal 7 was
// released.
$options = is_array($options) ? $options : array();
$options['language'] = array();
// To ensure hooks are only run once per entity, only process items without
// the _field_view_prepared flag.
// @todo: resolve this more generally for both entity and field level hooks.
$prepare = array();
foreach ($entities as $id => $entity) {
if (empty($entity->_field_view_prepared)) {
// Add this entity to the items to be prepared.
$prepare[$id] = $entity;
// Determine the actual language to display for each field, given the
// languages available in the field data.
$options['language'][$id] = field_language($entity_type, $entity, NULL, $langcode);
// Mark this item as prepared.
$entity->_field_view_prepared = TRUE;
}
}
$null = NULL;
// First let the field types do their preparation.
_field_invoke_multiple('prepare_view', $entity_type, $prepare, $null, $null, $options);
// Then let the formatters do their own specific massaging.
// field_default_prepare_view() takes care of dispatching to the correct
// formatters according to the display settings for the view mode.
_field_invoke_multiple_default('prepare_view', $entity_type, $prepare, $view_mode, $null, $options);
}
/**
* Returns a renderable array for the fields on an entity.
*
* Each field is displayed according to the display options specified in the
* $instance definition for the given $view_mode.
*
* field_attach_prepare_view() and field_attach_view() are two halves
* of the same operation. It is safe to call
* field_attach_prepare_view() multiple times on the same entity
* before calling field_attach_view() on it, but calling any Field
* API operation on an entity between passing that entity to these two
* functions may yield incorrect results.
*
* Sample structure:
* @code
* array(
* 'field_foo' => array(
* '#theme' => 'field',
* '#title' => the label of the field instance,
* '#label_display' => the label display mode,
* '#object' => the fieldable entity being displayed,
* '#entity_type' => the type of the entity being displayed,
* '#language' => the language of the field values being displayed,
* '#view_mode' => the view mode,
* '#field_name' => the name of the field,
* '#field_type' => the type of the field,
* '#formatter' => the name of the formatter,
* '#items' => the field values being displayed,
* // The element's children are the formatted values returned by
* // hook_field_formatter_view().
* ),
* );
* @endcode
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The entity with fields to render.
* @param $view_mode
* View mode, e.g. 'full', 'teaser'...
* @param $langcode
* The language the field values are to be shown in. If no language is
* provided the current language is used.
* @param array $options
* An associative array of additional options. See _field_invoke() for
* details.
* @return
* A renderable array for the field values.
*/
function field_attach_view($entity_type, $entity, $view_mode, $langcode = NULL, $options = array()) {
// Validate $options since this is a new parameter added after Drupal 7 was
// released.
$options = is_array($options) ? $options : array();
// Determine the actual language to display for each field, given the
// languages available in the field data.
$display_language = field_language($entity_type, $entity, NULL, $langcode);
$options['language'] = $display_language;
// Invoke field_default_view().
$null = NULL;
$output = _field_invoke_default('view', $entity_type, $entity, $view_mode, $null, $options);
// Add custom weight handling.
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
$output['#pre_render'][] = '_field_extra_fields_pre_render';
$output['#entity_type'] = $entity_type;
$output['#bundle'] = $bundle;
// Let other modules alter the renderable array.
$context = array(
'entity_type' => $entity_type,
'entity' => $entity,
'view_mode' => $view_mode,
'display' => $view_mode,
'language' => $langcode,
);
drupal_alter('field_attach_view', $output, $context);
// Reset the _field_view_prepared flag set in field_attach_prepare_view(),
// in case the same entity is displayed with different settings later in
// the request.
unset($entity->_field_view_prepared);
return $output;
}
/**
* Populate the template variables with the field values available for rendering.
*
* The $variables array will be populated with all the field instance values
* associated with the given entity type, keyed by field name; in case of
* translatable fields the language currently chosen for display will be
* selected.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The entity with fields to render.
* @param $element
* The structured array containing the values ready for rendering.
* @param $variables
* The variables array is passed by reference and will be populated with field
* values.
*/
function field_attach_preprocess($entity_type, $entity, $element, &$variables) {
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$field_name = $instance['field_name'];
if (isset($element[$field_name]['#language'])) {
$langcode = $element[$field_name]['#language'];
$variables[$field_name] = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : NULL;
}
}
// Let other modules make changes to the $variables array.
$context = array(
'entity_type' => $entity_type,
'entity' => $entity,
'element' => $element,
);
drupal_alter('field_attach_preprocess', $variables, $context);
}
/**
* Prepares an entity for translation.
*
* This function is used to fill-in the form default values for Field API fields
* while performing entity translation. By default it copies all the source
* values in the given source language to the new entity and assigns them the
* target language.
*
* This is used as part of the 'per entity' translation pattern, which is
* implemented only for nodes by translation.module. Other entity types may be
* supported through contributed modules.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The entity to be prepared for translation.
* @param $langcode
* The language the entity has to be translated in.
* @param $source_entity
* The source entity holding the field values to be translated.
* @param $source_langcode
* The source language from which translate.
*/
function field_attach_prepare_translation($entity_type, $entity, $langcode, $source_entity, $source_langcode) {
$options = array('language' => $langcode);
// Copy source field values into the entity to be prepared.
_field_invoke_default('prepare_translation', $entity_type, $entity, $source_entity, $source_langcode, $options);
// Let field types handle their own advanced translation pattern if needed.
_field_invoke('prepare_translation', $entity_type, $entity, $source_entity, $source_langcode, $options);
// Let other modules alter the entity translation.
$context = array(
'entity_type' => $entity_type,
'langcode' => $langcode,
'source_entity' => $source_entity,
'source_langcode' => $source_langcode,
);
drupal_alter('field_attach_prepare_translation', $entity, $context);
}
/**
* Notify field.module that a new bundle was created.
*
* The default SQL-based storage doesn't need to do anything about it, but
* others might.
*
* @param $entity_type
* The entity type to which the bundle is bound.
* @param $bundle
* The name of the newly created bundle.
*/
function field_attach_create_bundle($entity_type, $bundle) {
// Clear the cache.
field_cache_clear();
// Let other modules act on creating the bundle.
module_invoke_all('field_attach_create_bundle', $entity_type, $bundle);
}
/**
* Notify field.module that a bundle was renamed.
*
* @param $entity_type
* The entity type to which the bundle is bound.
* @param $bundle_old
* The previous name of the bundle.
* @param $bundle_new
* The new name of the bundle.
*/
function field_attach_rename_bundle($entity_type, $bundle_old, $bundle_new) {
db_update('field_config_instance')
->fields(array('bundle' => $bundle_new))
->condition('entity_type', $entity_type)
->condition('bundle', $bundle_old)
->execute();
// Clear the cache.
field_cache_clear();
// Update bundle settings.
$settings = variable_get('field_bundle_settings_' . $entity_type . '__' . $bundle_old, array());
variable_set('field_bundle_settings_' . $entity_type . '__' . $bundle_new, $settings);
variable_del('field_bundle_settings_' . $entity_type . '__' . $bundle_old);
// Let other modules act on renaming the bundle.
module_invoke_all('field_attach_rename_bundle', $entity_type, $bundle_old, $bundle_new);
}
/**
* Notify field.module the a bundle was deleted.
*
* This deletes the data for the field instances as well as the field instances
* themselves. This function actually just marks the data and field instances
* and deleted, leaving the garbage collection for a separate process, because
* it is not always possible to delete this much data in a single page request
* (particularly since for some field types, the deletion is more than just a
* simple DELETE query).
*
* @param $entity_type
* The entity type to which the bundle is bound.
* @param $bundle
* The bundle to delete.
*/
function field_attach_delete_bundle($entity_type, $bundle) {
// First, delete the instances themselves. field_read_instances() must be
// used here since field_info_instances() does not return instances for
// disabled entity types or bundles.
$instances = field_read_instances(array('entity_type' => $entity_type, 'bundle' => $bundle), array('include_inactive' => 1));
foreach ($instances as $instance) {
field_delete_instance($instance);
}
// Clear the cache.
field_cache_clear();
// Clear bundle display settings.
variable_del('field_bundle_settings_' . $entity_type . '__' . $bundle);
// Let other modules act on deleting the bundle.
module_invoke_all('field_attach_delete_bundle', $entity_type, $bundle, $instances);
}
/**
* @} End of "defgroup field_attach".
*/
$field['field_name'])));
}
// Field name cannot contain invalid characters.
if (!preg_match('/^[_a-z]+[_a-z0-9]*$/', $field['field_name'])) {
throw new FieldException(format_string('Attempt to create a field @field_name with invalid characters. Only lowercase alphanumeric characters and underscores are allowed, and only lowercase letters and underscore are allowed as the first character', array('@field_name' => $field['field_name'])));
}
// Field name cannot be longer than 32 characters. We use drupal_strlen()
// because the DB layer assumes that column widths are given in characters,
// not bytes.
if (drupal_strlen($field['field_name']) > 32) {
throw new FieldException(t('Attempt to create a field with a name longer than 32 characters: %name',
array('%name' => $field['field_name'])));
}
// Ensure the field name is unique over active and disabled fields.
// We do not care about deleted fields.
$prior_field = field_read_field($field['field_name'], array('include_inactive' => TRUE));
if (!empty($prior_field)) {
$message = $prior_field['active']?
t('Attempt to create field name %name which already exists and is active.', array('%name' => $field['field_name'])):
t('Attempt to create field name %name which already exists, although it is inactive.', array('%name' => $field['field_name']));
throw new FieldException($message);
}
// Disallow reserved field names. This can't prevent all field name
// collisions with existing entity properties, but some is better
// than none.
foreach (entity_get_info() as $type => $info) {
if (in_array($field['field_name'], $info['entity keys'])) {
throw new FieldException(t('Attempt to create field name %name which is reserved by entity type %type.', array('%name' => $field['field_name'], '%type' => $type)));
}
}
$field += array(
'entity_types' => array(),
'cardinality' => 1,
'translatable' => FALSE,
'locked' => FALSE,
'settings' => array(),
'storage' => array(),
'deleted' => 0,
);
// Check that the field type is known.
$field_type = field_info_field_types($field['type']);
if (!$field_type) {
throw new FieldException(t('Attempt to create a field of unknown type %type.', array('%type' => $field['type'])));
}
// Create all per-field-type properties (needed here as long as we have
// settings that impact column definitions).
$field['settings'] += field_info_field_settings($field['type']);
$field['module'] = $field_type['module'];
$field['active'] = 1;
// Provide default storage.
$field['storage'] += array(
'type' => variable_get('field_storage_default', 'field_sql_storage'),
'settings' => array(),
);
// Check that the storage type is known.
$storage_type = field_info_storage_types($field['storage']['type']);
if (!$storage_type) {
throw new FieldException(t('Attempt to create a field with unknown storage type %type.', array('%type' => $field['storage']['type'])));
}
// Provide default storage settings.
$field['storage']['settings'] += field_info_storage_settings($field['storage']['type']);
$field['storage']['module'] = $storage_type['module'];
$field['storage']['active'] = 1;
// Collect storage information.
module_load_install($field['module']);
$schema = (array) module_invoke($field['module'], 'field_schema', $field);
$schema += array('columns' => array(), 'indexes' => array(), 'foreign keys' => array());
// 'columns' are hardcoded in the field type.
$field['columns'] = $schema['columns'];
// 'foreign keys' are hardcoded in the field type.
$field['foreign keys'] = $schema['foreign keys'];
// 'indexes' can be both hardcoded in the field type, and specified in the
// incoming $field definition.
$field += array(
'indexes' => array(),
);
$field['indexes'] += $schema['indexes'];
// The serialized 'data' column contains everything from $field that does not
// have its own column and is not automatically populated when the field is
// read.
$data = $field;
unset($data['columns'], $data['field_name'], $data['type'], $data['active'], $data['module'], $data['storage_type'], $data['storage_active'], $data['storage_module'], $data['locked'], $data['cardinality'], $data['deleted']);
// Additionally, do not save the 'bundles' property populated by
// field_info_field().
unset($data['bundles']);
$record = array(
'field_name' => $field['field_name'],
'type' => $field['type'],
'module' => $field['module'],
'active' => $field['active'],
'storage_type' => $field['storage']['type'],
'storage_module' => $field['storage']['module'],
'storage_active' => $field['storage']['active'],
'locked' => $field['locked'],
'data' => $data,
'cardinality' => $field['cardinality'],
'translatable' => $field['translatable'],
'deleted' => $field['deleted'],
);
// Store the field and get the id back.
drupal_write_record('field_config', $record);
$field['id'] = $record['id'];
// Invoke hook_field_storage_create_field after the field is
// complete (e.g. it has its id).
try {
// Invoke hook_field_storage_create_field after
// drupal_write_record() sets the field id.
module_invoke($storage_type['module'], 'field_storage_create_field', $field);
}
catch (Exception $e) {
// If storage creation failed, remove the field_config record before
// rethrowing the exception.
db_delete('field_config')
->condition('id', $field['id'])
->execute();
throw $e;
}
// Clear caches
field_cache_clear();
// Invoke external hooks after the cache is cleared for API consistency.
module_invoke_all('field_create_field', $field);
return $field;
}
/**
* Updates a field.
*
* Any module may forbid any update for any reason. For example, the
* field's storage module might forbid an update if it would change
* the storage schema while data for the field exists. A field type
* module might forbid an update if it would change existing data's
* semantics, or if there are external dependencies on field settings
* that cannot be updated.
*
* @param $field
* A field structure. $field['field_name'] must provided; it
* identifies the field that will be updated to match this
* structure. Any other properties of the field that are not
* specified in $field will be left unchanged, so it is not
* necessary to pass in a fully populated $field structure.
* @return
* Throws a FieldException if the update cannot be performed.
* @see field_create_field()
*/
function field_update_field($field) {
// Check that the specified field exists.
$prior_field = field_read_field($field['field_name']);
if (empty($prior_field)) {
throw new FieldException('Attempt to update a non-existent field.');
}
// Use the prior field values for anything not specifically set by the new
// field to be sure that all values are set.
$field += $prior_field;
$field['settings'] += $prior_field['settings'];
// Some updates are always disallowed.
if ($field['type'] != $prior_field['type']) {
throw new FieldException("Cannot change an existing field's type.");
}
if ($field['entity_types'] != $prior_field['entity_types']) {
throw new FieldException("Cannot change an existing field's entity_types property.");
}
if ($field['storage']['type'] != $prior_field['storage']['type']) {
throw new FieldException("Cannot change an existing field's storage type.");
}
// Collect the new storage information, since what is in
// $prior_field may no longer be right.
module_load_install($field['module']);
$schema = (array) module_invoke($field['module'], 'field_schema', $field);
$schema += array('columns' => array(), 'indexes' => array(), 'foreign keys' => array());
// 'columns' are hardcoded in the field type.
$field['columns'] = $schema['columns'];
// 'foreign keys' are hardcoded in the field type.
$field['foreign keys'] = $schema['foreign keys'];
// 'indexes' can be both hardcoded in the field type, and specified in the
// incoming $field definition.
$field += array(
'indexes' => array(),
);
$field['indexes'] += $schema['indexes'];
$has_data = field_has_data($field);
// See if any module forbids the update by throwing an exception.
foreach (module_implements('field_update_forbid') as $module) {
$function = $module . '_field_update_forbid';
$function($field, $prior_field, $has_data);
}
// Tell the storage engine to update the field. Do this before
// saving the new definition since it still might fail.
$storage_type = field_info_storage_types($field['storage']['type']);
module_invoke($storage_type['module'], 'field_storage_update_field', $field, $prior_field, $has_data);
// Save the new field definition. @todo: refactor with
// field_create_field.
// The serialized 'data' column contains everything from $field that does not
// have its own column and is not automatically populated when the field is
// read.
$data = $field;
unset($data['columns'], $data['field_name'], $data['type'], $data['locked'], $data['module'], $data['cardinality'], $data['active'], $data['deleted']);
// Additionally, do not save the 'bundles' property populated by
// field_info_field().
unset($data['bundles']);
$field['data'] = $data;
// Store the field and create the id.
$primary_key = array('id');
drupal_write_record('field_config', $field, $primary_key);
// Clear caches
field_cache_clear();
// Invoke external hooks after the cache is cleared for API consistency.
module_invoke_all('field_update_field', $field, $prior_field, $has_data);
}
/**
* Reads a single field record directly from the database.
*
* Generally, you should use the field_info_field() instead.
*
* This function will not return deleted fields. Use
* field_read_fields() instead for this purpose.
*
* @param $field_name
* The field name to read.
* @param array $include_additional
* The default behavior of this function is to not return a field that
* is inactive. Setting
* $include_additional['include_inactive'] to TRUE will override this
* behavior.
* @return
* A field definition array, or FALSE.
*/
function field_read_field($field_name, $include_additional = array()) {
$fields = field_read_fields(array('field_name' => $field_name), $include_additional);
return $fields ? current($fields) : FALSE;
}
/**
* Reads in fields that match an array of conditions.
*
* @param array $params
* An array of conditions to match against. Keys are columns from the
* 'field_config' table, values are conditions to match. Additionally,
* conditions on the 'entity_type' and 'bundle' columns from the
* 'field_config_instance' table are supported (select fields having an
* instance on a given bundle).
* @param array $include_additional
* The default behavior of this function is to not return fields that
* are inactive or have been deleted. Setting
* $include_additional['include_inactive'] or
* $include_additional['include_deleted'] to TRUE will override this
* behavior.
* @return
* An array of fields matching $params. If
* $include_additional['include_deleted'] is TRUE, the array is keyed
* by field id, otherwise it is keyed by field name.
*/
function field_read_fields($params = array(), $include_additional = array()) {
$query = db_select('field_config', 'fc', array('fetch' => PDO::FETCH_ASSOC));
$query->fields('fc');
// Turn the conditions into a query.
foreach ($params as $key => $value) {
// Allow filtering on the 'entity_type' and 'bundle' columns of the
// field_config_instance table.
if ($key == 'entity_type' || $key == 'bundle') {
if (empty($fci_join)) {
$fci_join = $query->join('field_config_instance', 'fci', 'fc.id = fci.field_id');
}
$key = 'fci.' . $key;
}
else {
$key = 'fc.' . $key;
}
$query->condition($key, $value);
}
if (!isset($include_additional['include_inactive']) || !$include_additional['include_inactive']) {
$query
->condition('fc.active', 1)
->condition('fc.storage_active', 1);
}
$include_deleted = (isset($include_additional['include_deleted']) && $include_additional['include_deleted']);
if (!$include_deleted) {
$query->condition('fc.deleted', 0);
}
$fields = array();
$results = $query->execute();
foreach ($results as $record) {
$field = unserialize($record['data']);
$field['id'] = $record['id'];
$field['field_name'] = $record['field_name'];
$field['type'] = $record['type'];
$field['module'] = $record['module'];
$field['active'] = $record['active'];
$field['storage']['type'] = $record['storage_type'];
$field['storage']['module'] = $record['storage_module'];
$field['storage']['active'] = $record['storage_active'];
$field['locked'] = $record['locked'];
$field['cardinality'] = $record['cardinality'];
$field['translatable'] = $record['translatable'];
$field['deleted'] = $record['deleted'];
module_invoke_all('field_read_field', $field);
// Populate storage information.
module_load_install($field['module']);
$schema = (array) module_invoke($field['module'], 'field_schema', $field);
$schema += array('columns' => array(), 'indexes' => array());
$field['columns'] = $schema['columns'];
$field_name = $field['field_name'];
if ($include_deleted) {
$field_name = $field['id'];
}
$fields[$field_name] = $field;
}
return $fields;
}
/**
* Marks a field and its instances and data for deletion.
*
* @param $field_name
* The field name to delete.
*/
function field_delete_field($field_name) {
// Delete all non-deleted instances.
$field = field_info_field($field_name);
if (isset($field['bundles'])) {
foreach ($field['bundles'] as $entity_type => $bundles) {
foreach ($bundles as $bundle) {
$instance = field_info_instance($entity_type, $field_name, $bundle);
field_delete_instance($instance, FALSE);
}
}
}
// Mark field data for deletion.
module_invoke($field['storage']['module'], 'field_storage_delete_field', $field);
// Mark the field for deletion.
db_update('field_config')
->fields(array('deleted' => 1))
->condition('field_name', $field_name)
->execute();
// Clear the cache.
field_cache_clear();
module_invoke_all('field_delete_field', $field);
}
/**
* Creates an instance of a field, binding it to a bundle.
*
* @param $instance
* A field instance definition array. The field_name, entity_type and
* bundle properties are required. Other properties, if omitted,
* will be given the following default values:
* - label: the field name
* - description: empty string
* - required: FALSE
* - default_value_function: empty string
* - settings: each omitted setting is given the default value specified in
* hook_field_info().
* - widget:
* - type: the default widget specified in hook_field_info().
* - settings: each omitted setting is given the default value specified in
* hook_field_widget_info().
* - display:
* Settings for the 'default' view mode will be added if not present, and
* each view mode in the definition will be completed with the following
* default values:
* - label: 'above'
* - type: the default formatter specified in hook_field_info().
* - settings: each omitted setting is given the default value specified in
* hook_field_formatter_info().
* View modes not present in the definition are left empty, and the field
* will not be displayed in this mode.
*
* @return
* The $instance array with the id property filled in.
*
* @throws FieldException
*
* See: @link field Field API data structures @endlink.
*/
function field_create_instance($instance) {
$field = field_read_field($instance['field_name']);
if (empty($field)) {
throw new FieldException(t("Attempt to create an instance of a field @field_name that doesn't exist or is currently inactive.", array('@field_name' => $instance['field_name'])));
}
// Check that the required properties exists.
if (empty($instance['entity_type'])) {
throw new FieldException(t('Attempt to create an instance of field @field_name without an entity type.', array('@field_name' => $instance['field_name'])));
}
if (empty($instance['bundle'])) {
throw new FieldException(t('Attempt to create an instance of field @field_name without a bundle.', array('@field_name' => $instance['field_name'])));
}
// Check that the field can be attached to this entity type.
if (!empty($field['entity_types']) && !in_array($instance['entity_type'], $field['entity_types'])) {
throw new FieldException(t('Attempt to create an instance of field @field_name on forbidden entity type @entity_type.', array('@field_name' => $instance['field_name'], '@entity_type' => $instance['entity_type'])));
}
// Set the field id.
$instance['field_id'] = $field['id'];
// Note that we do *not* prevent creating a field on non-existing bundles,
// because that would break the 'Body as field' upgrade for contrib
// node types.
// TODO: Check that the widget type is known and can handle the field type ?
// TODO: Check that the formatters are known and can handle the field type ?
// TODO: Check that the display view modes are known for the entity type ?
// Those checks should probably happen in _field_write_instance() ?
// Problem : this would mean that a UI module cannot update an instance with a disabled formatter.
// Ensure the field instance is unique within the bundle.
// We only check for instances of active fields, since adding an instance of
// a disabled field is not supported.
$prior_instance = field_read_instance($instance['entity_type'], $instance['field_name'], $instance['bundle']);
if (!empty($prior_instance)) {
$message = t('Attempt to create an instance of field @field_name on bundle @bundle that already has an instance of that field.', array('@field_name' => $instance['field_name'], '@bundle' => $instance['bundle']));
throw new FieldException($message);
}
_field_write_instance($instance);
// Clear caches
field_cache_clear();
// Invoke external hooks after the cache is cleared for API consistency.
module_invoke_all('field_create_instance', $instance);
return $instance;
}
/**
* Updates an instance of a field.
*
* @param $instance
* An associative array representing an instance structure. The following
* required array elements specify which field instance is being updated:
* - entity_type: The type of the entity the field is attached to.
* - bundle: The bundle this field belongs to.
* - field_name: The name of an existing field.
* The other array elements represent properties of the instance, and all
* properties must be specified or their default values will be used (except
* internal-use properties, which are assigned automatically). To avoid
* losing the previously stored properties of the instance when making a
* change, first load the instance with field_info_instance(), then override
* the values you want to override, and finally save using this function.
* Example:
* @code
* // Fetch an instance info array.
* $instance_info = field_info_instance($entity_type, $field_name, $bundle_name);
* // Change a single property in the instance definition.
* $instance_info['required'] = TRUE;
* // Write the changed definition back.
* field_update_instance($instance_info);
* @endcode
*
* @throws FieldException
*
* @see field_info_instance()
* @see field_create_instance()
*/
function field_update_instance($instance) {
// Check that the specified field exists.
$field = field_read_field($instance['field_name']);
if (empty($field)) {
throw new FieldException(t('Attempt to update an instance of a nonexistent field @field.', array('@field' => $instance['field_name'])));
}
// Check that the field instance exists (even if it is inactive, since we
// want to be able to replace inactive widgets with new ones).
$prior_instance = field_read_instance($instance['entity_type'], $instance['field_name'], $instance['bundle'], array('include_inactive' => TRUE));
if (empty($prior_instance)) {
throw new FieldException(t("Attempt to update an instance of field @field on bundle @bundle that doesn't exist.", array('@field' => $instance['field_name'], '@bundle' => $instance['bundle'])));
}
$instance['id'] = $prior_instance['id'];
$instance['field_id'] = $prior_instance['field_id'];
_field_write_instance($instance, TRUE);
// Clear caches.
field_cache_clear();
module_invoke_all('field_update_instance', $instance, $prior_instance);
}
/**
* Stores an instance record in the field configuration database.
*
* @param $instance
* An instance structure.
* @param $update
* Whether this is a new or existing instance.
*/
function _field_write_instance($instance, $update = FALSE) {
$field = field_read_field($instance['field_name']);
$field_type = field_info_field_types($field['type']);
// Set defaults.
$instance += array(
'settings' => array(),
'display' => array(),
'widget' => array(),
'required' => FALSE,
'label' => $instance['field_name'],
'description' => '',
'deleted' => 0,
);
// Set default instance settings.
$instance['settings'] += field_info_instance_settings($field['type']);
// Set default widget and settings.
$instance['widget'] += array(
// TODO: what if no 'default_widget' specified ?
'type' => $field_type['default_widget'],
'settings' => array(),
);
// If no weight specified, make sure the field sinks at the bottom.
if (!isset($instance['widget']['weight'])) {
$max_weight = field_info_max_weight($instance['entity_type'], $instance['bundle'], 'form');
$instance['widget']['weight'] = isset($max_weight) ? $max_weight + 1 : 0;
}
// Check widget module.
$widget_type = field_info_widget_types($instance['widget']['type']);
$instance['widget']['module'] = $widget_type['module'];
$instance['widget']['settings'] += field_info_widget_settings($instance['widget']['type']);
// Make sure there are at least display settings for the 'default' view mode,
// and fill in defaults for each view mode specified in the definition.
$instance['display'] += array(
'default' => array(),
);
foreach ($instance['display'] as $view_mode => $display) {
$display += array(
'label' => 'above',
'type' => isset($field_type['default_formatter']) ? $field_type['default_formatter'] : 'hidden',
'settings' => array(),
);
if ($display['type'] != 'hidden') {
$formatter_type = field_info_formatter_types($display['type']);
$display['module'] = $formatter_type['module'];
$display['settings'] += field_info_formatter_settings($display['type']);
}
// If no weight specified, make sure the field sinks at the bottom.
if (!isset($display['weight'])) {
$max_weight = field_info_max_weight($instance['entity_type'], $instance['bundle'], $view_mode);
$display['weight'] = isset($max_weight) ? $max_weight + 1 : 0;
}
$instance['display'][$view_mode] = $display;
}
// The serialized 'data' column contains everything from $instance that does
// not have its own column and is not automatically populated when the
// instance is read.
$data = $instance;
unset($data['id'], $data['field_id'], $data['field_name'], $data['entity_type'], $data['bundle'], $data['deleted']);
$record = array(
'field_id' => $instance['field_id'],
'field_name' => $instance['field_name'],
'entity_type' => $instance['entity_type'],
'bundle' => $instance['bundle'],
'data' => $data,
'deleted' => $instance['deleted'],
);
// We need to tell drupal_update_record() the primary keys to trigger an
// update.
if ($update) {
$record['id'] = $instance['id'];
$primary_key = array('id');
}
else {
$primary_key = array();
}
drupal_write_record('field_config_instance', $record, $primary_key);
}
/**
* Reads a single instance record from the database.
*
* Generally, you should use field_info_instance() instead, as it
* provides caching and allows other modules the opportunity to
* append additional formatters, widgets, and other information.
*
* @param $entity_type
* The type of entity to which the field is bound.
* @param $field_name
* The field name to read.
* @param $bundle
* The bundle to which the field is bound.
* @param array $include_additional
* The default behavior of this function is to not return an instance that
* has been deleted, or whose field is inactive. Setting
* $include_additional['include_inactive'] or
* $include_additional['include_deleted'] to TRUE will override this
* behavior.
* @return
* An instance structure, or FALSE.
*/
function field_read_instance($entity_type, $field_name, $bundle, $include_additional = array()) {
$instances = field_read_instances(array('entity_type' => $entity_type, 'field_name' => $field_name, 'bundle' => $bundle), $include_additional);
return $instances ? current($instances) : FALSE;
}
/**
* Reads in field instances that match an array of conditions.
*
* @param $param
* An array of properties to use in selecting a field
* instance. Valid keys include any column of the
* field_config_instance table. If NULL, all instances will be returned.
* @param $include_additional
* The default behavior of this function is to not return field
* instances that have been marked deleted, or whose field is inactive.
* Setting $include_additional['include_inactive'] or
* $include_additional['include_deleted'] to TRUE will override this
* behavior.
* @return
* An array of instances matching the arguments.
*/
function field_read_instances($params = array(), $include_additional = array()) {
$include_inactive = isset($include_additional['include_inactive']) && $include_additional['include_inactive'];
$include_deleted = isset($include_additional['include_deleted']) && $include_additional['include_deleted'];
$query = db_select('field_config_instance', 'fci', array('fetch' => PDO::FETCH_ASSOC));
$query->join('field_config', 'fc', 'fc.id = fci.field_id');
$query->fields('fci');
// Turn the conditions into a query.
foreach ($params as $key => $value) {
$query->condition('fci.' . $key, $value);
}
if (!$include_inactive) {
$query
->condition('fc.active', 1)
->condition('fc.storage_active', 1);
}
if (!$include_deleted) {
$query->condition('fc.deleted', 0);
$query->condition('fci.deleted', 0);
}
$instances = array();
$query->orderBy('fci.id');
$results = $query->execute();
foreach ($results as $record) {
// Filter out instances on unknown entity types (for instance because the
// module exposing them was disabled).
$entity_info = entity_get_info($record['entity_type']);
if ($include_inactive || $entity_info) {
$instance = unserialize($record['data']);
$instance['id'] = $record['id'];
$instance['field_id'] = $record['field_id'];
$instance['field_name'] = $record['field_name'];
$instance['entity_type'] = $record['entity_type'];
$instance['bundle'] = $record['bundle'];
$instance['deleted'] = $record['deleted'];
module_invoke_all('field_read_instance', $instance);
$instances[] = $instance;
}
}
return $instances;
}
/**
* Marks a field instance and its data for deletion.
*
* @param $instance
* An instance structure.
* @param $field_cleanup
* If TRUE, the field will be deleted as well if its last instance is being
* deleted. If FALSE, it is the caller's responsibility to handle the case of
* fields left without instances. Defaults to TRUE.
*/
function field_delete_instance($instance, $field_cleanup = TRUE) {
// Mark the field instance for deletion.
db_update('field_config_instance')
->fields(array('deleted' => 1))
->condition('field_name', $instance['field_name'])
->condition('entity_type', $instance['entity_type'])
->condition('bundle', $instance['bundle'])
->execute();
// Clear the cache.
field_cache_clear();
// Mark instance data for deletion.
$field = field_info_field($instance['field_name']);
module_invoke($field['storage']['module'], 'field_storage_delete_instance', $instance);
// Let modules react to the deletion of the instance.
module_invoke_all('field_delete_instance', $instance);
// Delete the field itself if we just deleted its last instance.
if ($field_cleanup && count($field['bundles']) == 0) {
field_delete_field($field['field_name']);
}
}
/**
* @} End of "defgroup field_crud".
*/
/**
* @defgroup field_purge Field API bulk data deletion
* @{
* Clean up after Field API bulk deletion operations.
*
* Field API provides functions for deleting data attached to individual
* entities as well as deleting entire fields or field instances in a single
* operation.
*
* Deleting field data items for an entity with field_attach_delete() involves
* three separate operations:
* - Invoking the Field Type API hook_field_delete() for each field on the
* entity. The hook for each field type receives the entity and the specific
* field being deleted. A file field module might use this hook to delete
* uploaded files from the filesystem.
* - Invoking the Field Storage API hook_field_storage_delete() to remove
* data from the primary field storage. The hook implementation receives the
* entity being deleted and deletes data for all of the entity's bundle's
* fields.
* - Invoking the global Field Attach API hook_field_attach_delete() for all
* modules that implement it. Each hook implementation receives the entity
* being deleted and can operate on whichever subset of the entity's bundle's
* fields it chooses to.
*
* These hooks are invoked immediately when field_attach_delete() is
* called. Similar operations are performed for field_attach_delete_revision().
*
* When a field, bundle, or field instance is deleted, it is not practical to
* invoke these hooks immediately on every affected entity in a single page
* request; there could be thousands or millions of them. Instead, the
* appropriate field data items, instances, and/or fields are marked as deleted
* so that subsequent load or query operations will not return them. Later, a
* separate process cleans up, or "purges", the marked-as-deleted data by going
* through the three-step process described above and, finally, removing
* deleted field and instance records.
*
* Purging field data is made somewhat tricky by the fact that, while
* field_attach_delete() has a complete entity to pass to the various deletion
* hooks, the Field API purge process only has the field data it has previously
* stored. It cannot reconstruct complete original entities to pass to the
* deletion hooks. It is even possible that the original entity to which some
* Field API data was attached has been itself deleted before the field purge
* operation takes place.
*
* Field API resolves this problem by using "pseudo-entities" during purge
* operations. A pseudo-entity contains only the information from the original
* entity that Field API knows about: entity type, id, revision id, and
* bundle. It also contains the field data for whichever field instance is
* currently being purged. For example, suppose that the node type 'story' used
* to contain a field called 'subtitle' but the field was deleted. If node 37
* was a story with a subtitle, the pseudo-entity passed to the purge hooks
* would look something like this:
*
* @code
* $entity = stdClass Object(
* [nid] => 37,
* [vid] => 37,
* [type] => 'story',
* [subtitle] => array(
* [0] => array(
* 'value' => 'subtitle text',
* ),
* ),
* );
* @endcode
*
* See @link field Field API @endlink for information about the other parts of
* the Field API.
*/
/**
* Purges a batch of deleted Field API data, instances, or fields.
*
* This function will purge deleted field data in batches. The batch size
* is defined as an argument to the function, and once each batch is finished,
* it continues with the next batch until all have completed. If a deleted field
* instance with no remaining data records is found, the instance itself will
* be purged. If a deleted field with no remaining field instances is found, the
* field itself will be purged.
*
* @param $batch_size
* The maximum number of field data records to purge before returning.
*/
function field_purge_batch($batch_size) {
// Retrieve all deleted field instances. We cannot use field_info_instances()
// because that function does not return deleted instances.
$instances = field_read_instances(array('deleted' => 1), array('include_deleted' => 1));
foreach ($instances as $instance) {
// field_purge_data() will need the field array.
$field = field_info_field_by_id($instance['field_id']);
// Retrieve some entities.
$query = new EntityFieldQuery();
$results = $query
->fieldCondition($field)
->entityCondition('bundle', $instance['bundle'])
->deleted(TRUE)
->range(0, $batch_size)
->execute();
if ($results) {
foreach ($results as $entity_type => $stub_entities) {
field_attach_load($entity_type, $stub_entities, FIELD_LOAD_CURRENT, array('field_id' => $field['id'], 'deleted' => 1));
foreach ($stub_entities as $stub_entity) {
// Purge the data for the entity.
field_purge_data($entity_type, $stub_entity, $field, $instance);
}
}
}
else {
// No field data remains for the instance, so we can remove it.
field_purge_instance($instance);
}
}
// Retrieve all deleted fields. Any that have no instances can be purged.
$fields = field_read_fields(array('deleted' => 1), array('include_deleted' => 1));
foreach ($fields as $field) {
$instances = field_read_instances(array('field_id' => $field['id']), array('include_deleted' => 1));
if (empty($instances)) {
field_purge_field($field);
}
}
}
/**
* Purges the field data for a single field on a single pseudo-entity.
*
* This is basically the same as field_attach_delete() except it only applies
* to a single field. The entity itself is not being deleted, and it is quite
* possible that other field data will remain attached to it.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The pseudo-entity whose field data is being purged.
* @param $field
* The (possibly deleted) field whose data is being purged.
* @param $instance
* The deleted field instance whose data is being purged.
*/
function field_purge_data($entity_type, $entity, $field, $instance) {
// Each field type's hook_field_delete() only expects to operate on a single
// field at a time, so we can use it as-is for purging.
$options = array('field_id' => $instance['field_id'], 'deleted' => TRUE);
_field_invoke('delete', $entity_type, $entity, $dummy, $dummy, $options);
// Tell the field storage system to purge the data.
module_invoke($field['storage']['module'], 'field_storage_purge', $entity_type, $entity, $field, $instance);
// Let other modules act on purging the data.
foreach (module_implements('field_attach_purge') as $module) {
$function = $module . '_field_attach_purge';
$function($entity_type, $entity, $field, $instance);
}
}
/**
* Purges a field instance record from the database.
*
* This function assumes all data for the instance has already been purged, and
* should only be called by field_purge_batch().
*
* @param $instance
* The instance record to purge.
*/
function field_purge_instance($instance) {
db_delete('field_config_instance')
->condition('id', $instance['id'])
->execute();
// Notify the storage engine.
$field = field_info_field_by_id($instance['field_id']);
module_invoke($field['storage']['module'], 'field_storage_purge_instance', $instance);
// Clear the cache.
field_info_cache_clear();
// Invoke external hooks after the cache is cleared for API consistency.
module_invoke_all('field_purge_instance', $instance);
}
/**
* Purges a field record from the database.
*
* This function assumes all instances for the field has already been purged,
* and should only be called by field_purge_batch().
*
* @param $field
* The field record to purge.
*/
function field_purge_field($field) {
$instances = field_read_instances(array('field_id' => $field['id']), array('include_deleted' => 1));
if (count($instances) > 0) {
throw new FieldException(t('Attempt to purge a field @field_name that still has instances.', array('@field_name' => $field['field_name'])));
}
db_delete('field_config')
->condition('id', $field['id'])
->execute();
// Notify the storage engine.
module_invoke($field['storage']['module'], 'field_storage_purge_field', $field);
// Clear the cache.
field_info_cache_clear();
// Invoke external hooks after the cache is cleared for API consistency.
module_invoke_all('field_purge_field', $field);
}
/**
* @} End of "defgroup field_purge".
*/
{$field['field_name']}[$langcode], or an empty array if unset.
* @param $errors
* The array of errors, keyed by field name and by value delta, that have
* already been reported for the entity. The function should add its errors
* to this array. Each error is an associative array, with the following
* keys and values:
* - 'error': an error code (should be a string, prefixed with the module name)
* - 'message': the human readable message to be displayed.
*/
function field_default_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
// Filter out empty values.
$items = _field_filter_items($field, $items);
// Check that the number of values doesn't exceed the field cardinality.
// For form submitted values, this can only happen with 'multiple value'
// widgets.
if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && count($items) > $field['cardinality']) {
$errors[$field['field_name']][$langcode][0][] = array(
'error' => 'field_cardinality',
'message' => t('%name: this field cannot hold more than @count values.', array('%name' => $instance['label'], '@count' => $field['cardinality'])),
);
}
}
function field_default_submit($entity_type, $entity, $field, $instance, $langcode, &$items, $form, &$form_state) {
// Filter out empty values.
$items = _field_filter_items($field, $items);
// Reorder items to account for drag-n-drop reordering.
$items = _field_sort_items($field, $items);
}
/**
* Default field 'insert' operation.
*
* Insert default value if no $entity->$field_name entry was provided.
* This can happen with programmatic saves, or on form-based creation where
* the current user doesn't have 'edit' permission for the field.
*/
function field_default_insert($entity_type, $entity, $field, $instance, $langcode, &$items) {
// _field_invoke() populates $items with an empty array if the $entity has no
// entry for the field, so we check on the $entity itself.
// We also check that the current field translation is actually defined before
// assigning it a default value. This way we ensure that only the intended
// languages get a default value. Otherwise we could have default values for
// not yet open languages.
if (empty($entity) || !property_exists($entity, $field['field_name']) ||
(isset($entity->{$field['field_name']}[$langcode]) && count($entity->{$field['field_name']}[$langcode]) == 0)) {
$items = field_get_default_value($entity_type, $entity, $field, $instance, $langcode);
}
}
/**
* Invokes hook_field_formatter_prepare_view() on the relevant formatters.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entities
* An array of entities being displayed, keyed by entity id.
* @param $field
* The field structure for the operation.
* @param $instances
* Array of instance structures for $field for each entity, keyed by entity
* id.
* @param $langcode
* The language associated to $items.
* @param $items
* Array of field values already loaded for the entities, keyed by entity id.
* @param $display
* Can be either:
* - the name of a view mode
* - or an array of display settings to use for display, as found in the
* 'display' entry of $instance definitions.
*/
function field_default_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $display) {
// Group entities, instances and items by formatter module.
$modules = array();
foreach ($instances as $id => $instance) {
if (is_string($display)) {
$view_mode = $display;
$instance_display = field_get_display($instance, $view_mode, $entities[$id]);
}
else {
$instance_display = $display;
}
if ($instance_display['type'] !== 'hidden') {
$module = $instance_display['module'];
$modules[$module] = $module;
$grouped_entities[$module][$id] = $entities[$id];
$grouped_instances[$module][$id] = $instance;
$grouped_displays[$module][$id] = $instance_display;
// hook_field_formatter_prepare_view() alters $items by reference.
$grouped_items[$module][$id] = &$items[$id];
}
}
foreach ($modules as $module) {
// Invoke hook_field_formatter_prepare_view().
$function = $module . '_field_formatter_prepare_view';
if (function_exists($function)) {
$function($entity_type, $grouped_entities[$module], $field, $grouped_instances[$module], $langcode, $grouped_items[$module], $grouped_displays[$module]);
}
}
}
/**
* Builds a renderable array for one field on one entity instance.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* A single object of type $entity_type.
* @param $field
* The field structure for the operation.
* @param $instance
* An array containing each field on $entity's bundle.
* @param $langcode
* The language associated to $items.
* @param $items
* Array of field values already loaded for the entities, keyed by entity id.
* @param $display
* Can be either:
* - the name of a view mode;
* - or an array of custom display settings, as found in the 'display' entry
* of $instance definitions.
*/
function field_default_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
$addition = array();
// Prepare incoming display specifications.
if (is_string($display)) {
$view_mode = $display;
$display = field_get_display($instance, $view_mode, $entity);
}
else {
$view_mode = '_custom_display';
}
if ($display['type'] !== 'hidden') {
// Calling the formatter function through module_invoke() can have a
// performance impact on pages with many fields and values.
$function = $display['module'] . '_field_formatter_view';
if (function_exists($function)) {
$elements = $function($entity_type, $entity, $field, $instance, $langcode, $items, $display);
if ($elements) {
$info = array(
'#theme' => 'field',
'#weight' => $display['weight'],
'#title' => $instance['label'],
'#access' => field_access('view', $field, $entity_type, $entity),
'#label_display' => $display['label'],
'#view_mode' => $view_mode,
'#language' => $langcode,
'#field_name' => $field['field_name'],
'#field_type' => $field['type'],
'#field_translatable' => $field['translatable'],
'#entity_type' => $entity_type,
'#bundle' => $bundle,
'#object' => $entity,
'#items' => $items,
'#formatter' => $display['type']
);
$addition[$field['field_name']] = array_merge($info, $elements);
}
}
}
return $addition;
}
/**
* Copies source field values into the entity to be prepared.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The entity to be prepared for translation.
* @param $field
* The field structure for the operation.
* @param $instance
* The instance structure for $field on $entity's bundle.
* @param $langcode
* The language the entity has to be translated in.
* @param $items
* $entity->{$field['field_name']}[$langcode], or an empty array if unset.
* @param $source_entity
* The source entity holding the field values to be translated.
* @param $source_langcode
* The source language from which translate.
*/
function field_default_prepare_translation($entity_type, $entity, $field, $instance, $langcode, &$items, $source_entity, $source_langcode) {
$field_name = $field['field_name'];
// If the field is untranslatable keep using LANGUAGE_NONE.
if ($langcode == LANGUAGE_NONE) {
$source_langcode = LANGUAGE_NONE;
}
if (isset($source_entity->{$field_name}[$source_langcode])) {
$items = $source_entity->{$field_name}[$source_langcode];
}
}
$entity_type,
'entity' => $entity,
'field' => $field,
'instance' => $instance,
);
drupal_alter(array('field_widget_properties', 'field_widget_properties_' . $entity_type), $instance['widget'], $context);
// Collect widget elements.
$elements = array();
// Store field information in $form_state.
if (!field_form_get_state($parents, $field_name, $langcode, $form_state)) {
$field_state = array(
'field' => $field,
'instance' => $instance,
'items_count' => count($items),
'array_parents' => array(),
'errors' => array(),
);
field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
}
// If field module handles multiple values for this form element, and we are
// not displaying an individual element, process the multiple value form.
if (!isset($get_delta) && field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) {
// Store the entity in the form.
$form['#entity'] = $entity;
$elements = field_multiple_value_form($field, $instance, $langcode, $items, $form, $form_state);
}
// If the widget is handling multiple values (e.g Options), or if we are
// displaying an individual element, just get a single form element and make
// it the $delta value.
else {
$delta = isset($get_delta) ? $get_delta : 0;
$function = $instance['widget']['module'] . '_field_widget_form';
if (function_exists($function)) {
$element = array(
'#entity' => $entity,
'#entity_type' => $instance['entity_type'],
'#bundle' => $instance['bundle'],
'#field_name' => $field_name,
'#language' => $langcode,
'#field_parents' => $parents,
'#columns' => array_keys($field['columns']),
'#title' => check_plain($instance['label']),
'#description' => field_filter_xss($instance['description']),
// Only the first widget should be required.
'#required' => $delta == 0 && $instance['required'],
'#delta' => $delta,
);
if ($element = $function($form, $form_state, $field, $instance, $langcode, $items, $delta, $element)) {
// Allow modules to alter the field widget form element.
$context = array(
'form' => $form,
'field' => $field,
'instance' => $instance,
'langcode' => $langcode,
'items' => $items,
'delta' => $delta,
);
drupal_alter(array('field_widget_form', 'field_widget_' . $instance['widget']['type'] . '_form'), $element, $form_state, $context);
// If we're processing a specific delta value for a field where the
// field module handles multiples, set the delta in the result.
// For fields that handle their own processing, we can't make
// assumptions about how the field is structured, just merge in the
// returned element.
if (field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) {
$elements[$delta] = $element;
}
else {
$elements = $element;
}
}
}
}
// Also aid in theming of field widgets by rendering a classified container.
$addition[$field_name] = array(
'#type' => 'container',
'#attributes' => array(
'class' => array(
'field-type-' . drupal_html_class($field['type']),
'field-name-' . drupal_html_class($field_name),
'field-widget-' . drupal_html_class($instance['widget']['type']),
),
),
'#weight' => $instance['widget']['weight'],
);
// Populate the 'array_parents' information in $form_state['field'] after
// the form is built, so that we catch changes in the form structure performed
// in alter() hooks.
$elements['#after_build'][] = 'field_form_element_after_build';
$elements['#field_name'] = $field_name;
$elements['#language'] = $langcode;
$elements['#field_parents'] = $parents;
$addition[$field_name] += array(
'#tree' => TRUE,
// The '#language' key can be used to access the field's form element
// when $langcode is unknown.
'#language' => $langcode,
$langcode => $elements,
'#access' => field_access('edit', $field, $entity_type, $entity),
);
return $addition;
}
/**
* Special handling to create form elements for multiple values.
*
* Handles generic features for multiple fields:
* - number of widgets
* - AHAH-'add more' button
* - drag-n-drop value reordering
*/
function field_multiple_value_form($field, $instance, $langcode, $items, &$form, &$form_state) {
$field_name = $field['field_name'];
$parents = $form['#parents'];
// Determine the number of widgets to display.
switch ($field['cardinality']) {
case FIELD_CARDINALITY_UNLIMITED:
$field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
$max = $field_state['items_count'];
break;
default:
$max = $field['cardinality'] - 1;
break;
}
$title = check_plain($instance['label']);
$description = field_filter_xss($instance['description']);
$id_prefix = implode('-', array_merge($parents, array($field_name)));
$wrapper_id = drupal_html_id($id_prefix . '-add-more-wrapper');
$field_elements = array();
$function = $instance['widget']['module'] . '_field_widget_form';
if (function_exists($function)) {
for ($delta = 0; $delta <= $max; $delta++) {
$multiple = $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED;
$element = array(
'#entity_type' => $instance['entity_type'],
'#entity' => $form['#entity'],
'#bundle' => $instance['bundle'],
'#field_name' => $field_name,
'#language' => $langcode,
'#field_parents' => $parents,
'#columns' => array_keys($field['columns']),
'#title' => $title,
'#description' => $description,
// Only the first widget should be required.
'#required' => $delta == 0 && $instance['required'],
'#delta' => $delta,
'#weight' => $delta,
);
// For multiple fields, title and description are handled by the wrapping
// table.
if ($multiple) {
if ($delta == 0) {
$element['#title'] = $title;
}
else {
$element['#title'] = t('!title (value @number)', array('@number' => $delta + 1, '!title' => $title));
}
$element['#title_display'] = 'invisible';
$element['#description'] = '';
}
if ($element = $function($form, $form_state, $field, $instance, $langcode, $items, $delta, $element)) {
// Input field for the delta (drag-n-drop reordering).
if ($multiple) {
// We name the element '_weight' to avoid clashing with elements
// defined by widget.
$element['_weight'] = array(
'#type' => 'weight',
'#title' => t('Weight for row @number', array('@number' => $delta + 1)),
'#title_display' => 'invisible',
// Note: this 'delta' is the FAPI 'weight' element's property.
'#delta' => $max,
'#default_value' => isset($items[$delta]['_weight']) ? $items[$delta]['_weight'] : $delta,
'#weight' => 100,
);
}
// Allow modules to alter the field widget form element.
$context = array(
'form' => $form,
'field' => $field,
'instance' => $instance,
'langcode' => $langcode,
'items' => $items,
'delta' => $delta,
);
drupal_alter(array('field_widget_form', 'field_widget_' . $instance['widget']['type'] . '_form'), $element, $form_state, $context);
$field_elements[$delta] = $element;
}
}
if ($field_elements) {
$field_elements += array(
'#theme' => 'field_multiple_value_form',
'#field_name' => $field['field_name'],
'#cardinality' => $field['cardinality'],
'#title' => $title,
'#required' => $instance['required'],
'#description' => $description,
'#prefix' => '',
'#suffix' => '
',
'#max_delta' => $max,
);
// Add 'add more' button, if not working with a programmed form.
if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED && empty($form_state['programmed'])) {
$field_elements['add_more'] = array(
'#type' => 'submit',
'#name' => strtr($id_prefix, '-', '_') . '_add_more',
'#value' => t('Add another item'),
'#attributes' => array('class' => array('field-add-more-submit')),
'#limit_validation_errors' => array(array_merge($parents, array($field_name, $langcode))),
'#submit' => array('field_add_more_submit'),
'#ajax' => array(
'callback' => 'field_add_more_js',
'wrapper' => $wrapper_id,
'effect' => 'fade',
),
);
}
}
}
return $field_elements;
}
/**
* Returns HTML for an individual form element.
*
* Combine multiple values into a table with drag-n-drop reordering.
* TODO : convert to a template.
*
* @param $variables
* An associative array containing:
* - element: A render element representing the form element.
*
* @ingroup themeable
*/
function theme_field_multiple_value_form($variables) {
$element = $variables['element'];
$output = '';
if ($element['#cardinality'] > 1 || $element['#cardinality'] == FIELD_CARDINALITY_UNLIMITED) {
$table_id = drupal_html_id($element['#field_name'] . '_values');
$order_class = $element['#field_name'] . '-delta-order';
$required = !empty($element['#required']) ? theme('form_required_marker', $variables) : '';
$header = array(
array(
'data' => '' . t('!title !required', array('!title' => $element['#title'], '!required' => $required)) . " ",
'colspan' => 2,
'class' => array('field-label'),
),
t('Order'),
);
$rows = array();
// Sort items according to '_weight' (needed when the form comes back after
// preview or failed validation)
$items = array();
foreach (element_children($element) as $key) {
if ($key === 'add_more') {
$add_more_button = &$element[$key];
}
else {
$items[] = &$element[$key];
}
}
usort($items, '_field_sort_items_value_helper');
// Add the items as table rows.
foreach ($items as $key => $item) {
$item['_weight']['#attributes']['class'] = array($order_class);
$delta_element = drupal_render($item['_weight']);
$cells = array(
array('data' => '', 'class' => array('field-multiple-drag')),
drupal_render($item),
array('data' => $delta_element, 'class' => array('delta-order')),
);
$rows[] = array(
'data' => $cells,
'class' => array('draggable'),
);
}
$output = '';
drupal_add_tabledrag($table_id, 'order', 'sibling', $order_class);
}
else {
foreach (element_children($element) as $key) {
$output .= drupal_render($element[$key]);
}
}
return $output;
}
/**
* #after_build callback for field elements in a form.
*
* This stores the final location of the field within the form structure so
* that field_default_form_errors() can assign validation errors to the right
* form element.
*
* @see field_default_form_errors()
*/
function field_form_element_after_build($element, &$form_state) {
$parents = $element['#field_parents'];
$field_name = $element['#field_name'];
$langcode = $element['#language'];
$field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
$field_state['array_parents'] = $element['#array_parents'];
field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
return $element;
}
/**
* Transfer field-level validation errors to widgets.
*/
function field_default_form_errors($entity_type, $entity, $field, $instance, $langcode, $items, $form, &$form_state) {
$field_state = field_form_get_state($form['#parents'], $field['field_name'], $langcode, $form_state);
if (!empty($field_state['errors'])) {
// Locate the correct element in the form.
$element = drupal_array_get_nested_value($form_state['complete form'], $field_state['array_parents']);
// Only set errors if the element is accessible.
if (!isset($element['#access']) || $element['#access']) {
$function = $instance['widget']['module'] . '_field_widget_error';
$function_exists = function_exists($function);
$multiple_widget = field_behaviors_widget('multiple values', $instance) != FIELD_BEHAVIOR_DEFAULT;
foreach ($field_state['errors'] as $delta => $delta_errors) {
// For multiple single-value widgets, pass errors by delta.
// For a multiple-value widget, pass all errors to the main widget.
$error_element = $multiple_widget ? $element : $element[$delta];
foreach ($delta_errors as $error) {
if ($function_exists) {
$function($error_element, $error, $form, $form_state);
}
else {
// Make sure that errors are reported (even incorrectly flagged) if
// the widget module fails to implement hook_field_widget_error().
form_error($error_element, $error['message']);
}
}
}
// Reinitialize the errors list for the next submit.
$field_state['errors'] = array();
field_form_set_state($form['#parents'], $field['field_name'], $langcode, $form_state, $field_state);
}
}
}
/**
* Submit handler for the "Add another item" button of a field form.
*
* This handler is run regardless of whether JS is enabled or not. It makes
* changes to the form state. If the button was clicked with JS disabled, then
* the page is reloaded with the complete rebuilt form. If the button was
* clicked with JS enabled, then ajax_form_callback() calls field_add_more_js()
* to return just the changed part of the form.
*/
function field_add_more_submit($form, &$form_state) {
$button = $form_state['triggering_element'];
// Go one level up in the form, to the widgets container.
$element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -1));
$field_name = $element['#field_name'];
$langcode = $element['#language'];
$parents = $element['#field_parents'];
// Increment the items count.
$field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
$field_state['items_count']++;
field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
$form_state['rebuild'] = TRUE;
}
/**
* Ajax callback in response to a new empty widget being added to the form.
*
* This returns the new page content to replace the page content made obsolete
* by the form submission.
*
* @see field_add_more_submit()
*/
function field_add_more_js($form, $form_state) {
$button = $form_state['triggering_element'];
// Go one level up in the form, to the widgets container.
$element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -1));
$field_name = $element['#field_name'];
$langcode = $element['#language'];
$parents = $element['#field_parents'];
$field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
$field = $field_state['field'];
if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED) {
return;
}
// Add a DIV around the delta receiving the Ajax effect.
$delta = $element['#max_delta'];
$element[$delta]['#prefix'] = '' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
$element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '
';
return $element;
}
/**
* Retrieves processing information about a field from $form_state.
*
* @param $parents
* The array of #parents where the field lives in the form.
* @param $field_name
* The field name.
* @param $langcode
* The language in which the field values are entered.
* @param $form_state
* The form state.
*
* @return
* An array with the following key/data pairs:
* - field: the field definition array,
* - instance: the field instance definition array,
* - items_count: the number of widgets to display for the field,
* - array_parents: the location of the field's widgets within the $form
* structure. This entry is populated at '#after_build' time.
* - errors: the array of field validation errors reported on the field. This
* entry is populated at field_attach_form_validate() time.
*
* @see field_form_set_state()
*/
function field_form_get_state($parents, $field_name, $langcode, &$form_state) {
$form_state_parents = _field_form_state_parents($parents, $field_name, $langcode);
return drupal_array_get_nested_value($form_state, $form_state_parents);
}
/**
* Stores processing information about a field in $form_state.
*
* @param $parents
* The array of #parents where the field lives in the form.
* @param $field_name
* The field name.
* @param $langcode
* The language in which the field values are entered.
* @param $form_state
* The form state.
* @param $field_state
* The array of data to store. See field_form_get_state() for the structure
* and content of the array.
*
* @see field_form_get_state()
*/
function field_form_set_state($parents, $field_name, $langcode, &$form_state, $field_state) {
$form_state_parents = _field_form_state_parents($parents, $field_name, $langcode);
drupal_array_set_nested_value($form_state, $form_state_parents, $field_state);
}
/**
* Returns the location of processing information within $form_state.
*/
function _field_form_state_parents($parents, $field_name, $langcode) {
// To ensure backwards compatibility on regular entity forms for widgets that
// still access $form_state['field'][$field_name] directly,
// - top-level fields (empty $parents) are placed directly under
// $form_state['fields'][$field_name].
// - Other fields are placed under
// $form_state['field']['#parents'][...$parents...]['#fields'][$field_name]
// to avoid clashes between field names and $parents parts.
// @todo Remove backwards compatibility in Drupal 8, and use a unique
// $form_state['field'][...$parents...]['#fields'][$field_name] structure.
if (!empty($parents)) {
$form_state_parents = array_merge(array('#parents'), $parents, array('#fields'));
}
else {
$form_state_parents = array();
}
$form_state_parents = array_merge(array('field'), $form_state_parents, array($field_name, $langcode));
return $form_state_parents;
}
/**
* Retrieves the field definition for a widget's helper callbacks.
*
* Widgets helper element callbacks (such as #process, #element_validate,
* #value_callback, ...) should use field_widget_field() and
* field_widget_instance() instead of field_info_field() and
* field_info_instance() when they need to access field or instance properties.
* See hook_field_widget_form() for more details.
*
* @param $element
* The structured array for the widget.
* @param $form_state
* The form state.
*
* @return
* The $field definition array for the current widget.
*
* @see field_widget_instance()
* @see hook_field_widget_form()
*/
function field_widget_field($element, $form_state) {
$field_state = field_form_get_state($element['#field_parents'], $element['#field_name'], $element['#language'], $form_state);
return $field_state['field'];
}
/**
* Retrieves the instance definition array for a widget's helper callbacks.
*
* Widgets helper element callbacks (such as #process, #element_validate,
* #value_callback, ...) should use field_widget_field() and
* field_widget_instance() instead of field_info_field() and
* field_info_instance() when they need to access field or instance properties.
* See hook_field_widget_form() for more details.
*
* @param $element
* The structured array for the widget.
* @param $form_state
* The form state.
*
* @return
* The $instance definition array for the current widget.
*
* @see field_widget_field()
* @see hook_field_widget_form()
*/
function field_widget_instance($element, $form_state) {
$field_state = field_form_get_state($element['#field_parents'], $element['#field_name'], $element['#language'], $form_state);
return $field_state['instance'];
}
name = Field
description = Field API to add fields to entities like nodes and users.
package = Core
version = VERSION
core = 7.x
files[] = field.module
files[] = field.attach.inc
files[] = field.info.class.inc
files[] = tests/field.test
dependencies[] = field_sql_storage
required = TRUE
stylesheets[all][] = theme/field.css
; Information added by Drupal.org packaging script on 2023-03-15
version = "7.95"
project = "drupal"
datestamp = "1678902530"
fieldMap = NULL;
$this->fieldsById = array();
$this->fieldIdsByName = array();
$this->loadedAllFields = FALSE;
$this->unknownFields = array();
$this->bundleInstances = array();
$this->loadedAllInstances = FALSE;
$this->emptyBundles = array();
$this->bundleExtraFields = array();
cache_clear_all('field_info:', 'cache_field', TRUE);
}
/**
* Collects a lightweight map of fields across bundles.
*
* @return
* An array keyed by field name. Each value is an array with two entries:
* - type: The field type.
* - bundles: The bundles in which the field appears, as an array with
* entity types as keys and the array of bundle names as values.
*/
public function getFieldMap() {
// Read from the "static" cache.
if ($this->fieldMap !== NULL) {
return $this->fieldMap;
}
// Read from persistent cache.
if ($cached = cache_get('field_info:field_map', 'cache_field')) {
$map = $cached->data;
// Save in "static" cache.
$this->fieldMap = $map;
return $map;
}
$map = array();
$query = db_query('SELECT fc.type, fci.field_name, fci.entity_type, fci.bundle FROM {field_config_instance} fci INNER JOIN {field_config} fc ON fc.id = fci.field_id WHERE fc.active = 1 AND fc.storage_active = 1 AND fc.deleted = 0 AND fci.deleted = 0 ORDER BY bundle, entity_type');
foreach ($query as $row) {
$map[$row->field_name]['bundles'][$row->entity_type][] = $row->bundle;
$map[$row->field_name]['type'] = $row->type;
}
// Save in "static" and persistent caches.
$this->fieldMap = $map;
if (lock_acquire('field_info:field_map')) {
cache_set('field_info:field_map', $map, 'cache_field');
lock_release('field_info:field_map');
}
return $map;
}
/**
* Returns all active fields, including deleted ones.
*
* @return
* An array of field definitions, keyed by field ID.
*/
public function getFields() {
// Read from the "static" cache.
if ($this->loadedAllFields) {
return $this->fieldsById;
}
// Read from persistent cache.
if ($cached = cache_get('field_info:fields', 'cache_field')) {
$this->fieldsById = $cached->data;
}
else {
// Collect and prepare fields.
foreach (field_read_fields(array(), array('include_deleted' => TRUE)) as $field) {
$this->fieldsById[$field['id']] = $this->prepareField($field);
}
// Store in persistent cache.
if (lock_acquire('field_info:fields')) {
cache_set('field_info:fields', $this->fieldsById, 'cache_field');
lock_release('field_info:fields');
}
}
// Fill the name/ID map.
foreach ($this->fieldsById as $field) {
if (!$field['deleted']) {
$this->fieldIdsByName[$field['field_name']] = $field['id'];
}
}
$this->loadedAllFields = TRUE;
return $this->fieldsById;
}
/**
* Retrieves all active, non-deleted instances definitions.
*
* @param $entity_type
* (optional) The entity type.
*
* @return
* If $entity_type is not set, all instances keyed by entity type and bundle
* name. If $entity_type is set, all instances for that entity type, keyed
* by bundle name.
*/
public function getInstances($entity_type = NULL) {
// If the full list is not present in "static" cache yet.
if (!$this->loadedAllInstances) {
// Read from persistent cache.
if ($cached = cache_get('field_info:instances', 'cache_field')) {
$this->bundleInstances = $cached->data;
}
else {
// Collect and prepare instances.
// We also need to populate the static field cache, since it will not
// be set by subsequent getBundleInstances() calls.
$this->getFields();
// Initialize empty arrays for all existing entity types and bundles.
// This is not strictly needed, but is done to preserve the behavior of
// field_info_instances() before http://drupal.org/node/1915646.
foreach (field_info_bundles() as $existing_entity_type => $bundles) {
foreach ($bundles as $bundle => $bundle_info) {
$this->bundleInstances[$existing_entity_type][$bundle] = array();
}
}
foreach (field_read_instances() as $instance) {
$field = $this->getField($instance['field_name']);
$instance = $this->prepareInstance($instance, $field['type']);
$this->bundleInstances[$instance['entity_type']][$instance['bundle']][$instance['field_name']] = $instance;
}
// Store in persistent cache.
if (lock_acquire('field_info:instances')) {
cache_set('field_info:instances', $this->bundleInstances, 'cache_field');
lock_release('field_info:instances');
}
}
$this->loadedAllInstances = TRUE;
}
if (isset($entity_type)) {
return isset($this->bundleInstances[$entity_type]) ? $this->bundleInstances[$entity_type] : array();
}
else {
return $this->bundleInstances;
}
}
/**
* Returns a field definition from a field name.
*
* This method only retrieves active, non-deleted fields.
*
* @param $field_name
* The field name.
*
* @return
* The field definition, or NULL if no field was found.
*/
public function getField($field_name) {
// Read from the "static" cache.
if (isset($this->fieldIdsByName[$field_name])) {
$field_id = $this->fieldIdsByName[$field_name];
return $this->fieldsById[$field_id];
}
if (isset($this->unknownFields[$field_name])) {
return;
}
// Do not check the (large) persistent cache, but read the definition.
// Cache miss: read from definition.
if ($field = field_read_field($field_name)) {
$field = $this->prepareField($field);
// Save in the "static" cache.
$this->fieldsById[$field['id']] = $field;
$this->fieldIdsByName[$field['field_name']] = $field['id'];
return $field;
}
else {
$this->unknownFields[$field_name] = TRUE;
}
}
/**
* Returns a field definition from a field ID.
*
* This method only retrieves active fields, deleted or not.
*
* @param $field_id
* The field ID.
*
* @return
* The field definition, or NULL if no field was found.
*/
public function getFieldById($field_id) {
// Read from the "static" cache.
if (isset($this->fieldsById[$field_id])) {
return $this->fieldsById[$field_id];
}
if (isset($this->unknownFields[$field_id])) {
return;
}
// No persistent cache, fields are only persistently cached as part of a
// bundle.
// Cache miss: read from definition.
if ($fields = field_read_fields(array('id' => $field_id), array('include_deleted' => TRUE))) {
$field = current($fields);
$field = $this->prepareField($field);
// Store in the static cache.
$this->fieldsById[$field['id']] = $field;
if (!$field['deleted']) {
$this->fieldIdsByName[$field['field_name']] = $field['id'];
}
return $field;
}
else {
$this->unknownFields[$field_id] = TRUE;
}
}
/**
* Retrieves the instances for a bundle.
*
* The function also populates the corresponding field definitions in the
* "static" cache.
*
* @param $entity_type
* The entity type.
* @param $bundle
* The bundle name.
*
* @return
* The array of instance definitions, keyed by field name.
*/
public function getBundleInstances($entity_type, $bundle) {
// Read from the "static" cache.
if (isset($this->bundleInstances[$entity_type][$bundle])) {
return $this->bundleInstances[$entity_type][$bundle];
}
if (isset($this->emptyBundles[$entity_type][$bundle])) {
return array();
}
// Read from the persistent cache.
if ($cached = cache_get("field_info:bundle:$entity_type:$bundle", 'cache_field')) {
$info = $cached->data;
// Extract the field definitions and save them in the "static" cache.
foreach ($info['fields'] as $field) {
if (!isset($this->fieldsById[$field['id']])) {
$this->fieldsById[$field['id']] = $field;
if (!$field['deleted']) {
$this->fieldIdsByName[$field['field_name']] = $field['id'];
}
}
}
unset($info['fields']);
// Store the instance definitions in the "static" cache'. Empty (or
// non-existent) bundles are stored separately, so that they do not
// pollute the global list returned by getInstances().
if ($info['instances']) {
$this->bundleInstances[$entity_type][$bundle] = $info['instances'];
}
else {
$this->emptyBundles[$entity_type][$bundle] = TRUE;
}
return $info['instances'];
}
// Cache miss: collect from the definitions.
$instances = array();
// Collect the fields in the bundle.
$params = array('entity_type' => $entity_type, 'bundle' => $bundle);
$fields = field_read_fields($params);
// This iterates on non-deleted instances, so deleted fields are kept out of
// the persistent caches.
foreach (field_read_instances($params) as $instance) {
$field = $fields[$instance['field_name']];
$instance = $this->prepareInstance($instance, $field['type']);
$instances[$field['field_name']] = $instance;
// If the field is not in our global "static" list yet, add it.
if (!isset($this->fieldsById[$field['id']])) {
$field = $this->prepareField($field);
$this->fieldsById[$field['id']] = $field;
$this->fieldIdsByName[$field['field_name']] = $field['id'];
}
}
// Store in the 'static' cache'. Empty (or non-existent) bundles are stored
// separately, so that they do not pollute the global list returned by
// getInstances().
if ($instances) {
$this->bundleInstances[$entity_type][$bundle] = $instances;
}
else {
$this->emptyBundles[$entity_type][$bundle] = TRUE;
}
// The persistent cache additionally contains the definitions of the fields
// involved in the bundle.
$cache = array(
'instances' => $instances,
'fields' => array()
);
foreach ($instances as $instance) {
$cache['fields'][] = $this->fieldsById[$instance['field_id']];
}
if (lock_acquire("field_info:bundle:$entity_type:$bundle")) {
cache_set("field_info:bundle:$entity_type:$bundle", $cache, 'cache_field');
lock_release("field_info:bundle:$entity_type:$bundle");
}
return $instances;
}
/**
* Retrieves the "extra fields" for a bundle.
*
* @param $entity_type
* The entity type.
* @param $bundle
* The bundle name.
*
* @return
* The array of extra fields.
*/
public function getBundleExtraFields($entity_type, $bundle) {
// Read from the "static" cache.
if (isset($this->bundleExtraFields[$entity_type][$bundle])) {
return $this->bundleExtraFields[$entity_type][$bundle];
}
// Read from the persistent cache.
if ($cached = cache_get("field_info:bundle_extra:$entity_type:$bundle", 'cache_field')) {
$this->bundleExtraFields[$entity_type][$bundle] = $cached->data;
return $this->bundleExtraFields[$entity_type][$bundle];
}
// Cache miss: read from hook_field_extra_fields(). Note: given the current
// shape of the hook, we have no other way than collecting extra fields on
// all bundles.
$info = array();
$extra = module_invoke_all('field_extra_fields');
drupal_alter('field_extra_fields', $extra);
// Merge in saved settings.
if (isset($extra[$entity_type][$bundle])) {
$info = $this->prepareExtraFields($extra[$entity_type][$bundle], $entity_type, $bundle);
}
// Store in the 'static' and persistent caches.
$this->bundleExtraFields[$entity_type][$bundle] = $info;
if (lock_acquire("field_info:bundle_extra:$entity_type:$bundle")) {
cache_set("field_info:bundle_extra:$entity_type:$bundle", $info, 'cache_field');
lock_release("field_info:bundle_extra:$entity_type:$bundle");
}
return $this->bundleExtraFields[$entity_type][$bundle];
}
/**
* Prepares a field definition for the current run-time context.
*
* @param $field
* The raw field structure as read from the database.
*
* @return
* The field definition completed for the current runtime context.
*/
public function prepareField($field) {
// Make sure all expected field settings are present.
$field['settings'] += field_info_field_settings($field['type']);
$field['storage']['settings'] += field_info_storage_settings($field['storage']['type']);
// Add storage details.
$details = (array) module_invoke($field['storage']['module'], 'field_storage_details', $field);
drupal_alter('field_storage_details', $details, $field);
$field['storage']['details'] = $details;
// Populate the list of bundles using the field.
$field['bundles'] = array();
if (!$field['deleted']) {
$map = $this->getFieldMap();
if (isset($map[$field['field_name']])) {
$field['bundles'] = $map[$field['field_name']]['bundles'];
}
}
return $field;
}
/**
* Prepares an instance definition for the current run-time context.
*
* @param $instance
* The raw instance structure as read from the database.
* @param $field_type
* The field type.
*
* @return
* The field instance array completed for the current runtime context.
*/
public function prepareInstance($instance, $field_type) {
// Make sure all expected instance settings are present.
$instance['settings'] += field_info_instance_settings($field_type);
// Set a default value for the instance.
if (field_behaviors_widget('default value', $instance) == FIELD_BEHAVIOR_DEFAULT && !isset($instance['default_value'])) {
$instance['default_value'] = NULL;
}
// Prepare widget settings.
$instance['widget'] = $this->prepareInstanceWidget($instance['widget'], $field_type);
// Prepare display settings.
foreach ($instance['display'] as $view_mode => $display) {
$instance['display'][$view_mode] = $this->prepareInstanceDisplay($display, $field_type);
}
// Fall back to 'hidden' for view modes configured to use custom display
// settings, and for which the instance has no explicit settings.
$entity_info = entity_get_info($instance['entity_type']);
$view_modes = array_merge(array('default'), array_keys($entity_info['view modes']));
$view_mode_settings = field_view_mode_settings($instance['entity_type'], $instance['bundle']);
foreach ($view_modes as $view_mode) {
if ($view_mode == 'default' || !empty($view_mode_settings[$view_mode]['custom_settings'])) {
if (!isset($instance['display'][$view_mode])) {
$instance['display'][$view_mode] = array(
'type' => 'hidden',
'label' => 'above',
'settings' => array(),
'weight' => 0,
);
}
}
}
return $instance;
}
/**
* Prepares widget properties for the current run-time context.
*
* @param $widget
* Widget specifications as found in $instance['widget'].
* @param $field_type
* The field type.
*
* @return
* The widget properties completed for the current runtime context.
*/
public function prepareInstanceWidget($widget, $field_type) {
$field_type_info = field_info_field_types($field_type);
// Fill in default values.
$widget += array(
'type' => $field_type_info['default_widget'],
'settings' => array(),
'weight' => 0,
);
$widget_type_info = field_info_widget_types($widget['type']);
// Fall back to default formatter if formatter type is not available.
if (!$widget_type_info) {
$widget['type'] = $field_type_info['default_widget'];
$widget_type_info = field_info_widget_types($widget['type']);
}
$widget['module'] = $widget_type_info['module'];
// Fill in default settings for the widget.
$widget['settings'] += field_info_widget_settings($widget['type']);
return $widget;
}
/**
* Adapts display specifications to the current run-time context.
*
* @param $display
* Display specifications as found in $instance['display']['a_view_mode'].
* @param $field_type
* The field type.
*
* @return
* The display properties completed for the current runtime context.
*/
public function prepareInstanceDisplay($display, $field_type) {
$field_type_info = field_info_field_types($field_type);
// Fill in default values.
$display += array(
'label' => 'above',
'settings' => array(),
'weight' => 0,
);
if (empty($display['type'])) {
$display['type'] = $field_type_info['default_formatter'];
}
if ($display['type'] != 'hidden') {
$formatter_type_info = field_info_formatter_types($display['type']);
// Fall back to default formatter if formatter type is not available.
if (!$formatter_type_info) {
$display['type'] = $field_type_info['default_formatter'];
$formatter_type_info = field_info_formatter_types($display['type']);
}
$display['module'] = $formatter_type_info['module'];
// Fill in default settings for the formatter.
$display['settings'] += field_info_formatter_settings($display['type']);
}
return $display;
}
/**
* Prepares 'extra fields' for the current run-time context.
*
* @param $extra_fields
* The array of extra fields, as collected in hook_field_extra_fields().
* @param $entity_type
* The entity type.
* @param $bundle
* The bundle name.
*
* @return
* The list of extra fields completed for the current runtime context.
*/
public function prepareExtraFields($extra_fields, $entity_type, $bundle) {
$entity_type_info = entity_get_info($entity_type);
$bundle_settings = field_bundle_settings($entity_type, $bundle);
$extra_fields += array('form' => array(), 'display' => array());
$result = array();
// Extra fields in forms.
foreach ($extra_fields['form'] as $name => $field_data) {
$settings = isset($bundle_settings['extra_fields']['form'][$name]) ? $bundle_settings['extra_fields']['form'][$name] : array();
if (isset($settings['weight'])) {
$field_data['weight'] = $settings['weight'];
}
$result['form'][$name] = $field_data;
}
// Extra fields in displayed entities.
$data = $extra_fields['display'];
foreach ($extra_fields['display'] as $name => $field_data) {
$settings = isset($bundle_settings['extra_fields']['display'][$name]) ? $bundle_settings['extra_fields']['display'][$name] : array();
$view_modes = array_merge(array('default'), array_keys($entity_type_info['view modes']));
foreach ($view_modes as $view_mode) {
if (isset($settings[$view_mode])) {
$field_data['display'][$view_mode] = $settings[$view_mode];
}
else {
$field_data['display'][$view_mode] = array(
'weight' => $field_data['weight'],
'visible' => TRUE,
);
}
}
unset($field_data['weight']);
$result['display'][$name] = $field_data;
}
return $result;
}
}
flush();
}
/**
* Collates all information on existing fields and instances.
*
* Deprecated. This function is kept to ensure backwards compatibility, but has
* a serious performance impact, and should be absolutely avoided.
* See http://drupal.org/node/1915646.
*
* Use the regular field_info_*() API functions to access the information, or
* field_info_cache_clear() to clear the cached data.
*/
function _field_info_collate_fields($reset = FALSE) {
if ($reset) {
_field_info_field_cache()->flush();
return;
}
$cache = _field_info_field_cache();
// Collect fields, and build the array of IDs keyed by field_name.
$fields = $cache->getFields();
$field_ids = array();
foreach ($fields as $id => $field) {
if (!$field['deleted']) {
$field_ids[$field['field_name']] = $id;
}
}
// Collect extra fields for all entity types.
$extra_fields = array();
foreach (field_info_bundles() as $entity_type => $bundles) {
foreach ($bundles as $bundle => $info) {
$extra_fields[$entity_type][$bundle] = $cache->getBundleExtraFields($entity_type, $bundle);
}
}
return array(
'fields' => $fields,
'field_ids' => $field_ids,
'instances' => $cache->getInstances(),
'extra_fields' => $extra_fields,
);
}
/**
* Collates all information on field types, widget types and related structures.
*
* @param $reset
* If TRUE, clear the cache. The information will be rebuilt from the database
* next time it is needed. Defaults to FALSE.
*
* @return
* If $reset is TRUE, nothing.
* If $reset is FALSE, an array containing the following elements:
* - 'field types': Array of hook_field_info() results, keyed by field_type.
* Each element has the following components: label, description, settings,
* instance_settings, default_widget, default_formatter, and behaviors
* from hook_field_info(), as well as module, giving the module that exposes
* the field type.
* - 'widget types': Array of hook_field_widget_info() results, keyed by
* widget_type. Each element has the following components: label, field
* types, settings, weight, and behaviors from hook_field_widget_info(),
* as well as module, giving the module that exposes the widget type.
* - 'formatter types': Array of hook_field_formatter_info() results, keyed by
* formatter_type. Each element has the following components: label, field
* types, and behaviors from hook_field_formatter_info(), as well as
* module, giving the module that exposes the formatter type.
* - 'storage types': Array of hook_field_storage_info() results, keyed by
* storage type names. Each element has the following components: label,
* description, and settings from hook_field_storage_info(), as well as
* module, giving the module that exposes the storage type.
* - 'fieldable types': Array of hook_entity_info() results, keyed by
* entity_type. Each element has the following components: name, id key,
* revision key, bundle key, cacheable, and bundles from hook_entity_info(),
* as well as module, giving the module that exposes the entity type.
*/
function _field_info_collate_types($reset = FALSE) {
global $language;
static $info;
// The _info() hooks invoked below include translated strings, so each
// language is cached separately.
$langcode = $language->language;
if ($reset) {
$info = NULL;
// Clear all languages.
cache_clear_all('field_info_types:', 'cache_field', TRUE);
return;
}
if (!isset($info)) {
if ($cached = cache_get("field_info_types:$langcode", 'cache_field')) {
$info = $cached->data;
}
else {
$info = array(
'field types' => array(),
'widget types' => array(),
'formatter types' => array(),
'storage types' => array(),
);
// Populate field types.
foreach (module_implements('field_info') as $module) {
$field_types = (array) module_invoke($module, 'field_info');
foreach ($field_types as $name => $field_info) {
// Provide defaults.
$field_info += array(
'settings' => array(),
'instance_settings' => array(),
);
$info['field types'][$name] = $field_info;
$info['field types'][$name]['module'] = $module;
}
}
drupal_alter('field_info', $info['field types']);
// Populate widget types.
foreach (module_implements('field_widget_info') as $module) {
$widget_types = (array) module_invoke($module, 'field_widget_info');
foreach ($widget_types as $name => $widget_info) {
// Provide defaults.
$widget_info += array(
'settings' => array(),
);
$info['widget types'][$name] = $widget_info;
$info['widget types'][$name]['module'] = $module;
}
}
drupal_alter('field_widget_info', $info['widget types']);
uasort($info['widget types'], 'drupal_sort_weight');
// Populate formatter types.
foreach (module_implements('field_formatter_info') as $module) {
$formatter_types = (array) module_invoke($module, 'field_formatter_info');
foreach ($formatter_types as $name => $formatter_info) {
// Provide defaults.
$formatter_info += array(
'settings' => array(),
);
$info['formatter types'][$name] = $formatter_info;
$info['formatter types'][$name]['module'] = $module;
}
}
drupal_alter('field_formatter_info', $info['formatter types']);
// Populate storage types.
foreach (module_implements('field_storage_info') as $module) {
$storage_types = (array) module_invoke($module, 'field_storage_info');
foreach ($storage_types as $name => $storage_info) {
// Provide defaults.
$storage_info += array(
'settings' => array(),
);
$info['storage types'][$name] = $storage_info;
$info['storage types'][$name]['module'] = $module;
}
}
drupal_alter('field_storage_info', $info['storage types']);
// Set the cache if we can acquire a lock.
if (lock_acquire("field_info_types:$langcode")) {
cache_set("field_info_types:$langcode", $info, 'cache_field');
lock_release("field_info_types:$langcode");
}
}
}
return $info;
}
/**
* Prepares a field definition for the current run-time context.
*
* The functionality has moved to the FieldInfo class. This function is kept as
* a backwards-compatibility layer. See http://drupal.org/node/1915646.
*
* @see FieldInfo::prepareField()
*/
function _field_info_prepare_field($field) {
$cache = _field_info_field_cache();
return $cache->prepareField($field);
}
/**
* Prepares an instance definition for the current run-time context.
*
* The functionality has moved to the FieldInfo class. This function is kept as
* a backwards-compatibility layer. See http://drupal.org/node/1915646.
*
* @see FieldInfo::prepareInstance()
*/
function _field_info_prepare_instance($instance, $field) {
$cache = _field_info_field_cache();
return $cache->prepareInstance($instance, $field['type']);
}
/**
* Adapts display specifications to the current run-time context.
*
* The functionality has moved to the FieldInfo class. This function is kept as
* a backwards-compatibility layer. See http://drupal.org/node/1915646.
*
* @see FieldInfo::prepareInstanceDisplay()
*/
function _field_info_prepare_instance_display($field, $display) {
$cache = _field_info_field_cache();
return $cache->prepareInstanceDisplay($display, $field['type']);
}
/**
* Prepares widget specifications for the current run-time context.
*
* The functionality has moved to the FieldInfo class. This function is kept as
* a backwards-compatibility layer. See http://drupal.org/node/1915646.
*
* @see FieldInfo::prepareInstanceWidget()
*/
function _field_info_prepare_instance_widget($field, $widget) {
$cache = _field_info_field_cache();
return $cache->prepareInstanceWidget($widget, $field['type']);
}
/**
* Prepares 'extra fields' for the current run-time context.
*
* The functionality has moved to the FieldInfo class. This function is kept as
* a backwards-compatibility layer. See http://drupal.org/node/1915646.
*
* @see FieldInfo::prepareExtraFields()
*/
function _field_info_prepare_extra_fields($extra_fields, $entity_type, $bundle) {
$cache = _field_info_field_cache();
return $cache->prepareExtraFields($extra_fields, $entity_type, $bundle);
}
/**
* Determines the behavior of a widget with respect to an operation.
*
* @param $op
* The name of the operation. Currently supported: 'default value',
* 'multiple values'.
* @param $instance
* The field instance array.
*
* @return
* One of these values:
* - FIELD_BEHAVIOR_NONE: Do nothing for this operation.
* - FIELD_BEHAVIOR_CUSTOM: Use the widget's callback function.
* - FIELD_BEHAVIOR_DEFAULT: Use field.module default behavior.
*/
function field_behaviors_widget($op, $instance) {
$info = field_info_widget_types($instance['widget']['type']);
return isset($info['behaviors'][$op]) ? $info['behaviors'][$op] : FIELD_BEHAVIOR_DEFAULT;
}
/**
* Returns information about field types from hook_field_info().
*
* @param $field_type
* (optional) A field type name. If omitted, all field types will be
* returned.
*
* @return
* Either a field type description, as provided by hook_field_info(), or an
* array of all existing field types, keyed by field type name.
*/
function field_info_field_types($field_type = NULL) {
$info = _field_info_collate_types();
$field_types = $info['field types'];
if ($field_type) {
if (isset($field_types[$field_type])) {
return $field_types[$field_type];
}
}
else {
return $field_types;
}
}
/**
* Returns information about field widgets from hook_field_widget_info().
*
* @param $widget_type
* (optional) A widget type name. If omitted, all widget types will be
* returned.
*
* @return
* Either a single widget type description, as provided by
* hook_field_widget_info(), or an array of all existing widget types, keyed
* by widget type name.
*/
function field_info_widget_types($widget_type = NULL) {
$info = _field_info_collate_types();
$widget_types = $info['widget types'];
if ($widget_type) {
if (isset($widget_types[$widget_type])) {
return $widget_types[$widget_type];
}
}
else {
return $widget_types;
}
}
/**
* Returns information about field formatters from hook_field_formatter_info().
*
* @param $formatter_type
* (optional) A formatter type name. If omitted, all formatter types will be
* returned.
*
* @return
* Either a single formatter type description, as provided by
* hook_field_formatter_info(), or an array of all existing formatter types,
* keyed by formatter type name.
*/
function field_info_formatter_types($formatter_type = NULL) {
$info = _field_info_collate_types();
$formatter_types = $info['formatter types'];
if ($formatter_type) {
if (isset($formatter_types[$formatter_type])) {
return $formatter_types[$formatter_type];
}
}
else {
return $formatter_types;
}
}
/**
* Returns information about field storage from hook_field_storage_info().
*
* @param $storage_type
* (optional) A storage type name. If omitted, all storage types will be
* returned.
*
* @return
* Either a storage type description, as provided by
* hook_field_storage_info(), or an array of all existing storage types,
* keyed by storage type name.
*/
function field_info_storage_types($storage_type = NULL) {
$info = _field_info_collate_types();
$storage_types = $info['storage types'];
if ($storage_type) {
if (isset($storage_types[$storage_type])) {
return $storage_types[$storage_type];
}
}
else {
return $storage_types;
}
}
/**
* Returns information about existing bundles.
*
* @param $entity_type
* The type of entity; e.g. 'node' or 'user'.
*
* @return
* An array of bundles for the $entity_type keyed by bundle name,
* or, if no $entity_type was provided, the array of all existing bundles,
* keyed by entity type.
*/
function field_info_bundles($entity_type = NULL) {
$info = entity_get_info();
if ($entity_type) {
return isset($info[$entity_type]['bundles']) ? $info[$entity_type]['bundles'] : array();
}
$bundles = array();
foreach ($info as $type => $entity_info) {
$bundles[$type] = $entity_info['bundles'];
}
return $bundles;
}
/**
* Returns a lightweight map of fields across bundles.
*
* The function only returns active, non deleted fields.
*
* @return
* An array keyed by field name. Each value is an array with two entries:
* - type: The field type.
* - bundles: The bundles in which the field appears, as an array with entity
* types as keys and the array of bundle names as values.
* Example:
* @code
* array(
* 'body' => array(
* 'bundles' => array(
* 'node' => array('page', 'article'),
* ),
* 'type' => 'text_with_summary',
* ),
* );
* @endcode
*/
function field_info_field_map() {
$cache = _field_info_field_cache();
return $cache->getFieldMap();
}
/**
* Returns all field definitions.
*
* Use of this function should be avoided when possible, since it loads and
* statically caches a potentially large array of information. Use
* field_info_field_map() instead.
*
* When iterating over the fields present in a given bundle after a call to
* field_info_instances($entity_type, $bundle), it is recommended to use
* field_info_field() on each individual field instead.
*
* @return
* An array of field definitions, keyed by field name. Each field has an
* additional property, 'bundles', which is an array of all the bundles to
* which this field belongs keyed by entity type.
*
* @see field_info_field_map()
*/
function field_info_fields() {
$cache = _field_info_field_cache();
$info = $cache->getFields();
$fields = array();
foreach ($info as $key => $field) {
if (!$field['deleted']) {
$fields[$field['field_name']] = $field;
}
}
return $fields;
}
/**
* Returns data about an individual field, given a field name.
*
* @param $field_name
* The name of the field to retrieve. $field_name can only refer to a
* non-deleted, active field. For deleted fields, use
* field_info_field_by_id(). To retrieve information about inactive fields,
* use field_read_fields().
*
* @return
* The field array, as returned by field_read_fields(), with an
* additional element 'bundles', whose value is an array of all the bundles
* this field belongs to keyed by entity type. NULL if the field was not
* found.
*
* @see field_info_field_by_id()
*/
function field_info_field($field_name) {
$cache = _field_info_field_cache();
return $cache->getField($field_name);
}
/**
* Returns data about an individual field, given a field ID.
*
* @param $field_id
* The id of the field to retrieve. $field_id can refer to a
* deleted field, but not an inactive one.
*
* @return
* The field array, as returned by field_read_fields(), with an
* additional element 'bundles', whose value is an array of all the bundles
* this field belongs to.
*
* @see field_info_field()
*/
function field_info_field_by_id($field_id) {
$cache = _field_info_field_cache();
return $cache->getFieldById($field_id);
}
/**
* Returns the same data as field_info_field_by_id() for every field.
*
* Use of this function should be avoided when possible, since it loads and
* statically caches a potentially large array of information.
*
* When iterating over the fields present in a given bundle after a call to
* field_info_instances($entity_type, $bundle), it is recommended to use
* field_info_field() on each individual field instead.
*
* @return
* An array, each key is a field ID and the values are field arrays as
* returned by field_read_fields(), with an additional element 'bundles',
* whose value is an array of all the bundle this field belongs to.
*
* @see field_info_field()
* @see field_info_field_by_id()
*/
function field_info_field_by_ids() {
$cache = _field_info_field_cache();
return $cache->getFields();
}
/**
* Retrieves information about field instances.
*
* Use of this function to retrieve instances across separate bundles (i.e.
* when the $bundle parameter is NULL) should be avoided when possible, since
* it loads and statically caches a potentially large array of information. Use
* field_info_field_map() instead.
*
* When retrieving the instances of a specific bundle (i.e. when both
* $entity_type and $bundle_name are provided), the function also populates a
* static cache with the corresponding field definitions, allowing fast
* retrieval of field_info_field() later in the request.
*
* @param $entity_type
* (optional) The entity type for which to return instances.
* @param $bundle_name
* (optional) The bundle name for which to return instances. If $entity_type
* is NULL, the $bundle_name parameter is ignored.
*
* @return
* If $entity_type is not set, return all instances keyed by entity type and
* bundle name. If $entity_type is set, return all instances for that entity
* type, keyed by bundle name. If $entity_type and $bundle_name are set, return
* all instances for that bundle.
*
* @see field_info_field_map()
*/
function field_info_instances($entity_type = NULL, $bundle_name = NULL) {
$cache = _field_info_field_cache();
if (!isset($entity_type)) {
return $cache->getInstances();
}
if (!isset($bundle_name)) {
return $cache->getInstances($entity_type);
}
return $cache->getBundleInstances($entity_type, $bundle_name);
}
/**
* Returns an array of instance data for a specific field and bundle.
*
* The function populates a static cache with all fields and instances used in
* the bundle, allowing fast retrieval of field_info_field() or
* field_info_instance() later in the request.
*
* @param $entity_type
* The entity type for the instance.
* @param $field_name
* The field name for the instance.
* @param $bundle_name
* The bundle name for the instance.
*
* @return
* An associative array of instance data for the specific field and bundle;
* NULL if the instance does not exist.
*/
function field_info_instance($entity_type, $field_name, $bundle_name) {
$cache = _field_info_field_cache();
$info = $cache->getBundleInstances($entity_type, $bundle_name);
if (isset($info[$field_name])) {
return $info[$field_name];
}
}
/**
* Returns a list and settings of pseudo-field elements in a given bundle.
*
* If $context is 'form', an array with the following structure:
* @code
* array(
* 'name_of_pseudo_field_component' => array(
* 'label' => The human readable name of the component,
* 'description' => A short description of the component content,
* 'weight' => The weight of the component in edit forms,
* ),
* 'name_of_other_pseudo_field_component' => array(
* // ...
* ),
* );
* @endcode
*
* If $context is 'display', an array with the following structure:
* @code
* array(
* 'name_of_pseudo_field_component' => array(
* 'label' => The human readable name of the component,
* 'description' => A short description of the component content,
* // One entry per view mode, including the 'default' mode:
* 'display' => array(
* 'default' => array(
* 'weight' => The weight of the component in displayed entities in
* this view mode,
* 'visible' => TRUE if the component is visible, FALSE if hidden, in
* displayed entities in this view mode,
* ),
* 'teaser' => array(
* // ...
* ),
* ),
* ),
* 'name_of_other_pseudo_field_component' => array(
* // ...
* ),
* );
* @endcode
*
* @param $entity_type
* The type of entity; e.g. 'node' or 'user'.
* @param $bundle
* The bundle name.
* @param $context
* The context for which the list of pseudo-fields is requested. Either
* 'form' or 'display'.
*
* @return
* The array of pseudo-field elements in the bundle.
*/
function field_info_extra_fields($entity_type, $bundle, $context) {
$cache = _field_info_field_cache();
$info = $cache->getBundleExtraFields($entity_type, $bundle);
return isset($info[$context]) ? $info[$context] : array();
}
/**
* Returns the maximum weight of all the components in an entity.
*
* This includes fields, 'extra_fields', and other components added by
* third-party modules (e.g. field_group).
*
* @param $entity_type
* The type of entity; e.g. 'node' or 'user'.
* @param $bundle
* The bundle name.
* @param $context
* The context for which the maximum weight is requested. Either 'form', or
* the name of a view mode.
* @return
* The maximum weight of the entity's components, or NULL if no components
* were found.
*/
function field_info_max_weight($entity_type, $bundle, $context) {
$weights = array();
// Collect weights for fields.
foreach (field_info_instances($entity_type, $bundle) as $instance) {
if ($context == 'form') {
$weights[] = $instance['widget']['weight'];
}
elseif (isset($instance['display'][$context]['weight'])) {
$weights[] = $instance['display'][$context]['weight'];
}
}
// Collect weights for extra fields.
foreach (field_info_extra_fields($entity_type, $bundle, $context) as $extra) {
$weights[] = $extra['weight'];
}
// Let other modules feedback about their own additions.
$weights = array_merge($weights, module_invoke_all('field_info_max_weight', $entity_type, $bundle, $context));
$max_weight = $weights ? max($weights) : NULL;
return $max_weight;
}
/**
* Returns a field type's default settings.
*
* @param $type
* A field type name.
*
* @return
* The field type's default settings, as provided by hook_field_info(), or an
* empty array if type or settings are not defined.
*/
function field_info_field_settings($type) {
$info = field_info_field_types($type);
return isset($info['settings']) ? $info['settings'] : array();
}
/**
* Returns a field type's default instance settings.
*
* @param $type
* A field type name.
*
* @return
* The field type's default instance settings, as provided by
* hook_field_info(), or an empty array if type or settings are not defined.
*/
function field_info_instance_settings($type) {
$info = field_info_field_types($type);
return isset($info['instance_settings']) ? $info['instance_settings'] : array();
}
/**
* Returns a field widget's default settings.
*
* @param $type
* A widget type name.
*
* @return
* The widget type's default settings, as provided by
* hook_field_widget_info(), or an empty array if type or settings are
* undefined.
*/
function field_info_widget_settings($type) {
$info = field_info_widget_types($type);
return isset($info['settings']) ? $info['settings'] : array();
}
/**
* Returns a field formatter's default settings.
*
* @param $type
* A field formatter type name.
*
* @return
* The formatter type's default settings, as provided by
* hook_field_formatter_info(), or an empty array if type or settings are
* undefined.
*/
function field_info_formatter_settings($type) {
$info = field_info_formatter_types($type);
return isset($info['settings']) ? $info['settings'] : array();
}
/**
* Returns a field storage type's default settings.
*
* @param $type
* A field storage type name.
*
* @return
* The storage type's default settings, as provided by
* hook_field_storage_info(), or an empty array if type or settings are
* undefined.
*/
function field_info_storage_settings($type) {
$info = field_info_storage_types($type);
return isset($info['settings']) ? $info['settings'] : array();
}
/**
* @} End of "defgroup field_info".
*/
array(
'id' => array(
'type' => 'serial',
'not null' => TRUE,
'description' => 'The primary identifier for a field',
),
'field_name' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'description' => 'The name of this field. Non-deleted field names are unique, but multiple deleted fields can have the same name.',
),
'type' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'description' => 'The type of this field.',
),
'module' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The module that implements the field type.',
),
'active' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 0,
'description' => 'Boolean indicating whether the module that implements the field type is enabled.',
),
'storage_type' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'description' => 'The storage backend for the field.',
),
'storage_module' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The module that implements the storage backend.',
),
'storage_active' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 0,
'description' => 'Boolean indicating whether the module that implements the storage backend is enabled.',
),
'locked' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 0,
'description' => '@TODO',
),
'data' => array(
'type' => 'blob',
'size' => 'big',
'not null' => TRUE,
'serialize' => TRUE,
'description' => 'Serialized data containing the field properties that do not warrant a dedicated column.',
),
'cardinality' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 0,
),
'translatable' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 0,
),
'deleted' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('id'),
'indexes' => array(
'field_name' => array('field_name'),
// Used by field_sync_field_status().
'active' => array('active'),
'storage_active' => array('storage_active'),
'deleted' => array('deleted'),
// Used by field_modules_disabled().
'module' => array('module'),
'storage_module' => array('storage_module'),
'type' => array('type'),
'storage_type' => array('storage_type'),
),
);
$schema['field_config_instance'] = array(
'fields' => array(
'id' => array(
'type' => 'serial',
'not null' => TRUE,
'description' => 'The primary identifier for a field instance',
),
'field_id' => array(
'type' => 'int',
'not null' => TRUE,
'description' => 'The identifier of the field attached by this instance',
),
'field_name' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => ''
),
'entity_type' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => ''
),
'bundle' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => ''
),
'data' => array(
'type' => 'blob',
'size' => 'big',
'not null' => TRUE,
'serialize' => TRUE,
),
'deleted' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('id'),
'indexes' => array(
// Used by field_delete_instance().
'field_name_bundle' => array('field_name', 'entity_type', 'bundle'),
// Used by field_read_instances().
'deleted' => array('deleted'),
),
);
$schema['cache_field'] = drupal_get_schema_unprocessed('system', 'cache');
$schema['cache_field']['description'] = 'Cache table for the Field module to store already built field information.';
return $schema;
}
/**
* Utility function: create a field by writing directly to the database.
*
* This function can be used for databases whose schema is at field module
* version 7000 or higher.
*
* @ingroup update_api
*/
function _update_7000_field_create_field(&$field) {
// Merge in default values.`
$field += array(
'entity_types' => array(),
'cardinality' => 1,
'translatable' => FALSE,
'locked' => FALSE,
'settings' => array(),
'indexes' => array(),
'deleted' => 0,
'active' => 1,
);
// Set storage.
$field['storage'] = array(
'type' => 'field_sql_storage',
'settings' => array(),
'module' => 'field_sql_storage',
'active' => 1,
);
// Fetch the field schema to initialize columns and indexes. The field module
// is not guaranteed to be loaded at this point.
module_load_install($field['module']);
$schema = (array) module_invoke($field['module'], 'field_schema', $field);
$schema += array('columns' => array(), 'indexes' => array());
// 'columns' are hardcoded in the field type.
$field['columns'] = $schema['columns'];
// 'indexes' can be both hardcoded in the field type, and specified in the
// incoming $field definition.
$field['indexes'] += $schema['indexes'];
// The serialized 'data' column contains everything from $field that does not
// have its own column and is not automatically populated when the field is
// read.
$data = $field;
unset($data['columns'], $data['field_name'], $data['type'], $data['active'], $data['module'], $data['storage_type'], $data['storage_active'], $data['storage_module'], $data['locked'], $data['cardinality'], $data['deleted']);
// Additionally, do not save the 'bundles' property populated by
// field_info_field().
unset($data['bundles']);
// Write the field to the database.
$record = array(
'field_name' => $field['field_name'],
'type' => $field['type'],
'module' => $field['module'],
'active' => (int) $field['active'],
'storage_type' => $field['storage']['type'],
'storage_module' => $field['storage']['module'],
'storage_active' => (int) $field['storage']['active'],
'locked' => (int) $field['locked'],
'data' => serialize($data),
'cardinality' => $field['cardinality'],
'translatable' => (int) $field['translatable'],
'deleted' => (int) $field['deleted'],
);
// We don't use drupal_write_record() here because it depends on the schema.
$field['id'] = db_insert('field_config')
->fields($record)
->execute();
// Create storage for the field.
field_sql_storage_field_storage_create_field($field);
}
/**
* Utility function: delete a field stored in SQL storage directly from the database.
*
* To protect user data, this function can only be used to delete fields once
* all information it stored is gone. Delete all data from the
* field_data_$field_name table before calling by either manually issuing
* delete queries against it or using _update_7000_field_delete_instance().
*
* This function can be used for databases whose schema is at field module
* version 7000 or higher.
*
* @param $field_name
* The field name to delete.
*
* @ingroup update_api
*/
function _update_7000_field_delete_field($field_name) {
$table_name = 'field_data_' . $field_name;
if (db_select($table_name)->range(0, 1)->countQuery()->execute()->fetchField()) {
$t = get_t();
throw new Exception($t('This function can only be used to delete fields without data'));
}
// Delete all instances.
db_delete('field_config_instance')
->condition('field_name', $field_name)
->execute();
// Nuke field data and revision tables.
db_drop_table($table_name);
db_drop_table('field_revision_' . $field_name);
// Delete the field.
db_delete('field_config')
->condition('field_name', $field_name)
->execute();
}
/**
* Utility function: delete an instance and all its data of a field stored in SQL Storage.
*
* BEWARE: this function deletes user data from the field storage tables.
*
* This function is valid for a database schema version 7000.
*
* @ingroup update_api
*/
function _update_7000_field_delete_instance($field_name, $entity_type, $bundle) {
// Delete field instance configuration data.
db_delete('field_config_instance')
->condition('field_name', $field_name)
->condition('entity_type', $entity_type)
->condition('bundle', $bundle)
->execute();
// Nuke data.
db_delete('field_data_' . $field_name)
->condition('entity_type', $entity_type)
->condition('bundle', $bundle)
->execute();
db_delete('field_revision_' . $field_name)
->condition('entity_type', $entity_type)
->condition('bundle', $bundle)
->execute();
}
/**
* Utility function: fetch all the field definitions from the database.
*
* Warning: unlike the field_read_fields() API function, this function returns
* all fields by default, including deleted and inactive fields, unless
* specified otherwise in the $conditions parameter.
*
* @param $conditions
* An array of conditions to limit the select query to.
* @param $key
* The name of the field property the return array is indexed by. Using
* anything else than 'id' might cause incomplete results if the $conditions
* do not filter out deleted fields.
*
* @return
* An array of fields matching $conditions, keyed by the property specified
* by the $key parameter.
*
* @ingroup update_api
*/
function _update_7000_field_read_fields(array $conditions = array(), $key = 'id') {
$fields = array();
$query = db_select('field_config', 'fc', array('fetch' => PDO::FETCH_ASSOC))
->fields('fc');
foreach ($conditions as $column => $value) {
$query->condition($column, $value);
}
foreach ($query->execute() as $record) {
$field = unserialize($record['data']);
$field['id'] = $record['id'];
$field['field_name'] = $record['field_name'];
$field['type'] = $record['type'];
$field['module'] = $record['module'];
$field['active'] = $record['active'];
$field['storage']['type'] = $record['storage_type'];
$field['storage']['module'] = $record['storage_module'];
$field['storage']['active'] = $record['storage_active'];
$field['locked'] = $record['locked'];
$field['cardinality'] = $record['cardinality'];
$field['translatable'] = $record['translatable'];
$field['deleted'] = $record['deleted'];
$fields[$field[$key]] = $field;
}
return $fields;
}
/**
* Utility function: write a field instance directly to the database.
*
* This function can be used for databases whose schema is at field module
* version 7000 or higher.
*
* @ingroup update_api
*/
function _update_7000_field_create_instance($field, &$instance) {
// Merge in defaults.
$instance += array(
'field_id' => $field['id'],
'field_name' => $field['field_name'],
'deleted' => 0,
);
// The serialized 'data' column contains everything from $instance that does
// not have its own column and is not automatically populated when the
// instance is read.
$data = $instance;
unset($data['id'], $data['field_id'], $data['field_name'], $data['entity_type'], $data['bundle'], $data['deleted']);
$record = array(
'field_id' => $instance['field_id'],
'field_name' => $instance['field_name'],
'entity_type' => $instance['entity_type'],
'bundle' => $instance['bundle'],
'data' => serialize($data),
'deleted' => (int) $instance['deleted'],
);
$instance['id'] = db_insert('field_config_instance')
->fields($record)
->execute();
}
/**
* @addtogroup updates-6.x-to-7.x
* @{
*/
/**
* Field update version placeholder.
*/
function field_update_7000() {
// Some update helper functions (such as _update_7000_field_create_field())
// modify the database directly. They can be used safely only if the database
// schema matches the field module schema established for Drupal 7.0 (i.e.
// version 7000). This function exists solely to set the schema version to
// 7000, so that update functions calling those helpers can do so safely
// by declaring a dependency on field_update_7000().
}
/**
* Fix fields definitions created during the d6 to d7 upgrade path.
*/
function field_update_7001() {
$fields = _update_7000_field_read_fields();
foreach ($fields as $field) {
// _update_7000_field_create_field() was broken in d7 RC2, and the fields
// created during a d6 to d7 upgrade do not correcly store the 'index'
// entry. See http://drupal.org/node/996160.
module_load_install($field['module']);
$schema = (array) module_invoke($field['module'], 'field_schema', $field);
$schema += array('indexes' => array());
// 'indexes' can be both hardcoded in the field type, and specified in the
// incoming $field definition.
$field['indexes'] += $schema['indexes'];
// Place the updated entries in the existing serialized 'data' column.
$data = db_query("SELECT data FROM {field_config} WHERE id = :id", array(':id' => $field['id']))->fetchField();
$data = unserialize($data);
$data['columns'] = $field['columns'];
$data['indexes'] = $field['indexes'];
// Save the new data.
$query = db_update('field_config')
->condition('id', $field['id'])
->fields(array('data' => serialize($data)))
->execute();
}
}
/**
* @} End of "addtogroup updates-6.x-to-7.x".
*/
/**
* @addtogroup updates-7.x-extra
* @{
*/
/**
* Split the all-inclusive field_bundle_settings variable per bundle.
*/
function field_update_7002() {
$settings = variable_get('field_bundle_settings', array());
if ($settings) {
foreach ($settings as $entity_type => $entity_type_settings) {
foreach ($entity_type_settings as $bundle => $bundle_settings) {
variable_set('field_bundle_settings_' . $entity_type . '__' . $bundle, $bundle_settings);
}
}
variable_del('field_bundle_settings');
}
}
/**
* Add the FieldInfo class to the class registry.
*/
function field_update_7003() {
// Empty update to force a rebuild of the registry.
}
/**
* Grant the new "administer fields" permission to trusted users.
*/
function field_update_7004() {
// Assign the permission to anyone that already has a trusted core permission
// that would have previously let them administer fields on an entity type.
$rids = array();
$permissions = array(
'administer site configuration',
'administer content types',
'administer users',
);
foreach ($permissions as $permission) {
$rids = array_merge($rids, array_keys(user_roles(FALSE, $permission)));
}
$rids = array_unique($rids);
foreach ($rids as $rid) {
_update_7000_user_role_grant_permissions($rid, array('administer fields'), 'field');
}
}
/**
* @} End of "addtogroup updates-7.x-extra".
*/
$field_name. Maximum length is 32 characters.
* - type (string): The type of the field, such as 'text' or 'image'. Field
* types are defined by modules that implement hook_field_info().
* - entity_types (array): The array of entity types that can hold instances
* of this field. If empty or not specified, the field can have instances
* in any entity type.
* - cardinality (integer): The number of values the field can hold. Legal
* values are any positive integer or FIELD_CARDINALITY_UNLIMITED.
* - translatable (integer): Whether the field is translatable.
* - locked (integer): Whether or not the field is available for editing. If
* TRUE, users can't change field settings or create new instances of the
* field in the UI. Defaults to FALSE.
* - module (string, read-only): The name of the module that implements the
* field type.
* - active (integer, read-only): TRUE if the module that implements the field
* type is currently enabled, FALSE otherwise.
* - deleted (integer, read-only): TRUE if this field has been deleted, FALSE
* otherwise. Deleted fields are ignored by the Field Attach API. This
* property exists because fields can be marked for deletion but only
* actually destroyed by a separate garbage-collection process.
* - columns (array, read-only): An array of the Field API columns used to
* store each value of this field. The column list may depend on field
* settings; it is not constant per field type. Field API column
* specifications are exactly like Schema API column specifications but,
* depending on the field storage module in use, the name of the column may
* not represent an actual column in an SQL database.
* - indexes (array): An array of indexes on data columns, using the same
* definition format as Schema API index specifications. Only columns that
* appear in the 'columns' setting are allowed. Note that field types can
* specify default indexes, which can be modified or added to when
* creating a field.
* - foreign keys: (optional) An associative array of relations, using the same
* structure as the 'foreign keys' definition of hook_schema(). Note,
* however, that the field data is not necessarily stored in SQL. Also, the
* possible usage is limited, as you cannot specify another field as
* related, only existing SQL tables, such as filter formats.
* - settings (array): A sub-array of key/value pairs of field-type-specific
* settings. Each field type module defines and documents its own field
* settings.
* - storage (array): A sub-array of key/value pairs identifying the storage
* backend to use for the for the field:
* - type (string): The storage backend used by the field. Storage backends
* are defined by modules that implement hook_field_storage_info().
* - module (string, read-only): The name of the module that implements the
* storage backend.
* - active (integer, read-only): TRUE if the module that implements the
* storage backend is currently enabled, FALSE otherwise.
* - settings (array): A sub-array of key/value pairs of settings. Each
* storage backend defines and documents its own settings.
*
* Field instance definitions are represented as an array of key/value pairs.
*
* array $instance:
* - id (integer, read-only): The primary identifier of this field instance.
* It is assigned automatically by field_create_instance().
* - field_id (integer, read-only): The foreign key of the field attached to
* the bundle by this instance. It is populated automatically by
* field_create_instance().
* - field_name (string): The name of the field attached to the bundle by this
* instance.
* - entity_type (string): The name of the entity type the instance is attached
* to.
* - bundle (string): The name of the bundle that the field is attached to.
* - label (string): A human-readable label for the field when used with this
* bundle. For example, the label will be the title of Form API elements
* for this instance.
* - description (string): A human-readable description for the field when
* used with this bundle. For example, the description will be the help
* text of Form API elements for this instance.
* - required (integer): TRUE if a value for this field is required when used
* with this bundle, FALSE otherwise. Currently, required-ness is only
* enforced during Form API operations, not by field_attach_load(),
* field_attach_insert(), or field_attach_update().
* - default_value_function (string): The name of the function, if any, that
* will provide a default value.
* - default_value (array): If default_value_function is not set, then fixed
* values can be provided.
* - deleted (integer, read-only): TRUE if this instance has been deleted,
* FALSE otherwise. Deleted instances are ignored by the Field Attach API.
* This property exists because instances can be marked for deletion but
* only actually destroyed by a separate garbage-collection process.
* - settings (array): A sub-array of key/value pairs of field-type-specific
* instance settings. Each field type module defines and documents its own
* instance settings.
* - widget (array): A sub-array of key/value pairs identifying the Form API
* input widget for the field when used by this bundle:
* - type (string): The type of the widget, such as text_textfield. Widget
* types are defined by modules that implement hook_field_widget_info().
* - settings (array): A sub-array of key/value pairs of
* widget-type-specific settings. Each field widget type module defines
* and documents its own widget settings.
* - weight (float): The weight of the widget relative to the other elements
* in entity edit forms.
* - module (string, read-only): The name of the module that implements the
* widget type.
* - display (array): A sub-array of key/value pairs identifying the way field
* values should be displayed in each of the entity type's view modes, plus
* the 'default' mode. For each view mode, Field UI lets site administrators
* define whether they want to use a dedicated set of display options or the
* 'default' options to reduce the number of displays to maintain as they
* add new fields. For nodes, on a fresh install, only the 'teaser' view
* mode is configured to use custom display options, all other view modes
* defined use the 'default' options by default. When programmatically
* adding field instances on nodes, it is therefore recommended to at least
* specify display options for 'default' and 'teaser':
* - default (array): A sub-array of key/value pairs describing the display
* options to be used when the field is being displayed in view modes
* that are not configured to use dedicated display options:
* - label (string): Position of the label. 'inline', 'above' and
* 'hidden' are the values recognized by the default 'field' theme
* implementation.
* - type (string): The type of the display formatter, or 'hidden' for
* no display.
* - settings (array): A sub-array of key/value pairs of display
* options specific to the formatter.
* - weight (float): The weight of the field relative to the other entity
* components displayed in this view mode.
* - module (string, read-only): The name of the module which implements
* the display formatter.
* - some_mode: A sub-array of key/value pairs describing the display
* options to be used when the field is being displayed in the 'some_mode'
* view mode. Those options will only be actually applied at run time if
* the view mode is not configured to use default settings for this bundle:
* - ...
* - other_mode:
* - ...
*
* The (default) render arrays produced for field instances are documented at
* field_attach_view().
*
* Bundles are represented by two strings, an entity type and a bundle name.
*
* - @link field_types Field Types API @endlink. Defines field types,
* widget types, and display formatters. Field modules use this API
* to provide field types like Text and Node Reference along with the
* associated form elements and display formatters.
*
* - @link field_crud Field CRUD API @endlink. Create, updates, and
* deletes fields, bundles (a.k.a. "content types"), and instances.
* Modules use this API, often in hook_install(), to create
* custom data structures.
*
* - @link field_attach Field Attach API @endlink. Connects entity
* types to the Field API. Field Attach API functions load, store,
* generate Form API structures, display, and perform a variety of
* other functions for field data connected to individual entities.
* Fieldable entity types like node and user use this API to make
* themselves fieldable.
*
* - @link field_info Field Info API @endlink. Exposes information
* about all fields, instances, widgets, and related information
* defined by or with the Field API.
*
* - @link field_storage Field Storage API @endlink. Provides a
* pluggable back-end storage system for actual field data. The
* default implementation, field_sql_storage.module, stores field data
* in the local SQL database.
*
* - @link field_purge Field API bulk data deletion @endlink. Cleans
* up after bulk deletion operations such as field_delete_field()
* and field_delete_instance().
*
* - @link field_language Field language API @endlink. Provides native
* multilingual support for the Field API.
*/
/**
* Value for field API indicating a field accepts an unlimited number of values.
*/
define('FIELD_CARDINALITY_UNLIMITED', -1);
/**
* Value for field API indicating a widget doesn't accept default values.
*
* @see hook_field_widget_info()
*/
define('FIELD_BEHAVIOR_NONE', 0x0001);
/**
* Value for field API concerning widget default and multiple value settings.
*
* @see hook_field_widget_info()
*
* When used in a widget default context, indicates the widget accepts default
* values. When used in a multiple value context for a widget that allows the
* input of one single field value, indicates that the widget will be repeated
* for each value input.
*/
define('FIELD_BEHAVIOR_DEFAULT', 0x0002);
/**
* Value for field API indicating a widget can receive several field values.
*
* @see hook_field_widget_info()
*/
define('FIELD_BEHAVIOR_CUSTOM', 0x0004);
/**
* Age argument for loading the most recent version of an entity's
* field data with field_attach_load().
*/
define('FIELD_LOAD_CURRENT', 'FIELD_LOAD_CURRENT');
/**
* Age argument for loading the version of an entity's field data
* specified in the entity with field_attach_load().
*/
define('FIELD_LOAD_REVISION', 'FIELD_LOAD_REVISION');
/**
* Exception class thrown by hook_field_update_forbid().
*/
class FieldUpdateForbiddenException extends FieldException {}
/**
* Implements hook_help().
*/
function field_help($path, $arg) {
switch ($path) {
case 'admin/help#field':
$output = '';
$output .= '' . t('About') . ' ';
$output .= '' . t('The Field module allows custom data fields to be defined for entity types (entities include content items, comments, user accounts, and taxonomy terms). The Field module takes care of storing, loading, editing, and rendering field data. Most users will not interact with the Field module directly, but will instead use the Field UI module user interface. Module developers can use the Field API to make new entity types "fieldable" and thus allow fields to be attached to them. For more information, see the online handbook entry for Field module .', array('@field-ui-help' => url('admin/help/field_ui'), '@field' => 'http://drupal.org/documentation/modules/field')) . '
';
$output .= '' . t('Uses') . ' ';
$output .= '';
$output .= '' . t('Enabling field types') . ' ';
$output .= '' . t('The Field module provides the infrastructure for fields and field attachment; the field types and input widgets themselves are provided by additional modules. Some of the modules are required; the optional modules can be enabled from the Modules administration page . Drupal core includes the following field type modules: Number (required), Text (required), List (required), Taxonomy (optional), Image (optional), and File (optional); the required Options module provides input widgets for other field modules. Additional fields and widgets may be provided by contributed modules, which you can find in the contributed module section of Drupal.org . Currently enabled field and input widget modules:', array('@modules' => url('admin/modules'), '@contrib' => 'http://drupal.org/project/modules', '@options' => url('admin/help/options')));
// Make a list of all widget and field modules currently enabled, in
// order by displayed module name (module names are not translated).
$items = array();
$info = system_get_info('module');
$modules = array_merge(module_implements('field_info'), module_implements('field_widget_info'));
$modules = array_unique($modules);
sort($modules);
foreach ($modules as $module) {
$display = $info[$module]['name'];
if (module_hook($module, 'help')) {
$items['items'][] = l($display, 'admin/help/' . $module);
}
else {
$items['items'][] = $display;
}
}
$output .= theme('item_list', $items) . ' ';
$output .= '' . t('Managing field data storage') . ' ';
$output .= '' . t('Developers of field modules can either use the default Field SQL storage module to store data for their fields, or a contributed or custom module developed using the field storage API .', array('@storage-api' => 'http://api.drupal.org/api/group/field_storage/7', '@sql-store' => url('admin/help/field_sql_storage'))) . ' ';
$output .= ' ';
return $output;
}
}
/**
* Implements hook_permission().
*/
function field_permission() {
return array(
'administer fields' => array(
'title' => t('Administer fields'),
'description' => t('Additional permissions are required based on what the fields are attached to (for example, administer content types to manage fields attached to content).', array(
'@url' => '#module-node',
)),
'restrict access' => TRUE,
),
);
}
/**
* Implements hook_theme().
*/
function field_theme() {
return array(
'field' => array(
'render element' => 'element',
),
'field_multiple_value_form' => array(
'render element' => 'element',
),
);
}
/**
* Implements hook_cron().
*/
function field_cron() {
// Refresh the 'active' status of fields.
field_sync_field_status();
// Do a pass of purging on deleted Field API data, if any exists.
$limit = variable_get('field_purge_batch_size', 10);
field_purge_batch($limit);
}
/**
* Implements hook_system_info_alter().
*
* Goes through a list of all modules that provide a field type, and makes them
* required if there are any active fields of that type.
*/
function field_system_info_alter(&$info, $file, $type) {
if ($type == 'module' && module_hook($file->name, 'field_info')) {
$fields = field_read_fields(array('module' => $file->name), array('include_deleted' => TRUE));
if ($fields) {
$info['required'] = TRUE;
// Provide an explanation message (only mention pending deletions if there
// remains no actual, non-deleted fields)
$non_deleted = FALSE;
foreach ($fields as $field) {
if (empty($field['deleted'])) {
$non_deleted = TRUE;
break;
}
}
if ($non_deleted) {
if (module_exists('field_ui')) {
$explanation = t('Field type(s) in use - see Field list ', array('@fields-page' => url('admin/reports/fields')));
}
else {
$explanation = t('Fields type(s) in use');
}
}
else {
$explanation = t('Fields pending deletion');
}
$info['explanation'] = $explanation;
}
}
}
/**
* Implements hook_flush_caches().
*/
function field_flush_caches() {
// Refresh the 'active' status of fields.
field_sync_field_status();
// Request a flush of our cache table.
return array('cache_field');
}
/**
* Implements hook_modules_enabled().
*/
function field_modules_enabled($modules) {
// Refresh the 'active' status of fields.
field_sync_field_status();
}
/**
* Implements hook_modules_disabled().
*/
function field_modules_disabled($modules) {
// Refresh the 'active' status of fields.
field_sync_field_status();
}
/**
* Refreshes the 'active' and 'storage_active' columns for fields.
*/
function field_sync_field_status() {
// Refresh the 'active' and 'storage_active' columns according to the current
// set of enabled modules.
$modules = module_list();
foreach ($modules as $module_name) {
field_associate_fields($module_name);
}
db_update('field_config')
->fields(array('active' => 0))
->condition('module', $modules, 'NOT IN')
->execute();
db_update('field_config')
->fields(array('storage_active' => 0))
->condition('storage_module', $modules, 'NOT IN')
->execute();
}
/**
* Allows a module to update the database for fields and columns it controls.
*
* @param $module
* The name of the module to update on.
*/
function field_associate_fields($module) {
// Associate field types.
$field_types = (array) module_invoke($module, 'field_info');
if ($field_types) {
db_update('field_config')
->fields(array('module' => $module, 'active' => 1))
->condition('type', array_keys($field_types))
->execute();
}
// Associate storage backends.
$storage_types = (array) module_invoke($module, 'field_storage_info');
if ($storage_types) {
db_update('field_config')
->fields(array('storage_module' => $module, 'storage_active' => 1))
->condition('storage_type', array_keys($storage_types))
->execute();
}
}
/**
* Helper function to get the default value for a field on an entity.
*
* @param $entity_type
* The type of $entity; e.g., 'node' or 'user'.
* @param $entity
* The entity for the operation.
* @param $field
* The field structure.
* @param $instance
* The instance structure.
* @param $langcode
* The field language to fill-in with the default value.
*/
function field_get_default_value($entity_type, $entity, $field, $instance, $langcode = NULL) {
$items = array();
if (!empty($instance['default_value_function'])) {
$function = $instance['default_value_function'];
if (function_exists($function)) {
$items = $function($entity_type, $entity, $field, $instance, $langcode);
}
}
elseif (!empty($instance['default_value'])) {
$items = $instance['default_value'];
}
return $items;
}
/**
* Helper function to filter out empty field values.
*
* @param $field
* The field definition.
* @param $items
* The field values to filter.
*
* @return
* The array of items without empty field values. The function also renumbers
* the array keys to ensure sequential deltas.
*/
function _field_filter_items($field, $items) {
$function = $field['module'] . '_field_is_empty';
foreach ((array) $items as $delta => $item) {
// Explicitly break if the function is undefined.
if ($function($item, $field)) {
unset($items[$delta]);
}
}
return array_values($items);
}
/**
* Helper function to sort items in a field according to
* user drag-n-drop reordering.
*/
function _field_sort_items($field, $items) {
if (($field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED) && isset($items[0]['_weight'])) {
usort($items, '_field_sort_items_helper');
foreach ($items as $delta => $item) {
if (is_array($items[$delta])) {
unset($items[$delta]['_weight']);
}
}
}
return $items;
}
/**
* Sort function for items order.
* (copied form element_sort(), which acts on #weight keys)
*/
function _field_sort_items_helper($a, $b) {
$a_weight = (is_array($a) ? $a['_weight'] : 0);
$b_weight = (is_array($b) ? $b['_weight'] : 0);
return $a_weight - $b_weight;
}
/**
* Same as above, using ['_weight']['#value']
*/
function _field_sort_items_value_helper($a, $b) {
$a_weight = (is_array($a) && isset($a['_weight']['#value']) ? $a['_weight']['#value'] : 0);
$b_weight = (is_array($b) && isset($b['_weight']['#value']) ? $b['_weight']['#value'] : 0);
return $a_weight - $b_weight;
}
/**
* Gets or sets administratively defined bundle settings.
*
* @param string $entity_type
* The type of $entity; e.g., 'node' or 'user'.
* @param string $bundle
* The bundle name.
* @param array|null $settings
* (optional) The settings to store, an associative array with the following
* elements:
* - view_modes: An associative array keyed by view mode, with the following
* key/value pairs:
* - custom_settings: Boolean specifying whether the view mode uses a
* dedicated set of display options (TRUE), or the 'default' options
* (FALSE). Defaults to FALSE.
* - extra_fields: An associative array containing the form and display
* settings for extra fields (also known as pseudo-fields):
* - form: An associative array whose keys are the names of extra fields,
* and whose values are associative arrays with the following elements:
* - weight: The weight of the extra field, determining its position on an
* entity form.
* - display: An associative array whose keys are the names of extra fields,
* and whose values are associative arrays keyed by the name of view
* modes. This array must include an item for the 'default' view mode.
* Each view mode sub-array contains the following elements:
* - weight: The weight of the extra field, determining its position when
* an entity is viewed.
* - visible: TRUE if the extra field is visible, FALSE otherwise.
*
* @return array|null
* If no $settings are passed, the current settings are returned.
*/
function field_bundle_settings($entity_type, $bundle, $settings = NULL) {
if (isset($settings)) {
variable_set('field_bundle_settings_' . $entity_type . '__' . $bundle, $settings);
field_info_cache_clear();
}
else {
$settings = variable_get('field_bundle_settings_' . $entity_type . '__' . $bundle, array());
$settings += array(
'view_modes' => array(),
'extra_fields' => array(),
);
$settings['extra_fields'] += array(
'form' => array(),
'display' => array(),
);
return $settings;
}
}
/**
* Returns view mode settings in a given bundle.
*
* @param $entity_type
* The type of entity; e.g. 'node' or 'user'.
* @param $bundle
* The bundle name to return view mode settings for.
*
* @return
* An array keyed by view mode, with the following key/value pairs:
* - custom_settings: Boolean specifying whether the view mode uses a
* dedicated set of display options (TRUE), or the 'default' options
* (FALSE). Defaults to FALSE.
*/
function field_view_mode_settings($entity_type, $bundle) {
$cache = &drupal_static(__FUNCTION__, array());
if (!isset($cache[$entity_type][$bundle])) {
$bundle_settings = field_bundle_settings($entity_type, $bundle);
$settings = $bundle_settings['view_modes'];
// Include view modes for which nothing has been stored yet, but whose
// definition in hook_entity_info() specify they should use custom settings
// by default.
$entity_info = entity_get_info($entity_type);
foreach ($entity_info['view modes'] as $view_mode => $view_mode_info) {
if (!isset($settings[$view_mode]['custom_settings']) && $view_mode_info['custom settings']) {
$settings[$view_mode]['custom_settings'] = TRUE;
}
}
$cache[$entity_type][$bundle] = $settings;
}
return $cache[$entity_type][$bundle];
}
/**
* Returns the display settings to use for an instance in a given view mode.
*
* @param $instance
* The field instance being displayed.
* @param $view_mode
* The view mode.
* @param $entity
* The entity being displayed.
*
* @return
* The display settings to be used when displaying the field values.
*/
function field_get_display($instance, $view_mode, $entity) {
// Check whether the view mode uses custom display settings or the 'default'
// mode.
$view_mode_settings = field_view_mode_settings($instance['entity_type'], $instance['bundle']);
$actual_mode = (!empty($view_mode_settings[$view_mode]['custom_settings']) ? $view_mode : 'default');
$display = $instance['display'][$actual_mode];
// Let modules alter the display settings.
$context = array(
'entity_type' => $instance['entity_type'],
'field' => field_info_field($instance['field_name']),
'instance' => $instance,
'entity' => $entity,
'view_mode' => $view_mode,
);
drupal_alter(array('field_display', 'field_display_' . $instance['entity_type']), $display, $context);
return $display;
}
/**
* Returns the display settings to use for pseudo-fields in a given view mode.
*
* @param $entity_type
* The type of $entity; e.g., 'node' or 'user'.
* @param $bundle
* The bundle name.
* @param $view_mode
* The view mode.
*
* @return
* The display settings to be used when viewing the bundle's pseudo-fields.
*/
function field_extra_fields_get_display($entity_type, $bundle, $view_mode) {
// Check whether the view mode uses custom display settings or the 'default'
// mode.
$view_mode_settings = field_view_mode_settings($entity_type, $bundle);
$actual_mode = (!empty($view_mode_settings[$view_mode]['custom_settings'])) ? $view_mode : 'default';
$extra_fields = field_info_extra_fields($entity_type, $bundle, 'display');
$displays = array();
foreach ($extra_fields as $name => $value) {
$displays[$name] = $extra_fields[$name]['display'][$actual_mode];
}
// Let modules alter the display settings.
$context = array(
'entity_type' => $entity_type,
'bundle' => $bundle,
'view_mode' => $view_mode,
);
drupal_alter('field_extra_fields_display', $displays, $context);
return $displays;
}
/**
* Pre-render callback to adjust weights and visibility of non-field elements.
*/
function _field_extra_fields_pre_render($elements) {
$entity_type = $elements['#entity_type'];
$bundle = $elements['#bundle'];
if (isset($elements['#type']) && $elements['#type'] == 'form') {
$extra_fields = field_info_extra_fields($entity_type, $bundle, 'form');
foreach ($extra_fields as $name => $settings) {
if (isset($elements[$name])) {
$elements[$name]['#weight'] = $settings['weight'];
}
}
}
elseif (isset($elements['#view_mode'])) {
$view_mode = $elements['#view_mode'];
$extra_fields = field_extra_fields_get_display($entity_type, $bundle, $view_mode);
foreach ($extra_fields as $name => $settings) {
if (isset($elements[$name])) {
$elements[$name]['#weight'] = $settings['weight'];
// Visibility: make sure we do not accidentally show a hidden element.
$elements[$name]['#access'] = isset($elements[$name]['#access']) ? ($elements[$name]['#access'] && $settings['visible']) : $settings['visible'];
}
}
}
return $elements;
}
/**
* Clear the field info and field data caches.
*/
function field_cache_clear() {
cache_clear_all('*', 'cache_field', TRUE);
field_info_cache_clear();
}
/**
* Like filter_xss_admin(), but with a shorter list of allowed tags.
*
* Used for items entered by administrators, like field descriptions,
* allowed values, where some (mainly inline) mark-up may be desired
* (so check_plain() is not acceptable).
*/
function field_filter_xss($string) {
return filter_xss($string, _field_filter_xss_allowed_tags());
}
/**
* List of tags allowed by field_filter_xss().
*/
function _field_filter_xss_allowed_tags() {
return array('a', 'b', 'big', 'code', 'del', 'em', 'i', 'ins', 'pre', 'q', 'small', 'span', 'strong', 'sub', 'sup', 'tt', 'ol', 'ul', 'li', 'p', 'br', 'img');
}
/**
* Human-readable list of allowed tags, for display in help texts.
*/
function _field_filter_xss_display_allowed_tags() {
return '<' . implode('> <', _field_filter_xss_allowed_tags()) . '>';
}
/**
* Returns a renderable array for a single field value.
*
* @param $entity_type
* The type of $entity; e.g., 'node' or 'user'.
* @param $entity
* The entity containing the field to display. Must at least contain the id
* key and the field data to display.
* @param $field_name
* The name of the field to display.
* @param $item
* The field value to display, as found in
* $entity->field_name[$langcode][$delta].
* @param $display
* Can be either the name of a view mode, or an array of display settings.
* See field_view_field() for more information.
* @param $langcode
* (Optional) The language of the value in $item. If not provided, the
* current language will be assumed.
* @return
* A renderable array for the field value.
*/
function field_view_value($entity_type, $entity, $field_name, $item, $display = array(), $langcode = NULL) {
$output = array();
if ($field = field_info_field($field_name)) {
// Determine the langcode that will be used by language fallback.
$langcode = field_language($entity_type, $entity, $field_name, $langcode);
// Push the item as the single value for the field, and defer to
// field_view_field() to build the render array for the whole field.
$clone = clone $entity;
$clone->{$field_name}[$langcode] = array($item);
$elements = field_view_field($entity_type, $clone, $field_name, $display, $langcode);
// Extract the part of the render array we need.
$output = isset($elements[0]) ? $elements[0] : array();
if (isset($elements['#access'])) {
$output['#access'] = $elements['#access'];
}
}
return $output;
}
/**
* Returns a renderable array for the value of a single field in an entity.
*
* The resulting output is a fully themed field with label and multiple values.
*
* This function can be used by third-party modules that need to output an
* isolated field.
* - Do not use inside node (or any other entity) templates; use
* render($content[FIELD_NAME]) instead.
* - Do not use to display all fields in an entity; use
* field_attach_prepare_view() and field_attach_view() instead.
* - The field_view_value() function can be used to output a single formatted
* field value, without label or wrapping field markup.
*
* The function takes care of invoking the prepare_view steps. It also respects
* field access permissions.
*
* @param $entity_type
* The type of $entity; e.g., 'node' or 'user'.
* @param $entity
* The entity containing the field to display. Must at least contain the id
* key and the field data to display.
* @param $field_name
* The name of the field to display.
* @param $display
* Can be either:
* - The name of a view mode. The field will be displayed according to the
* display settings specified for this view mode in the $instance
* definition for the field in the entity's bundle.
* If no display settings are found for the view mode, the settings for
* the 'default' view mode will be used.
* - An array of display settings, as found in the 'display' entry of
* $instance definitions. The following key/value pairs are allowed:
* - label: (string) Position of the label. The default 'field' theme
* implementation supports the values 'inline', 'above' and 'hidden'.
* Defaults to 'above'.
* - type: (string) The formatter to use. Defaults to the
* 'default_formatter' for the field type, specified in
* hook_field_info(). The default formatter will also be used if the
* requested formatter is not available.
* - settings: (array) Settings specific to the formatter. Defaults to the
* formatter's default settings, specified in
* hook_field_formatter_info().
* - weight: (float) The weight to assign to the renderable element.
* Defaults to 0.
* @param $langcode
* (Optional) The language the field values are to be shown in. The site's
* current language fallback logic will be applied no values are available
* for the language. If no language is provided the current language will be
* used.
* @return
* A renderable array for the field value.
*
* @see field_view_value()
*/
function field_view_field($entity_type, $entity, $field_name, $display = array(), $langcode = NULL) {
$output = array();
if ($field = field_info_field($field_name)) {
if (is_array($display)) {
// When using custom display settings, fill in default values.
$cache = _field_info_field_cache();
$display = $cache->prepareInstanceDisplay($display, $field["type"]);
}
// Hook invocations are done through the _field_invoke() functions in
// 'single field' mode, to reuse the language fallback logic.
// Determine the actual language to display for the field, given the
// languages available in the field data.
$display_language = field_language($entity_type, $entity, $field_name, $langcode);
$options = array('field_name' => $field_name, 'language' => $display_language);
$null = NULL;
// Invoke prepare_view steps if needed.
if (empty($entity->_field_view_prepared)) {
list($id) = entity_extract_ids($entity_type, $entity);
// First let the field types do their preparation.
_field_invoke_multiple('prepare_view', $entity_type, array($id => $entity), $display, $null, $options);
// Then let the formatters do their own specific massaging.
_field_invoke_multiple_default('prepare_view', $entity_type, array($id => $entity), $display, $null, $options);
}
// Build the renderable array.
$result = _field_invoke_default('view', $entity_type, $entity, $display, $null, $options);
// Invoke hook_field_attach_view_alter() to let other modules alter the
// renderable array, as in a full field_attach_view() execution.
$context = array(
'entity_type' => $entity_type,
'entity' => $entity,
'view_mode' => '_custom',
'display' => $display,
'language' => $langcode,
);
drupal_alter('field_attach_view', $result, $context);
if (isset($result[$field_name])) {
$output = $result[$field_name];
}
}
return $output;
}
/**
* Returns the field items in the language they currently would be displayed.
*
* @param $entity_type
* The type of $entity; e.g., 'node' or 'user'.
* @param $entity
* The entity containing the data to be displayed.
* @param $field_name
* The field to be displayed.
* @param $langcode
* (optional) The language code $entity->{$field_name} has to be displayed in.
* Defaults to the current language.
*
* @return
* An array of field items keyed by delta if available, FALSE otherwise.
*/
function field_get_items($entity_type, $entity, $field_name, $langcode = NULL) {
$langcode = field_language($entity_type, $entity, $field_name, $langcode);
return isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : FALSE;
}
/**
* Determine whether a field has any data.
*
* @param $field
* A field structure.
* @return
* TRUE if the field has data for any entity; FALSE otherwise.
*/
function field_has_data($field) {
$query = new EntityFieldQuery();
$query = $query->fieldCondition($field)
->range(0, 1)
->count()
// Neutralize the 'entity_field_access' query tag added by
// field_sql_storage_field_storage_query(). The result cannot depend on the
// access grants of the current user.
->addTag('DANGEROUS_ACCESS_CHECK_OPT_OUT');
return (bool) $query
->execute() || (bool) $query
->age(FIELD_LOAD_REVISION)
->execute();
}
/**
* Determine whether the user has access to a given field.
*
* This function does not determine whether access is granted to the entity
* itself, only the specific field. Callers are responsible for ensuring that
* entity access is also respected. For example, when checking field access for
* nodes, check node_access() before checking field_access(), and when checking
* field access for entities using the Entity API contributed module,
* check entity_access() before checking field_access().
*
* @param $op
* The operation to be performed. Possible values:
* - 'edit'
* - 'view'
* @param $field
* The full field structure array for the field on which the operation is to
* be performed. See field_info_field().
* @param $entity_type
* The type of $entity; e.g., 'node' or 'user'.
* @param $entity
* (optional) The entity for the operation.
* @param $account
* (optional) The account to check, if not given use currently logged in user.
*
* @return
* TRUE if the operation is allowed;
* FALSE if the operation is denied.
*/
function field_access($op, $field, $entity_type, $entity = NULL, $account = NULL) {
global $user;
if (!isset($account)) {
$account = $user;
}
foreach (module_implements('field_access') as $module) {
$function = $module . '_field_access';
$access = $function($op, $field, $entity_type, $entity, $account);
if ($access === FALSE) {
return FALSE;
}
}
return TRUE;
}
/**
* Helper function to extract the bundle name of from a bundle object.
*
* @param $entity_type
* The type of $entity; e.g., 'node' or 'user'.
* @param $bundle
* The bundle object (or string if bundles for this entity type do not exist
* as standalone objects).
* @return
* The bundle name.
*/
function field_extract_bundle($entity_type, $bundle) {
if (is_string($bundle)) {
return $bundle;
}
$info = entity_get_info($entity_type);
if (is_object($bundle) && isset($info['bundle keys']['bundle']) && isset($bundle->{$info['bundle keys']['bundle']})) {
return $bundle->{$info['bundle keys']['bundle']};
}
}
/**
* Theme preprocess function for theme_field() and field.tpl.php.
*
* @see theme_field()
* @see field.tpl.php
*/
function template_preprocess_field(&$variables, $hook) {
$element = $variables['element'];
// There's some overhead in calling check_plain() so only call it if the label
// variable is being displayed. Otherwise, set it to NULL to avoid PHP
// warnings if a theme implementation accesses the variable even when it's
// supposed to be hidden. If a theme implementation needs to print a hidden
// label, it needs to supply a preprocess function that sets it to the
// sanitized element title or whatever else is wanted in its place.
$variables['label_hidden'] = ($element['#label_display'] == 'hidden');
$variables['label'] = $variables['label_hidden'] ? NULL : check_plain($element['#title']);
// We want other preprocess functions and the theme implementation to have
// fast access to the field item render arrays. The item render array keys
// (deltas) should always be a subset of the keys in #items, and looping on
// those keys is faster than calling element_children() or looping on all keys
// within $element, since that requires traversal of all element properties.
$variables['items'] = array();
foreach ($element['#items'] as $delta => $item) {
if (!empty($element[$delta])) {
$variables['items'][$delta] = $element[$delta];
}
}
// Add default CSS classes. Since there can be many fields rendered on a page,
// save some overhead by calling strtr() directly instead of
// drupal_html_class().
$variables['field_name_css'] = strtr($element['#field_name'], '_', '-');
$variables['field_type_css'] = strtr($element['#field_type'], '_', '-');
$variables['classes_array'] = array(
'field',
'field-name-' . $variables['field_name_css'],
'field-type-' . $variables['field_type_css'],
'field-label-' . $element['#label_display'],
);
// Add a "clearfix" class to the wrapper since we float the label and the
// field items in field.css if the label is inline.
if ($element['#label_display'] == 'inline') {
$variables['classes_array'][] = 'clearfix';
}
// Add specific suggestions that can override the default implementation.
$variables['theme_hook_suggestions'] = array(
'field__' . $element['#field_type'],
'field__' . $element['#field_name'],
'field__' . $element['#bundle'],
'field__' . $element['#field_name'] . '__' . $element['#bundle'],
);
}
/**
* Theme process function for theme_field() and field.tpl.php.
*
* @see theme_field()
* @see field.tpl.php
*/
function template_process_field(&$variables, $hook) {
// The default theme implementation is a function, so template_process() does
// not automatically run, so we need to flatten the classes and attributes
// here. For best performance, only call drupal_attributes() when needed, and
// note that template_preprocess_field() does not initialize the
// *_attributes_array variables.
$variables['classes'] = implode(' ', $variables['classes_array']);
$variables['attributes'] = empty($variables['attributes_array']) ? '' : drupal_attributes($variables['attributes_array']);
$variables['title_attributes'] = empty($variables['title_attributes_array']) ? '' : drupal_attributes($variables['title_attributes_array']);
$variables['content_attributes'] = empty($variables['content_attributes_array']) ? '' : drupal_attributes($variables['content_attributes_array']);
foreach ($variables['items'] as $delta => $item) {
$variables['item_attributes'][$delta] = empty($variables['item_attributes_array'][$delta]) ? '' : drupal_attributes($variables['item_attributes_array'][$delta]);
}
}
/**
* @} End of "defgroup field".
*/
/**
* Returns HTML for a field.
*
* This is the default theme implementation to display the value of a field.
* Theme developers who are comfortable with overriding theme functions may do
* so in order to customize this markup. This function can be overridden with
* varying levels of specificity. For example, for a field named 'body'
* displayed on the 'article' content type, any of the following functions will
* override this default implementation. The first of these functions that
* exists is used:
* - THEMENAME_field__body__article()
* - THEMENAME_field__article()
* - THEMENAME_field__body()
* - THEMENAME_field()
*
* Theme developers who prefer to customize templates instead of overriding
* functions may copy the "field.tpl.php" from the "modules/field/theme" folder
* of the Drupal installation to somewhere within the theme's folder and
* customize it, just like customizing other Drupal templates such as
* page.tpl.php or node.tpl.php. However, it takes longer for the server to
* process templates than to call a function, so for websites with many fields
* displayed on a page, this can result in a noticeable slowdown of the website.
* For these websites, developers are discouraged from placing a field.tpl.php
* file into the theme's folder, but may customize templates for specific
* fields. For example, for a field named 'body' displayed on the 'article'
* content type, any of the following templates will override this default
* implementation. The first of these templates that exists is used:
* - field--body--article.tpl.php
* - field--article.tpl.php
* - field--body.tpl.php
* - field.tpl.php
* So, if the body field on the article content type needs customization, a
* field--body--article.tpl.php file can be added within the theme's folder.
* Because it's a template, it will result in slightly more time needed to
* display that field, but it will not impact other fields, and therefore,
* is unlikely to cause a noticeable change in website performance. A very rough
* guideline is that if a page is being displayed with more than 100 fields and
* they are all themed with a template instead of a function, it can add up to
* 5% to the time it takes to display that page. This is a guideline only and
* the exact performance impact depends on the server configuration and the
* details of the website.
*
* @param $variables
* An associative array containing:
* - label_hidden: A boolean indicating to show or hide the field label.
* - title_attributes: A string containing the attributes for the title.
* - label: The label for the field.
* - content_attributes: A string containing the attributes for the content's
* div.
* - items: An array of field items.
* - item_attributes: An array of attributes for each item.
* - classes: A string containing the classes for the wrapping div.
* - attributes: A string containing the attributes for the wrapping div.
*
* @see template_preprocess_field()
* @see template_process_field()
* @see field.tpl.php
*
* @ingroup themeable
*/
function theme_field($variables) {
$output = '';
// Render the label, if it's not hidden.
if (!$variables['label_hidden']) {
$output .= '' . $variables['label'] . ':
';
}
// Render the items.
$output .= '';
foreach ($variables['items'] as $delta => $item) {
$classes = 'field-item ' . ($delta % 2 ? 'odd' : 'even');
$output .= '
' . drupal_render($item) . '
';
}
$output .= '
';
// Render the top-level DIV.
$output = '' . $output . '
';
return $output;
}
/**
* DEPRECATED: Helper form element validator: integer.
*
* Use element_validate_integer() instead.
*
* @deprecated
* @see element_validate_integer()
*/
function _element_validate_integer($element, &$form_state) {
element_validate_integer($element, $form_state);
}
/**
* DEPRECATED: Helper form element validator: integer > 0.
*
* Use element_validate_integer_positive() instead.
*
* @deprecated
* @see element_validate_integer_positive()
*/
function _element_validate_integer_positive($element, &$form_state) {
element_validate_integer_positive($element, $form_state);
}
/**
* DEPRECATED: Helper form element validator: number.
*
* Use element_validate_number() instead.
*
* @deprecated
* @see element_validate_number()
*/
function _element_validate_number($element, &$form_state) {
element_validate_number($element, $form_state);
}
{$field_name}[$langcode][$delta][$column_name]
* @endcode
* Every field can hold a single or multiple value for each language belonging
* to the available languages set:
* - For untranslatable fields this set only contains LANGUAGE_NONE.
* - For translatable fields this set can contain any language code. By default
* it is the list returned by field_content_languages(), which contains all
* installed languages with the addition of LANGUAGE_NONE. This default can be
* altered by modules implementing hook_field_available_languages_alter().
*
* The available languages for a particular field are returned by
* field_available_languages(). Whether a field is translatable is determined by
* calling field_is_translatable(), which checks the $field['translatable']
* property returned by field_info_field(), and whether there is at least one
* translation handler available for the field. A translation handler is a
* module registering itself via hook_entity_info() to handle field
* translations.
*
* By default, _field_invoke() and _field_invoke_multiple() are processing a
* field in all available languages, unless they are given a language
* suggestion. Based on that suggestion, _field_language_suggestion() determines
* the languages to act on.
*
* Most field_attach_*() functions act on all available languages, except for
* the following:
* - field_attach_form() only takes a single language code, specifying which
* language the field values will be submitted in.
* - field_attach_view() requires the language the entity will be displayed in.
* Since it is unknown whether a field translation exists for the requested
* language, the translation handler is responsible for performing one of the
* following actions:
* - Ignore missing translations, i.e. do not show any field values for the
* requested language. For example, see locale_field_language_alter().
* - Provide a value in a different language as fallback. By default, the
* fallback logic is applied separately to each field to ensure that there
* is a value for each field to display.
* The field language fallback logic relies on the global language fallback
* configuration. Therefore, the displayed field values can be in the
* requested language, but may be different if no values for the requested
* language are available. The default language fallback rules inspect all the
* enabled languages ordered by their weight. This behavior can be altered or
* even disabled by modules implementing hook_field_language_alter(), making
* it possible to choose the first approach. The display language for each
* field is returned by field_language().
*
* See @link field Field API @endlink for information about the other parts of
* the Field API.
*/
/**
* Implements hook_multilingual_settings_changed().
*/
function field_multilingual_settings_changed() {
field_info_cache_clear();
}
/**
* Collects the available languages for the given entity type and field.
*
* If the given field has language support enabled, an array of available
* languages will be returned, otherwise only LANGUAGE_NONE will be returned.
* Since the default value for a 'translatable' entity property is FALSE, we
* ensure that only entities that are able to handle translations actually get
* translatable fields.
*
* @param $entity_type
* The type of the entity the field is attached to, e.g. 'node' or 'user'.
* @param $field
* A field structure.
*
* @return
* An array of valid language codes.
*/
function field_available_languages($entity_type, $field) {
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast['field_languages'] = &drupal_static(__FUNCTION__);
}
$field_languages = &$drupal_static_fast['field_languages'];
$field_name = $field['field_name'];
if (!isset($field_languages[$entity_type][$field_name])) {
// If the field has language support enabled we retrieve an (alterable) list
// of enabled languages, otherwise we return just LANGUAGE_NONE.
if (field_is_translatable($entity_type, $field)) {
$languages = field_content_languages();
// Let other modules alter the available languages.
$context = array('entity_type' => $entity_type, 'field' => $field);
drupal_alter('field_available_languages', $languages, $context);
$field_languages[$entity_type][$field_name] = $languages;
}
else {
$field_languages[$entity_type][$field_name] = array(LANGUAGE_NONE);
}
}
return $field_languages[$entity_type][$field_name];
}
/**
* Process the given language suggestion based on the available languages.
*
* If a non-empty language suggestion is provided it must appear among the
* available languages, otherwise it will be ignored.
*
* @param $available_languages
* An array of valid language codes.
* @param $language_suggestion
* A language code or an array of language codes keyed by field name.
* @param $field_name
* The name of the field being processed.
*
* @return
* An array of valid language codes.
*/
function _field_language_suggestion($available_languages, $language_suggestion, $field_name) {
// Handle possible language suggestions.
if (!empty($language_suggestion)) {
// We might have an array of language suggestions keyed by field name.
if (is_array($language_suggestion) && isset($language_suggestion[$field_name])) {
$language_suggestion = $language_suggestion[$field_name];
}
// If we have a language suggestion and the suggested language is available,
// we return only it.
if (in_array($language_suggestion, $available_languages)) {
$available_languages = array($language_suggestion);
}
}
return $available_languages;
}
/**
* Returns available content languages.
*
* The languages that may be associated to fields include LANGUAGE_NONE.
*
* @return
* An array of language codes.
*/
function field_content_languages() {
return array_keys(language_list() + array(LANGUAGE_NONE => NULL));
}
/**
* Checks whether a field has language support.
*
* A field has language support enabled if its 'translatable' property is set to
* TRUE, and its entity type has at least one translation handler registered.
*
* @param $entity_type
* The type of the entity the field is attached to.
* @param $field
* A field data structure.
*
* @return
* TRUE if the field can be translated.
*/
function field_is_translatable($entity_type, $field) {
return $field['translatable'] && field_has_translation_handler($entity_type);
}
/**
* Checks if a module is registered as a translation handler for a given entity.
*
* If no handler is passed, simply check if there is any translation handler
* enabled for the given entity type.
*
* @param $entity_type
* The type of the entity whose fields are to be translated.
* @param $handler
* (optional) The name of the handler to be checked. Defaults to NULL.
*
* @return
* TRUE, if the given handler is allowed to manage field translations. If no
* handler is passed, TRUE means there is at least one registered translation
* handler.
*/
function field_has_translation_handler($entity_type, $handler = NULL) {
$entity_info = entity_get_info($entity_type);
if (isset($handler)) {
return !empty($entity_info['translation'][$handler]);
}
elseif (isset($entity_info['translation'])) {
foreach ($entity_info['translation'] as $handler_info) {
// The translation handler must use a non-empty data structure.
if (!empty($handler_info)) {
return TRUE;
}
}
}
return FALSE;
}
/**
* Ensures that a given language code is valid.
*
* Checks whether the given language is one of the enabled languages. Otherwise,
* it returns the current, global language; or the site's default language, if
* the additional parameter $default is TRUE.
*
* @param $langcode
* The language code to validate.
* @param $default
* Whether to return the default language code or the current language code in
* case $langcode is invalid.
* @return
* A valid language code.
*/
function field_valid_language($langcode, $default = TRUE) {
$enabled_languages = field_content_languages();
if (in_array($langcode, $enabled_languages)) {
return $langcode;
}
global $language_content;
return $default ? language_default('language') : $language_content->language;
}
/**
* Returns the display language for the fields attached to the given entity.
*
* The actual language for each given field is determined based on the requested
* language and the actual data available in the fields themselves.
* If there is no registered translation handler for the given entity type, the
* display language to be used is just LANGUAGE_NONE, as no other language code
* is allowed by field_available_languages().
* If translation handlers are found, we let modules provide alternative display
* languages for fields not having the requested language available.
* Core language fallback rules are provided by locale_field_language_fallback()
* which is called by locale_field_language_alter().
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity to be displayed.
* @param $field_name
* (optional) The name of the field to be displayed. Defaults to NULL. If
* no value is specified, the display languages for every field attached to
* the given entity will be returned.
* @param $langcode
* (optional) The language code $entity has to be displayed in. Defaults to
* NULL. If no value is given the current language will be used.
*
* @return
* A language code if a field name is specified, an array of language codes
* keyed by field name otherwise.
*/
function field_language($entity_type, $entity, $field_name = NULL, $langcode = NULL) {
$display_languages = &drupal_static(__FUNCTION__, array());
list($id, , $bundle) = entity_extract_ids($entity_type, $entity);
$langcode = field_valid_language($langcode, FALSE);
if (!isset($display_languages[$entity_type][$id][$langcode])) {
$display_language = array();
// By default display language is set to LANGUAGE_NONE if the field
// translation is not available. It is up to translation handlers to
// implement language fallback rules.
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$display_language[$instance['field_name']] = isset($entity->{$instance['field_name']}[$langcode]) ? $langcode : LANGUAGE_NONE;
}
if (field_has_translation_handler($entity_type)) {
$context = array(
'entity_type' => $entity_type,
'entity' => $entity,
'language' => $langcode,
);
drupal_alter('field_language', $display_language, $context);
}
$display_languages[$entity_type][$id][$langcode] = $display_language;
}
$display_language = $display_languages[$entity_type][$id][$langcode];
// Single-field mode.
if (isset($field_name)) {
return isset($display_language[$field_name]) ? $display_language[$field_name] : FALSE;
}
return $display_language;
}
name = Field SQL storage
description = Stores field data in an SQL database.
package = Core
version = VERSION
core = 7.x
dependencies[] = field
files[] = field_sql_storage.test
required = TRUE
; Information added by Drupal.org packaging script on 2023-03-15
version = "7.95"
project = "drupal"
datestamp = "1678902530"
TRUE, 'include_inactive' => TRUE));
drupal_load('module', 'field_sql_storage');
foreach ($fields as $field) {
if ($field['storage']['type'] == 'field_sql_storage') {
$schema += _field_sql_storage_schema($field);
}
}
}
return $schema;
}
/**
* Utility function: write field data directly to SQL storage.
*
* This function can be used for databases whose schema is at field module
* version 7000 or higher.
*
* @ingroup update_api
*/
function _update_7000_field_sql_storage_write($entity_type, $bundle, $entity_id, $revision_id, $field_name, $data) {
$table_name = "field_data_{$field_name}";
$revision_name = "field_revision_{$field_name}";
db_delete($table_name)
->condition('entity_type', $entity_type)
->condition('entity_id', $entity_id)
->execute();
db_delete($revision_name)
->condition('entity_type', $entity_type)
->condition('entity_id', $entity_id)
->condition('revision_id', $revision_id)
->execute();
$columns = array();
foreach ($data as $langcode => $items) {
foreach ($items as $delta => $item) {
$record = array(
'entity_type' => $entity_type,
'entity_id' => $entity_id,
'revision_id' => $revision_id,
'bundle' => $bundle,
'delta' => $delta,
'language' => $langcode,
);
foreach ($item as $column => $value) {
$record[_field_sql_storage_columnname($field_name, $column)] = $value;
}
$records[] = $record;
// Record the columns used.
$columns += $record;
}
}
if ($columns) {
$query = db_insert($table_name)->fields(array_keys($columns));
$revision_query = db_insert($revision_name)->fields(array_keys($columns));
foreach ($records as $record) {
$query->values($record);
if ($revision_id) {
$revision_query->values($record);
}
}
$query->execute();
$revision_query->execute();
}
}
/**
* @addtogroup updates-6.x-to-7.x
* @{
*/
/**
* Field SQL storage update version placeholder.
*/
function field_sql_storage_update_7000() {
// Some update helper functions (such as
// _update_7000_field_sql_storage_write()) modify the database directly. They
// can be used safely only if the database schema matches the field module
// schema established for Drupal 7.0 (i.e. version 7000). This function exists
// solely to set the schema version to 7000, so that update functions calling
// those helpers can do so safely by declaring a dependency on
// field_sql_storage_update_7000().
}
/**
* Remove the field_config_entity_type table and store 'entity_type' strings.
*/
function field_sql_storage_update_7001(&$sandbox) {
if (!isset($sandbox['progress'])) {
// Collect current etids.
$sandbox['etids'] = db_query('SELECT etid, type FROM {field_config_entity_type}')->fetchAllKeyed();
// Collect affected tables: field data, field revision data, 'deleted'
// tables.
$sandbox['tables'] = array();
$results = db_select('field_config', 'fc', array('fetch' => PDO::FETCH_ASSOC))
->fields('fc')
->condition('storage_module', 'field_sql_storage')
->execute();
foreach ($results as $field) {
if ($field['deleted']) {
$sandbox['tables']["field_deleted_data_{$field['id']}"] = 'data';
$sandbox['tables']["field_deleted_revision_{$field['id']}"] = 'revision';
}
else {
$sandbox['tables']["field_data_{$field['field_name']}"] = 'data';
$sandbox['tables']["field_revision_{$field['field_name']}"] = 'revision';
}
}
reset($sandbox['tables']);
$sandbox['total'] = count($sandbox['tables']);
$sandbox['progress'] = 0;
}
if ($sandbox['tables']) {
// Grab the next table to process.
$table = key($sandbox['tables']);
$type = array_shift($sandbox['tables']);
if (db_table_exists($table)) {
// Add the 'entity_type' column.
if (!db_field_exists($table, 'entity_type')) {
$column = array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The entity type this data is attached to.',
);
db_add_field($table, 'entity_type', $column);
// Populate the 'entity_type' column based on the 'etid' column.
foreach ($sandbox['etids'] as $etid => $entity_type) {
db_update($table)
->fields(array('entity_type' => $entity_type))
->condition('etid', $etid)
->execute();
}
// Index the new column.
db_add_index($table, 'entity_type', array('entity_type'));
}
// Use the 'entity_type' column in the primary key.
db_drop_primary_key($table);
$primary_keys = array(
'data' => array('entity_type', 'entity_id', 'deleted', 'delta', 'language'),
'revision' => array('entity_type', 'entity_id', 'revision_id', 'deleted', 'delta', 'language'),
);
db_add_primary_key($table, $primary_keys[$type]);
// Drop the 'etid' column.
if (db_field_exists($table, 'etid')) {
db_drop_field($table, 'etid');
}
}
// Report progress.
$sandbox['progress']++;
$sandbox['#finished'] = min(0.99, $sandbox['progress'] / $sandbox['total']);
}
else {
// No more tables left: drop the field_config_entity_type table.
db_drop_table('field_config_entity_type');
// Drop the previous 'field_sql_storage_ENTITYTYPE_etid' system variables.
foreach ($sandbox['etids'] as $etid => $entity_type) {
variable_del('field_sql_storage_' . $entity_type . '_etid');
}
// We're done.
$sandbox['#finished'] = 1;
}
}
/**
* Fix primary keys in field revision data tables.
*/
function field_sql_storage_update_7002() {
$results = db_select('field_config', 'fc', array('fetch' => PDO::FETCH_ASSOC))
->fields('fc')
->condition('storage_module', 'field_sql_storage')
->execute();
foreach ($results as $field) {
// Revision tables of deleted fields do not need to be fixed, since no new
// data is written to them.
if (!$field['deleted']) {
$table = "field_revision_{$field['field_name']}";
db_drop_primary_key($table);
db_add_primary_key($table, array('entity_type', 'entity_id', 'revision_id', 'deleted', 'delta', 'language'));
}
}
}
/**
* @} End of "addtogroup updates-6.x-to-7.x".
*/
' . t('About') . '';
$output .= '' . t('The Field SQL storage module stores field data in the database. It is the default field storage module; other field storage mechanisms may be available as contributed modules. See the Field module help page for more information about fields.', array('@field-help' => url('admin/help/field'))) . '
';
return $output;
}
}
/**
* Implements hook_field_storage_info().
*/
function field_sql_storage_field_storage_info() {
return array(
'field_sql_storage' => array(
'label' => t('Default SQL storage'),
'description' => t('Stores fields in the local SQL database, using per-field tables.'),
),
);
}
/**
* Generate a table name for a field data table.
*
* @param $field
* The field structure.
* @return
* A string containing the generated name for the database table
*/
function _field_sql_storage_tablename($field) {
if ($field['deleted']) {
return "field_deleted_data_{$field['id']}";
}
else {
return "field_data_{$field['field_name']}";
}
}
/**
* Generate a table name for a field revision archive table.
*
* @param $name
* The field structure.
* @return
* A string containing the generated name for the database table
*/
function _field_sql_storage_revision_tablename($field) {
if ($field['deleted']) {
return "field_deleted_revision_{$field['id']}";
}
else {
return "field_revision_{$field['field_name']}";
}
}
/**
* Generates a table alias for a field data table.
*
* The table alias is unique for each unique combination of field name
* (represented by $tablename), delta_group and language_group.
*
* @param $tablename
* The name of the data table for this field.
* @param $field_key
* The numeric key of this field in this query.
* @param $query
* The EntityFieldQuery that is executed.
*
* @return
* A string containing the generated table alias.
*/
function _field_sql_storage_tablealias($tablename, $field_key, EntityFieldQuery $query) {
// No conditions present: use a unique alias.
if (empty($query->fieldConditions[$field_key])) {
return $tablename . $field_key;
}
// Find the delta and language condition values and append them to the alias.
$condition = $query->fieldConditions[$field_key];
$alias = $tablename;
$has_group_conditions = FALSE;
foreach (array('delta', 'language') as $column) {
if (isset($condition[$column . '_group'])) {
$alias .= '_' . $column . '_' . $condition[$column . '_group'];
$has_group_conditions = TRUE;
}
}
// Return the alias when it has delta/language group conditions.
if ($has_group_conditions) {
return $alias;
}
// Return a unique alias in other cases.
return $tablename . $field_key;
}
/**
* Generate a column name for a field data table.
*
* @param $name
* The name of the field
* @param $column
* The name of the column
* @return
* A string containing a generated column name for a field data
* table that is unique among all other fields.
*/
function _field_sql_storage_columnname($name, $column) {
return $name . '_' . $column;
}
/**
* Generate an index name for a field data table.
*
* @param $name
* The name of the field
* @param $column
* The name of the index
* @return
* A string containing a generated index name for a field data
* table that is unique among all other fields.
*/
function _field_sql_storage_indexname($name, $index) {
return $name . '_' . $index;
}
/**
* Return the database schema for a field. This may contain one or
* more tables. Each table will contain the columns relevant for the
* specified field. Leave the $field's 'columns' and 'indexes' keys
* empty to get only the base schema.
*
* @param $field
* The field structure for which to generate a database schema.
* @return
* One or more tables representing the schema for the field.
*/
function _field_sql_storage_schema($field) {
$deleted = $field['deleted'] ? 'deleted ' : '';
$current = array(
'description' => "Data storage for {$deleted}field {$field['id']} ({$field['field_name']})",
'fields' => array(
'entity_type' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The entity type this data is attached to',
),
'bundle' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance',
),
'deleted' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 0,
'description' => 'A boolean indicating whether this data item has been deleted'
),
'entity_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity id this data is attached to',
),
'revision_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => FALSE,
'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned',
),
// @todo Consider storing language as integer.
'language' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The language for this data item.',
),
'delta' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The sequence number for this data item, used for multi-value fields',
),
),
'primary key' => array('entity_type', 'entity_id', 'deleted', 'delta', 'language'),
'indexes' => array(
'entity_type' => array('entity_type'),
'bundle' => array('bundle'),
'deleted' => array('deleted'),
'entity_id' => array('entity_id'),
'revision_id' => array('revision_id'),
'language' => array('language'),
),
);
// If the target entity type uses a string for its entity ID then update
// the fields entity_id and revision_id columns from INT to VARCHAR.
if (!empty($field['entity_id_type']) && $field['entity_id_type'] === 'string') {
$current['fields']['entity_id']['type'] = 'varchar';
$current['fields']['entity_id']['length'] = 128;
unset($current['fields']['entity_id']['unsigned']);
$current['fields']['revision_id']['type'] = 'varchar';
$current['fields']['revision_id']['length'] = 128;
unset($current['fields']['revision_id']['unsigned']);
}
$field += array('columns' => array(), 'indexes' => array(), 'foreign keys' => array());
// Add field columns.
foreach ($field['columns'] as $column_name => $attributes) {
$real_name = _field_sql_storage_columnname($field['field_name'], $column_name);
$current['fields'][$real_name] = $attributes;
}
// Add indexes.
foreach ($field['indexes'] as $index_name => $columns) {
$real_name = _field_sql_storage_indexname($field['field_name'], $index_name);
foreach ($columns as $column_name) {
// Indexes can be specified as either a column name or an array with
// column name and length. Allow for either case.
if (is_array($column_name)) {
$current['indexes'][$real_name][] = array(
_field_sql_storage_columnname($field['field_name'], $column_name[0]),
$column_name[1],
);
}
else {
$current['indexes'][$real_name][] = _field_sql_storage_columnname($field['field_name'], $column_name);
}
}
}
// Add foreign keys.
foreach ($field['foreign keys'] as $specifier => $specification) {
$real_name = _field_sql_storage_indexname($field['field_name'], $specifier);
$current['foreign keys'][$real_name]['table'] = $specification['table'];
foreach ($specification['columns'] as $column_name => $referenced) {
$sql_storage_column = _field_sql_storage_columnname($field['field_name'], $column_name);
$current['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced;
}
}
// Construct the revision table.
$revision = $current;
$revision['description'] = "Revision archive storage for {$deleted}field {$field['id']} ({$field['field_name']})";
$revision['primary key'] = array('entity_type', 'entity_id', 'revision_id', 'deleted', 'delta', 'language');
$revision['fields']['revision_id']['not null'] = TRUE;
$revision['fields']['revision_id']['description'] = 'The entity revision id this data is attached to';
return array(
_field_sql_storage_tablename($field) => $current,
_field_sql_storage_revision_tablename($field) => $revision,
);
}
/**
* Implements hook_field_storage_create_field().
*/
function field_sql_storage_field_storage_create_field($field) {
$schema = _field_sql_storage_schema($field);
foreach ($schema as $name => $table) {
db_create_table($name, $table);
}
drupal_get_schema(NULL, TRUE);
}
/**
* Implements hook_field_update_forbid().
*
* Forbid any field update that changes column definitions if there is
* any data.
*/
function field_sql_storage_field_update_forbid($field, $prior_field, $has_data) {
if ($has_data && $field['columns'] != $prior_field['columns']) {
throw new FieldUpdateForbiddenException("field_sql_storage cannot change the schema for an existing field with data.");
}
}
/**
* Implements hook_field_storage_update_field().
*/
function field_sql_storage_field_storage_update_field($field, $prior_field, $has_data) {
if (! $has_data) {
// There is no data. Re-create the tables completely.
if (Database::getConnection()->supportsTransactionalDDL()) {
// If the database supports transactional DDL, we can go ahead and rely
// on it. If not, we will have to rollback manually if something fails.
$transaction = db_transaction();
}
try {
$prior_schema = _field_sql_storage_schema($prior_field);
foreach ($prior_schema as $name => $table) {
db_drop_table($name, $table);
}
$schema = _field_sql_storage_schema($field);
foreach ($schema as $name => $table) {
db_create_table($name, $table);
}
}
catch (Exception $e) {
if (Database::getConnection()->supportsTransactionalDDL()) {
$transaction->rollback();
}
else {
// Recreate tables.
$prior_schema = _field_sql_storage_schema($prior_field);
foreach ($prior_schema as $name => $table) {
if (!db_table_exists($name)) {
db_create_table($name, $table);
}
}
}
throw $e;
}
}
else {
// There is data, so there are no column changes. Drop all the
// prior indexes and create all the new ones, except for all the
// priors that exist unchanged.
$table = _field_sql_storage_tablename($prior_field);
$revision_table = _field_sql_storage_revision_tablename($prior_field);
foreach ($prior_field['indexes'] as $name => $columns) {
if (!isset($field['indexes'][$name]) || $columns != $field['indexes'][$name]) {
$real_name = _field_sql_storage_indexname($field['field_name'], $name);
db_drop_index($table, $real_name);
db_drop_index($revision_table, $real_name);
}
}
$table = _field_sql_storage_tablename($field);
$revision_table = _field_sql_storage_revision_tablename($field);
foreach ($field['indexes'] as $name => $columns) {
if (!isset($prior_field['indexes'][$name]) || $columns != $prior_field['indexes'][$name]) {
$real_name = _field_sql_storage_indexname($field['field_name'], $name);
$real_columns = array();
foreach ($columns as $column_name) {
// Indexes can be specified as either a column name or an array with
// column name and length. Allow for either case.
if (is_array($column_name)) {
$real_columns[] = array(
_field_sql_storage_columnname($field['field_name'], $column_name[0]),
$column_name[1],
);
}
else {
$real_columns[] = _field_sql_storage_columnname($field['field_name'], $column_name);
}
}
db_add_index($table, $real_name, $real_columns);
db_add_index($revision_table, $real_name, $real_columns);
}
}
}
drupal_get_schema(NULL, TRUE);
}
/**
* Implements hook_field_storage_delete_field().
*/
function field_sql_storage_field_storage_delete_field($field) {
// Mark all data associated with the field for deletion.
$field['deleted'] = 0;
$table = _field_sql_storage_tablename($field);
$revision_table = _field_sql_storage_revision_tablename($field);
db_update($table)
->fields(array('deleted' => 1))
->execute();
// Move the table to a unique name while the table contents are being deleted.
$field['deleted'] = 1;
$new_table = _field_sql_storage_tablename($field);
$revision_new_table = _field_sql_storage_revision_tablename($field);
db_rename_table($table, $new_table);
db_rename_table($revision_table, $revision_new_table);
drupal_get_schema(NULL, TRUE);
}
/**
* Implements hook_field_storage_load().
*/
function field_sql_storage_field_storage_load($entity_type, $entities, $age, $fields, $options) {
$load_current = $age == FIELD_LOAD_CURRENT;
foreach ($fields as $field_id => $ids) {
// By the time this hook runs, the relevant field definitions have been
// populated and cached in FieldInfo, so calling field_info_field_by_id()
// on each field individually is more efficient than loading all fields in
// memory upfront with field_info_field_by_ids().
$field = field_info_field_by_id($field_id);
$field_name = $field['field_name'];
$table = $load_current ? _field_sql_storage_tablename($field) : _field_sql_storage_revision_tablename($field);
$query = db_select($table, 't')
->fields('t')
->condition('entity_type', $entity_type)
->condition($load_current ? 'entity_id' : 'revision_id', $ids, 'IN')
->condition('language', field_available_languages($entity_type, $field), 'IN')
->orderBy('delta');
if (empty($options['deleted'])) {
$query->condition('deleted', 0);
}
$results = $query->execute();
$delta_count = array();
foreach ($results as $row) {
if (!isset($delta_count[$row->entity_id][$row->language])) {
$delta_count[$row->entity_id][$row->language] = 0;
}
if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$row->language] < $field['cardinality']) {
$item = array();
// For each column declared by the field, populate the item
// from the prefixed database column.
foreach ($field['columns'] as $column => $attributes) {
$column_name = _field_sql_storage_columnname($field_name, $column);
$item[$column] = $row->$column_name;
}
// Add the item to the field values for the entity.
$entities[$row->entity_id]->{$field_name}[$row->language][] = $item;
$delta_count[$row->entity_id][$row->language]++;
}
}
}
}
/**
* Callback for array_filter().
*/
function _field_sql_storage_write_compare_filter_callback($value) {
return NULL !== $value && '' !== $value;
}
/**
* Cleanup field values for later values comparison.
*
* @param array $field
* Field info as returned by field_info_field_by_id().
*
* @param array $array
* Field values to cleanup.
*
* @return array
* Filtered values.
*/
function _field_sql_storage_write_compare_filter($field, $array) {
foreach ($array as $language => $items) {
if (empty($items)) {
unset($array[$language]);
}
else {
foreach ($items as $delta => $item) {
// This should not happen but some modules provide invalid data to the
// field API.
if (!is_array($item)) {
continue;
}
// Let's start by pruning empty values and non storable values.
$array[$language][$delta] = array_filter(array_intersect_key($item, $field['columns']), '_field_sql_storage_write_compare_filter_callback');
// Ordering is important because for widget elements and loaded columns
// from database order might differ and give false positives on field
// value change, especially with complex fields such as image fields.
ksort($array[$language][$delta]);
}
}
}
return $array;
}
/**
* Compare a single field value for both entities and tell us if it changed.
*
* @param array $field
* Loaded field structure.
* @param object $entity1
* First entity to compare.
* @param object $entity2
* Second entity to compare.
*
* @return bool
* True if field value changed, false otherwise.
*/
function _field_sql_storage_write_compare($field, $entity1, $entity2) {
$field_name = $field['field_name'];
if (empty($entity1->$field_name) && empty($entity2->$field_name)) {
// Both are empty we can safely assume that it did not change.
return FALSE;
}
if (!isset($entity1->$field_name) || !isset($entity2->$field_name)) {
// One of them is missing but not the other the value changed.
return TRUE;
}
// We need to proceed to deep array comparison, but we cannot do it naively:
// in most cases the field values come from the edit form, and some Form API
// widget values that are not field columns may be present. We need to clean
// up both original and new field values before comparison.
$items1 = _field_sql_storage_write_compare_filter($field, (array) $entity1->$field_name);
$items2 = _field_sql_storage_write_compare_filter($field, (array) $entity2->$field_name);
return $items1 != $items2;
}
/**
* Implements hook_field_storage_write().
*/
function field_sql_storage_field_storage_write($entity_type, $entity, $op, $fields) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
if (!isset($vid)) {
$vid = $id;
}
// Check if the given entity is a new revision or not. In case of a new
// revision creation, we cannot skip any field.
if (!empty($vid) && !empty($entity->original)) {
list(, $original_vid) = entity_extract_ids($entity_type, $entity->original);
if (NULL === $original_vid) {
$original_vid = $id;
}
$is_new_revision = $original_vid != $vid;
}
else {
$is_new_revision = FALSE;
}
// Allow this optimization to be optional.
$skip_unchanged_fields = variable_get('field_sql_storage_skip_writing_unchanged_fields', FALSE);
foreach ($fields as $field_id) {
$field = field_info_field_by_id($field_id);
if ($skip_unchanged_fields && !$is_new_revision && !empty($entity->original) && !_field_sql_storage_write_compare($field, $entity, $entity->original)) {
continue;
}
$field_name = $field['field_name'];
$table_name = _field_sql_storage_tablename($field);
$revision_name = _field_sql_storage_revision_tablename($field);
$all_languages = field_available_languages($entity_type, $field);
$field_languages = array_intersect($all_languages, array_keys((array) $entity->$field_name));
// Delete and insert, rather than update, in case a value was added.
if ($op == FIELD_STORAGE_UPDATE) {
// Delete languages present in the incoming $entity->$field_name.
// Delete all languages if $entity->$field_name is empty.
$languages = !empty($entity->$field_name) ? $field_languages : $all_languages;
if ($languages) {
db_delete($table_name)
->condition('entity_type', $entity_type)
->condition('entity_id', $id)
->condition('language', $languages, 'IN')
->execute();
db_delete($revision_name)
->condition('entity_type', $entity_type)
->condition('entity_id', $id)
->condition('revision_id', $vid)
->condition('language', $languages, 'IN')
->execute();
}
}
// Prepare the multi-insert query.
$do_insert = FALSE;
$columns = array('entity_type', 'entity_id', 'revision_id', 'bundle', 'delta', 'language');
foreach ($field['columns'] as $column => $attributes) {
$columns[] = _field_sql_storage_columnname($field_name, $column);
}
$query = db_insert($table_name)->fields($columns);
$revision_query = db_insert($revision_name)->fields($columns);
foreach ($field_languages as $langcode) {
$items = (array) $entity->{$field_name}[$langcode];
$delta_count = 0;
foreach ($items as $delta => $item) {
// We now know we have something to insert.
$do_insert = TRUE;
$record = array(
'entity_type' => $entity_type,
'entity_id' => $id,
'revision_id' => $vid,
'bundle' => $bundle,
'delta' => $delta,
'language' => $langcode,
);
foreach ($field['columns'] as $column => $attributes) {
$record[_field_sql_storage_columnname($field_name, $column)] = isset($item[$column]) ? $item[$column] : NULL;
}
$query->values($record);
if (isset($vid)) {
$revision_query->values($record);
}
if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) {
break;
}
}
}
// Execute the query if we have values to insert.
if ($do_insert) {
$query->execute();
$revision_query->execute();
}
}
}
/**
* Implements hook_field_storage_delete().
*
* This function deletes data for all fields for an entity from the database.
*/
function field_sql_storage_field_storage_delete($entity_type, $entity, $fields) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
foreach (field_info_instances($entity_type, $bundle) as $instance) {
if (isset($fields[$instance['field_id']])) {
$field = field_info_field_by_id($instance['field_id']);
field_sql_storage_field_storage_purge($entity_type, $entity, $field, $instance);
}
}
}
/**
* Implements hook_field_storage_purge().
*
* This function deletes data from the database for a single field on
* an entity.
*/
function field_sql_storage_field_storage_purge($entity_type, $entity, $field, $instance) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
$table_name = _field_sql_storage_tablename($field);
$revision_name = _field_sql_storage_revision_tablename($field);
db_delete($table_name)
->condition('entity_type', $entity_type)
->condition('entity_id', $id)
->execute();
db_delete($revision_name)
->condition('entity_type', $entity_type)
->condition('entity_id', $id)
->execute();
}
/**
* Implements hook_field_storage_query().
*/
function field_sql_storage_field_storage_query(EntityFieldQuery $query) {
if ($query->age == FIELD_LOAD_CURRENT) {
$tablename_function = '_field_sql_storage_tablename';
$id_key = 'entity_id';
}
else {
$tablename_function = '_field_sql_storage_revision_tablename';
$id_key = 'revision_id';
}
$table_aliases = array();
$query_tables = NULL;
// Add tables for the fields used.
foreach ($query->fields as $key => $field) {
$tablename = $tablename_function($field);
$table_alias = _field_sql_storage_tablealias($tablename, $key, $query);
$table_aliases[$key] = $table_alias;
if ($key) {
if (!isset($query_tables[$table_alias])) {
$select_query->join($tablename, $table_alias, "$table_alias.entity_type = $field_base_table.entity_type AND $table_alias.$id_key = $field_base_table.$id_key");
}
}
else {
$select_query = db_select($tablename, $table_alias);
// Store a reference to the list of joined tables.
$query_tables =& $select_query->getTables();
// Allow queries internal to the Field API to opt out of the access
// check, for situations where the query's results should not depend on
// the access grants for the current user.
if (!isset($query->tags['DANGEROUS_ACCESS_CHECK_OPT_OUT'])) {
$select_query->addTag('entity_field_access');
}
$select_query->addMetaData('base_table', $tablename);
$select_query->fields($table_alias, array('entity_type', 'entity_id', 'revision_id', 'bundle'));
$field_base_table = $table_alias;
}
if ($field['cardinality'] != 1 || $field['translatable']) {
$select_query->distinct();
}
}
// Add field conditions. We need a fresh grouping cache.
drupal_static_reset('_field_sql_storage_query_field_conditions');
_field_sql_storage_query_field_conditions($query, $select_query, $query->fieldConditions, $table_aliases, '_field_sql_storage_columnname');
// Add field meta conditions.
_field_sql_storage_query_field_conditions($query, $select_query, $query->fieldMetaConditions, $table_aliases, '_field_sql_storage_query_columnname');
if (isset($query->deleted)) {
$select_query->condition("$field_base_table.deleted", (int) $query->deleted);
}
// Is there a need to sort the query by property?
$has_property_order = FALSE;
foreach ($query->order as $order) {
if ($order['type'] == 'property') {
$has_property_order = TRUE;
}
}
if ($query->propertyConditions || $has_property_order) {
if (empty($query->entityConditions['entity_type']['value'])) {
throw new EntityFieldQueryException('Property conditions and orders must have an entity type defined.');
}
$entity_type = $query->entityConditions['entity_type']['value'];
$entity_base_table = _field_sql_storage_query_join_entity($select_query, $entity_type, $field_base_table);
$query->entityConditions['entity_type']['operator'] = '=';
foreach ($query->propertyConditions as $property_condition) {
$query->addCondition($select_query, "$entity_base_table." . $property_condition['column'], $property_condition);
}
}
foreach ($query->entityConditions as $key => $condition) {
$query->addCondition($select_query, "$field_base_table.$key", $condition);
}
// Order the query.
foreach ($query->order as $order) {
if ($order['type'] == 'entity') {
$key = $order['specifier'];
$select_query->orderBy("$field_base_table.$key", $order['direction']);
}
elseif ($order['type'] == 'field') {
$specifier = $order['specifier'];
$field = $specifier['field'];
$table_alias = $table_aliases[$specifier['index']];
$sql_field = "$table_alias." . _field_sql_storage_columnname($field['field_name'], $specifier['column']);
$select_query->orderBy($sql_field, $order['direction']);
}
elseif ($order['type'] == 'property') {
$select_query->orderBy("$entity_base_table." . $order['specifier'], $order['direction']);
}
}
return $query->finishQuery($select_query, $id_key);
}
/**
* Adds the base entity table to a field query object.
*
* @param SelectQuery $select_query
* A SelectQuery containing at least one table as specified by
* _field_sql_storage_tablename().
* @param $entity_type
* The entity type for which the base table should be joined.
* @param $field_base_table
* Name of a table in $select_query. As only INNER JOINs are used, it does
* not matter which.
*
* @return
* The name of the entity base table joined in.
*/
function _field_sql_storage_query_join_entity(SelectQuery $select_query, $entity_type, $field_base_table) {
$entity_info = entity_get_info($entity_type);
$entity_base_table = $entity_info['base table'];
$entity_field = $entity_info['entity keys']['id'];
$select_query->join($entity_base_table, $entity_base_table, "$entity_base_table.$entity_field = $field_base_table.entity_id");
return $entity_base_table;
}
/**
* Adds field (meta) conditions to the given query objects respecting groupings.
*
* @param EntityFieldQuery $query
* The field query object to be processed.
* @param SelectQuery $select_query
* The SelectQuery that should get grouping conditions.
* @param condtions
* The conditions to be added.
* @param $table_aliases
* An associative array of table aliases keyed by field index.
* @param $column_callback
* A callback that should return the column name to be used for the field
* conditions. Accepts a field name and a field column name as parameters.
*/
function _field_sql_storage_query_field_conditions(EntityFieldQuery $query, SelectQuery $select_query, $conditions, $table_aliases, $column_callback) {
$groups = &drupal_static(__FUNCTION__, array());
foreach ($conditions as $key => $condition) {
$table_alias = $table_aliases[$key];
$field = $condition['field'];
// Add the specified condition.
$sql_field = "$table_alias." . $column_callback($field['field_name'], $condition['column']);
$query->addCondition($select_query, $sql_field, $condition);
// Add delta / language group conditions.
foreach (array('delta', 'language') as $column) {
if (isset($condition[$column . '_group'])) {
$group_name = $condition[$column . '_group'];
if (!isset($groups[$column][$group_name])) {
$groups[$column][$group_name] = $table_alias;
}
else {
$select_query->where("$table_alias.$column = " . $groups[$column][$group_name] . ".$column");
}
}
}
}
}
/**
* Field meta condition column callback.
*/
function _field_sql_storage_query_columnname($field_name, $column) {
return $column;
}
/**
* Implements hook_field_storage_delete_revision().
*
* This function actually deletes the data from the database.
*/
function field_sql_storage_field_storage_delete_revision($entity_type, $entity, $fields) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
if (isset($vid)) {
foreach ($fields as $field_id) {
$field = field_info_field_by_id($field_id);
$revision_name = _field_sql_storage_revision_tablename($field);
db_delete($revision_name)
->condition('entity_type', $entity_type)
->condition('entity_id', $id)
->condition('revision_id', $vid)
->execute();
}
}
}
/**
* Implements hook_field_storage_delete_instance().
*
* This function simply marks for deletion all data associated with the field.
*/
function field_sql_storage_field_storage_delete_instance($instance) {
$field = field_info_field($instance['field_name']);
$table_name = _field_sql_storage_tablename($field);
$revision_name = _field_sql_storage_revision_tablename($field);
db_update($table_name)
->fields(array('deleted' => 1))
->condition('entity_type', $instance['entity_type'])
->condition('bundle', $instance['bundle'])
->execute();
db_update($revision_name)
->fields(array('deleted' => 1))
->condition('entity_type', $instance['entity_type'])
->condition('bundle', $instance['bundle'])
->execute();
}
/**
* Implements hook_field_attach_rename_bundle().
*/
function field_sql_storage_field_attach_rename_bundle($entity_type, $bundle_old, $bundle_new) {
// We need to account for deleted or inactive fields and instances.
$instances = field_read_instances(array('entity_type' => $entity_type, 'bundle' => $bundle_new), array('include_deleted' => TRUE, 'include_inactive' => TRUE));
foreach ($instances as $instance) {
$field = field_info_field_by_id($instance['field_id']);
if ($field['storage']['type'] == 'field_sql_storage') {
$table_name = _field_sql_storage_tablename($field);
$revision_name = _field_sql_storage_revision_tablename($field);
db_update($table_name)
->fields(array('bundle' => $bundle_new))
->condition('entity_type', $entity_type)
->condition('bundle', $bundle_old)
->execute();
db_update($revision_name)
->fields(array('bundle' => $bundle_new))
->condition('entity_type', $entity_type)
->condition('bundle', $bundle_old)
->execute();
}
}
}
/**
* Implements hook_field_storage_purge_field().
*
* All field data items and instances have already been purged, so all
* that is left is to delete the table.
*/
function field_sql_storage_field_storage_purge_field($field) {
$table_name = _field_sql_storage_tablename($field);
$revision_name = _field_sql_storage_revision_tablename($field);
db_drop_table($table_name);
db_drop_table($revision_name);
}
/**
* Implements hook_field_storage_details().
*/
function field_sql_storage_field_storage_details($field) {
$details = array();
if (!empty($field['columns'])) {
// Add field columns.
foreach ($field['columns'] as $column_name => $attributes) {
$real_name = _field_sql_storage_columnname($field['field_name'], $column_name);
$columns[$column_name] = $real_name;
}
return array(
'sql' => array(
FIELD_LOAD_CURRENT => array(
_field_sql_storage_tablename($field) => $columns,
),
FIELD_LOAD_REVISION => array(
_field_sql_storage_revision_tablename($field) => $columns,
),
),
);
}
}
'Field SQL storage tests',
'description' => "Test field SQL storage module.",
'group' => 'Field API'
);
}
function setUp() {
parent::setUp('field_sql_storage', 'field', 'field_test', 'text');
$this->field_name = strtolower($this->randomName());
$this->field = array('field_name' => $this->field_name, 'type' => 'test_field', 'cardinality' => 4);
$this->field = field_create_field($this->field);
$this->instance = array(
'field_name' => $this->field_name,
'entity_type' => 'test_entity',
'bundle' => 'test_bundle'
);
$this->instance = field_create_instance($this->instance);
$this->table = _field_sql_storage_tablename($this->field);
$this->revision_table = _field_sql_storage_revision_tablename($this->field);
}
/**
* Uses the mysql tables and records to verify
* field_load_revision works correctly.
*/
function testFieldAttachLoad() {
$entity_type = 'test_entity';
$eid = 0;
$langcode = LANGUAGE_NONE;
$columns = array('entity_type', 'entity_id', 'revision_id', 'delta', 'language', $this->field_name . '_value');
// Insert data for four revisions to the field revisions table
$query = db_insert($this->revision_table)->fields($columns);
for ($evid = 0; $evid < 4; ++$evid) {
$values[$evid] = array();
// Note: we insert one extra value ('<=' instead of '<').
for ($delta = 0; $delta <= $this->field['cardinality']; $delta++) {
$value = mt_rand(1, 127);
$values[$evid][] = $value;
$query->values(array($entity_type, $eid, $evid, $delta, $langcode, $value));
}
}
$query->execute();
// Insert data for the "most current revision" into the field table
$query = db_insert($this->table)->fields($columns);
foreach ($values[0] as $delta => $value) {
$query->values(array($entity_type, $eid, 0, $delta, $langcode, $value));
}
$query->execute();
// Load the "most current revision"
$entity = field_test_create_stub_entity($eid, 0, $this->instance['bundle']);
field_attach_load($entity_type, array($eid => $entity));
foreach ($values[0] as $delta => $value) {
if ($delta < $this->field['cardinality']) {
$this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['value'], $value, "Value $delta is loaded correctly for current revision");
}
else {
$this->assertFalse(array_key_exists($delta, $entity->{$this->field_name}[$langcode]), "No extraneous value gets loaded for current revision.");
}
}
// Load every revision
for ($evid = 0; $evid < 4; ++$evid) {
$entity = field_test_create_stub_entity($eid, $evid, $this->instance['bundle']);
field_attach_load_revision($entity_type, array($eid => $entity));
foreach ($values[$evid] as $delta => $value) {
if ($delta < $this->field['cardinality']) {
$this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['value'], $value, "Value $delta for revision $evid is loaded correctly");
}
else {
$this->assertFalse(array_key_exists($delta, $entity->{$this->field_name}[$langcode]), "No extraneous value gets loaded for revision $evid.");
}
}
}
// Add a translation in an unavailable language and verify it is not loaded.
$eid = $evid = 1;
$unavailable_language = 'xx';
$entity = field_test_create_stub_entity($eid, $evid, $this->instance['bundle']);
$values = array($entity_type, $eid, $evid, 0, $unavailable_language, mt_rand(1, 127));
db_insert($this->table)->fields($columns)->values($values)->execute();
db_insert($this->revision_table)->fields($columns)->values($values)->execute();
field_attach_load($entity_type, array($eid => $entity));
$this->assertFalse(array_key_exists($unavailable_language, $entity->{$this->field_name}), 'Field translation in an unavailable language ignored');
}
/**
* Tests adding a field with an entity ID type of string.
*/
function testFieldSqlSchemaForEntityWithStringIdentifier() {
// Test programmatically adding field with string ID.
$field_name = 'string_id_example';
$field = array('field_name' => $field_name, 'type' => 'text', 'settings' => array('max_length' => 255), 'entity_id_type' => 'string');
field_create_field($field);
$schema = drupal_get_schema('field_data_' . $field_name);
$this->assertEqual($schema['fields']['entity_id']['type'], 'varchar');
$this->assertEqual($schema['fields']['revision_id']['type'], 'varchar');
// Test programmatically adding field with default ID(int).
$field_name = 'default_id_example';
$field = array('field_name' => $field_name, 'type' => 'text', 'settings' => array('max_length' => 255));
field_create_field($field);
$schema = drupal_get_schema('field_data_' . $field_name);
$this->assertEqual($schema['fields']['entity_id']['type'], 'int');
$this->assertEqual($schema['fields']['revision_id']['type'], 'int');
}
/**
* Reads mysql to verify correct data is
* written when using insert and update.
*/
function testFieldAttachInsertAndUpdate() {
$entity_type = 'test_entity';
$entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
$langcode = LANGUAGE_NONE;
// Test insert.
$values = array();
// Note: we try to insert one extra value ('<=' instead of '<').
// TODO : test empty values filtering and "compression" (store consecutive deltas).
for ($delta = 0; $delta <= $this->field['cardinality']; $delta++) {
$values[$delta]['value'] = mt_rand(1, 127);
}
$entity->{$this->field_name}[$langcode] = $rev_values[0] = $values;
field_attach_insert($entity_type, $entity);
$rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC);
foreach ($values as $delta => $value) {
if ($delta < $this->field['cardinality']) {
$this->assertEqual($rows[$delta][$this->field_name . '_value'], $value['value'], format_string("Value %delta is inserted correctly", array('%delta' => $delta)));
}
else {
$this->assertFalse(array_key_exists($delta, $rows), "No extraneous value gets inserted.");
}
}
// Test update.
$entity = field_test_create_stub_entity(0, 1, $this->instance['bundle']);
$values = array();
// Note: we try to update one extra value ('<=' instead of '<').
for ($delta = 0; $delta <= $this->field['cardinality']; $delta++) {
$values[$delta]['value'] = mt_rand(1, 127);
}
$entity->{$this->field_name}[$langcode] = $rev_values[1] = $values;
field_attach_update($entity_type, $entity);
$rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC);
foreach ($values as $delta => $value) {
if ($delta < $this->field['cardinality']) {
$this->assertEqual($rows[$delta][$this->field_name . '_value'], $value['value'], format_string("Value %delta is updated correctly", array('%delta' => $delta)));
}
else {
$this->assertFalse(array_key_exists($delta, $rows), "No extraneous value gets updated.");
}
}
// Check that data for both revisions are in the revision table.
// We make sure each value is stored correctly, then unset it.
// When an entire revision's values are unset (remembering that we
// put one extra value in $values per revision), unset the entire
// revision. Then, if $rev_values is empty at the end, all
// revision data was found.
$results = db_select($this->revision_table, 't')->fields('t')->execute();
foreach ($results as $row) {
$this->assertEqual($row->{$this->field_name . '_value'}, $rev_values[$row->revision_id][$row->delta]['value'], "Value {$row->delta} for revision {$row->revision_id} stored correctly");
unset($rev_values[$row->revision_id][$row->delta]);
if (count($rev_values[$row->revision_id]) == 1) {
unset($rev_values[$row->revision_id]);
}
}
$this->assertTrue(empty($rev_values), "All values for all revisions are stored in revision table {$this->revision_table}");
// Check that update leaves the field data untouched if
// $entity->{$field_name} is absent.
unset($entity->{$this->field_name});
field_attach_update($entity_type, $entity);
$rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC);
foreach ($values as $delta => $value) {
if ($delta < $this->field['cardinality']) {
$this->assertEqual($rows[$delta][$this->field_name . '_value'], $value['value'], format_string("Update with no field_name entry leaves value %delta untouched", array('%delta' => $delta)));
}
}
// Check that update with an empty $entity->$field_name empties the field.
$entity->{$this->field_name} = NULL;
field_attach_update($entity_type, $entity);
$rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC);
$this->assertEqual(count($rows), 0, "Update with an empty field_name entry empties the field.");
}
/**
* Tests insert and update with missing or NULL fields.
*/
function testFieldAttachSaveMissingData() {
$entity_type = 'test_entity';
$entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
$langcode = LANGUAGE_NONE;
// Insert: Field is missing
field_attach_insert($entity_type, $entity);
$count = db_select($this->table)
->countQuery()
->execute()
->fetchField();
$this->assertEqual($count, 0, 'Missing field results in no inserts');
// Insert: Field is NULL
$entity->{$this->field_name} = NULL;
field_attach_insert($entity_type, $entity);
$count = db_select($this->table)
->countQuery()
->execute()
->fetchField();
$this->assertEqual($count, 0, 'NULL field results in no inserts');
// Add some real data
$entity->{$this->field_name}[$langcode] = array(0 => array('value' => 1));
field_attach_insert($entity_type, $entity);
$count = db_select($this->table)
->countQuery()
->execute()
->fetchField();
$this->assertEqual($count, 1, 'Field data saved');
// Update: Field is missing. Data should survive.
unset($entity->{$this->field_name});
field_attach_update($entity_type, $entity);
$count = db_select($this->table)
->countQuery()
->execute()
->fetchField();
$this->assertEqual($count, 1, 'Missing field leaves data in table');
// Update: Field is NULL. Data should be wiped.
$entity->{$this->field_name} = NULL;
field_attach_update($entity_type, $entity);
$count = db_select($this->table)
->countQuery()
->execute()
->fetchField();
$this->assertEqual($count, 0, 'NULL field leaves no data in table');
// Add a translation in an unavailable language.
$unavailable_language = 'xx';
db_insert($this->table)
->fields(array('entity_type', 'bundle', 'deleted', 'entity_id', 'revision_id', 'delta', 'language'))
->values(array($entity_type, $this->instance['bundle'], 0, 0, 0, 0, $unavailable_language))
->execute();
$count = db_select($this->table)
->countQuery()
->execute()
->fetchField();
$this->assertEqual($count, 1, 'Field translation in an unavailable language saved.');
// Again add some real data.
$entity->{$this->field_name}[$langcode] = array(0 => array('value' => 1));
field_attach_insert($entity_type, $entity);
$count = db_select($this->table)
->countQuery()
->execute()
->fetchField();
$this->assertEqual($count, 2, 'Field data saved.');
// Update: Field translation is missing but field is not empty. Translation
// data should survive.
$entity->{$this->field_name}[$unavailable_language] = array(mt_rand(1, 127));
unset($entity->{$this->field_name}[$langcode]);
field_attach_update($entity_type, $entity);
$count = db_select($this->table)
->countQuery()
->execute()
->fetchField();
$this->assertEqual($count, 2, 'Missing field translation leaves data in table.');
// Update: Field translation is NULL but field is not empty. Translation
// data should be wiped.
$entity->{$this->field_name}[$langcode] = NULL;
field_attach_update($entity_type, $entity);
$count = db_select($this->table)
->countQuery()
->execute()
->fetchField();
$this->assertEqual($count, 1, 'NULL field translation is wiped.');
}
/**
* Tests the expected return values of _field_sql_storage_write_compare().
*/
public function testFieldCompareDataModification() {
$langcode = LANGUAGE_NONE;
$field_info = field_info_field($this->field_name);
// Make sure we have 2 sample field values that are unique.
$value1 = 0;
$value2 = 0;
while ($value1 == $value2) {
$value1 = mt_rand();
$value2 = (string) mt_rand();
}
// Create the 2 entities to compare.
$entity = field_test_create_stub_entity();
$entity->{$this->field_name}[$langcode][]['value'] = $value1;
$entity1 = clone $entity;
$entity2 = clone $entity;
// Make sure that it correctly compares identical entities.
$this->assert(!_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'The entities are identical.');
// Compare to an empty object.
$this->assert(_field_sql_storage_write_compare($field_info, $entity1, new stdClass()), 'The entity is not the same as an empty object.');
// Change one of the values.
$entity2->{$this->field_name}[$langcode][0]['value'] = $value2;
$this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'The values are not the same.');
// Reset $entity2.
$entity2 = clone $entity;
// Duplicate the value on one of the entities.
$entity1->{$this->field_name}[$langcode][]['value'] = $value1;
$this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'The fields do not have the same number of values.');
// Add a second value to both entities.
$entity2->{$this->field_name}[$langcode][]['value'] = $value2;
$this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'The values are not the same.');
// Replace the array containing the value with the actual value.
$entity2->{$this->field_name}[$langcode] = $entity2->{$this->field_name}[$langcode][0];
$this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'The array to hold field values is replaced by the value.');
// Null one value.
$entity2->{$this->field_name}[$langcode] = NULL;
$this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'One field is NULL and the other is not.');
// Null both values.
$entity1->{$this->field_name}[$langcode] = NULL;
$this->assert(!_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'Both fields are NULL.');
// Unset one of the fields.
unset($entity2->{$this->field_name});
$this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'One field structure is unset.');
// Unset both of the fields.
unset($entity1->{$this->field_name});
$this->assert(!_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'Both field structures are unset.');
}
/**
* Test trying to update a field with data.
*/
function testUpdateFieldSchemaWithData() {
// Create a decimal 5.2 field and add some data.
$field = array('field_name' => 'decimal52', 'type' => 'number_decimal', 'settings' => array('precision' => 5, 'scale' => 2));
$field = field_create_field($field);
$instance = array('field_name' => 'decimal52', 'entity_type' => 'test_entity', 'bundle' => 'test_bundle');
$instance = field_create_instance($instance);
$entity = field_test_create_stub_entity(0, 0, $instance['bundle']);
$entity->decimal52[LANGUAGE_NONE][0]['value'] = '1.235';
field_attach_insert('test_entity', $entity);
// Attempt to update the field in a way that would work without data.
$field['settings']['scale'] = 3;
try {
field_update_field($field);
$this->fail(t('Cannot update field schema with data.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot update field schema with data.'));
}
}
/**
* Test that failure to create fields is handled gracefully.
*/
function testFieldUpdateFailure() {
// Create a text field.
$field = array('field_name' => 'test_text', 'type' => 'text', 'settings' => array('max_length' => 255));
$field = field_create_field($field);
// Attempt to update the field in a way that would break the storage. The
// parenthesis suffix is needed because SQLite has *very* relaxed rules for
// data types, so we actually need to provide an invalid SQL syntax in order
// to break it.
// @see https://www.sqlite.org/datatype3.html
$prior_field = $field;
$field['settings']['max_length'] = '-1)';
try {
field_update_field($field);
$this->fail(t('Update succeeded.'));
}
catch (Exception $e) {
$this->pass(t('Update properly failed.'));
}
// Ensure that the field tables are still there.
foreach (_field_sql_storage_schema($prior_field) as $table_name => $table_info) {
$this->assertTrue(db_table_exists($table_name), format_string('Table %table exists.', array('%table' => $table_name)));
}
}
/**
* Test adding and removing indexes while data is present.
*/
function testFieldUpdateIndexesWithData() {
// Create a decimal field.
$field_name = 'testfield';
$field = array('field_name' => $field_name, 'type' => 'text');
$field = field_create_field($field);
$instance = array('field_name' => $field_name, 'entity_type' => 'test_entity', 'bundle' => 'test_bundle');
$instance = field_create_instance($instance);
$tables = array(_field_sql_storage_tablename($field), _field_sql_storage_revision_tablename($field));
// Verify the indexes we will create do not exist yet.
foreach ($tables as $table) {
$this->assertFalse(Database::getConnection()->schema()->indexExists($table, 'value'), format_string("No index named value exists in %table", array('%table' => $table)));
$this->assertFalse(Database::getConnection()->schema()->indexExists($table, 'value_format'), format_string("No index named value_format exists in %table", array('%table' => $table)));
}
// Add data so the table cannot be dropped.
$entity = field_test_create_stub_entity(0, 0, $instance['bundle']);
$entity->{$field_name}[LANGUAGE_NONE][0]['value'] = 'field data';
field_attach_insert('test_entity', $entity);
// Add an index
$field = array('field_name' => $field_name, 'indexes' => array('value' => array(array('value', 255))));
field_update_field($field);
foreach ($tables as $table) {
$this->assertTrue(Database::getConnection()->schema()->indexExists($table, "{$field_name}_value"), format_string("Index on value created in %table", array('%table' => $table)));
}
// Add a different index, removing the existing custom one.
$field = array('field_name' => $field_name, 'indexes' => array('value_format' => array(array('value', 127), array('format', 127))));
field_update_field($field);
foreach ($tables as $table) {
$this->assertTrue(Database::getConnection()->schema()->indexExists($table, "{$field_name}_value_format"), format_string("Index on value_format created in %table", array('%table' => $table)));
$this->assertFalse(Database::getConnection()->schema()->indexExists($table, "{$field_name}_value"), format_string("Index on value removed in %table", array('%table' => $table)));
}
// Verify that the tables were not dropped.
$entity = field_test_create_stub_entity(0, 0, $instance['bundle']);
field_attach_load('test_entity', array(0 => $entity));
$this->assertEqual($entity->{$field_name}[LANGUAGE_NONE][0]['value'], 'field data', "Index changes performed without dropping the tables");
}
/**
* Test the storage details.
*/
function testFieldStorageDetails() {
$current = _field_sql_storage_tablename($this->field);
$revision = _field_sql_storage_revision_tablename($this->field);
// Retrieve the field and instance with field_info so the storage details are attached.
$field = field_info_field($this->field['field_name']);
$instance = field_info_instance($this->instance['entity_type'], $this->instance['field_name'], $this->instance['bundle']);
// The storage details are indexed by a storage engine type.
$this->assertTrue(array_key_exists('sql', $field['storage']['details']), 'The storage type is SQL.');
// The SQL details are indexed by table name.
$details = $field['storage']['details']['sql'];
$this->assertTrue(array_key_exists($current, $details[FIELD_LOAD_CURRENT]), 'Table name is available in the instance array.');
$this->assertTrue(array_key_exists($revision, $details[FIELD_LOAD_REVISION]), 'Revision table name is available in the instance array.');
// Test current and revision storage details together because the columns
// are the same.
foreach ((array) $this->field['columns'] as $column_name => $attributes) {
$storage_column_name = _field_sql_storage_columnname($this->field['field_name'], $column_name);
$this->assertEqual($details[FIELD_LOAD_CURRENT][$current][$column_name], $storage_column_name, format_string('Column name %value matches the definition in %bin.', array('%value' => $column_name, '%bin' => $current)));
$this->assertEqual($details[FIELD_LOAD_REVISION][$revision][$column_name], $storage_column_name, format_string('Column name %value matches the definition in %bin.', array('%value' => $column_name, '%bin' => $revision)));
}
}
/**
* Test foreign key support.
*/
function testFieldSqlStorageForeignKeys() {
// Create a 'shape' field, with a configurable foreign key (see
// field_test_field_schema()).
$field_name = 'testfield';
$foreign_key_name = 'shape';
$field = array('field_name' => $field_name, 'type' => 'shape', 'settings' => array('foreign_key_name' => $foreign_key_name));
field_create_field($field);
// Retrieve the field definition and check that the foreign key is in place.
$field = field_info_field($field_name);
$this->assertEqual($field['foreign keys'][$foreign_key_name]['table'], $foreign_key_name, 'Foreign key table name preserved through CRUD');
$this->assertEqual($field['foreign keys'][$foreign_key_name]['columns'][$foreign_key_name], 'id', 'Foreign key column name preserved through CRUD');
// Update the field settings, it should update the foreign key definition
// too.
$foreign_key_name = 'color';
$field['settings']['foreign_key_name'] = $foreign_key_name;
field_update_field($field);
// Retrieve the field definition and check that the foreign key is in place.
$field = field_info_field($field_name);
$this->assertEqual($field['foreign keys'][$foreign_key_name]['table'], $foreign_key_name, 'Foreign key table name modified after update');
$this->assertEqual($field['foreign keys'][$foreign_key_name]['columns'][$foreign_key_name], 'id', 'Foreign key column name modified after update');
// Now grab the SQL schema and verify that too.
$schema = drupal_get_schema(_field_sql_storage_tablename($field), TRUE);
$this->assertEqual(count($schema['foreign keys']), 1, 'There is 1 foreign key in the schema');
$foreign_key = reset($schema['foreign keys']);
$foreign_key_column = _field_sql_storage_columnname($field['field_name'], $foreign_key_name);
$this->assertEqual($foreign_key['table'], $foreign_key_name, 'Foreign key table name preserved in the schema');
$this->assertEqual($foreign_key['columns'][$foreign_key_column], 'id', 'Foreign key column name preserved in the schema');
}
/**
* Test handling multiple conditions on one column of a field.
*
* Tests both the result and the complexity of the query.
*/
function testFieldSqlStorageMultipleConditionsSameColumn() {
$entity = field_test_create_stub_entity(NULL, NULL);
$entity->{$this->field_name}[LANGUAGE_NONE][0] = array('value' => 1);
field_test_entity_save($entity);
$entity = field_test_create_stub_entity(NULL, NULL);
$entity->{$this->field_name}[LANGUAGE_NONE][0] = array('value' => 2);
field_test_entity_save($entity);
$entity = field_test_create_stub_entity(NULL, NULL);
$entity->{$this->field_name}[LANGUAGE_NONE][0] = array('value' => 3);
field_test_entity_save($entity);
$query = new EntityFieldQuery();
// This tag causes field_test_query_store_global_test_query_alter() to be
// invoked so that the query can be tested.
$query->addTag('store_global_test_query');
$query->entityCondition('entity_type', 'test_entity');
$query->entityCondition('bundle', 'test_bundle');
$query->fieldCondition($this->field_name, 'value', 1, '<>', 0, LANGUAGE_NONE);
$query->fieldCondition($this->field_name, 'value', 2, '<>', 0, LANGUAGE_NONE);
$result = field_sql_storage_field_storage_query($query);
// Test the results.
$this->assertEqual(1, count($result), format_string('One result should be returned, got @count', array('@count' => count($result))));
// Test the complexity of the query.
$query = $GLOBALS['test_query'];
$this->assertNotNull($query, 'Precondition: the query should be available');
$tables = $query->getTables();
$this->assertEqual(1, count($tables), 'The query contains just one table.');
// Clean up.
unset($GLOBALS['test_query']);
}
/**
* Test handling multiple conditions on multiple columns of one field.
*
* Tests both the result and the complexity of the query.
*/
function testFieldSqlStorageMultipleConditionsDifferentColumns() {
// Create the multi-column shape field
$field_name = strtolower($this->randomName());
$field = array('field_name' => $field_name, 'type' => 'shape', 'cardinality' => 4);
$field = field_create_field($field);
$instance = array(
'field_name' => $field_name,
'entity_type' => 'test_entity',
'bundle' => 'test_bundle'
);
$instance = field_create_instance($instance);
$entity = field_test_create_stub_entity(NULL, NULL);
$entity->{$field_name}[LANGUAGE_NONE][0] = array('shape' => 'A', 'color' => 'X');
field_test_entity_save($entity);
$entity = field_test_create_stub_entity(NULL, NULL);
$entity->{$field_name}[LANGUAGE_NONE][0] = array('shape' => 'B', 'color' => 'X');
field_test_entity_save($entity);
$entity = field_test_create_stub_entity(NULL, NULL);
$entity->{$field_name}[LANGUAGE_NONE][0] = array('shape' => 'A', 'color' => 'Y');
field_test_entity_save($entity);
$query = new EntityFieldQuery();
// This tag causes field_test_query_store_global_test_query_alter() to be
// invoked so that the query can be tested.
$query->addTag('store_global_test_query');
$query->entityCondition('entity_type', 'test_entity');
$query->entityCondition('bundle', 'test_bundle');
$query->fieldCondition($field_name, 'shape', 'B', '=', 'something', LANGUAGE_NONE);
$query->fieldCondition($field_name, 'color', 'X', '=', 'something', LANGUAGE_NONE);
$result = field_sql_storage_field_storage_query($query);
// Test the results.
$this->assertEqual(1, count($result), format_string('One result should be returned, got @count', array('@count' => count($result))));
// Test the complexity of the query.
$query = $GLOBALS['test_query'];
$this->assertNotNull($query, 'Precondition: the query should be available');
$tables = $query->getTables();
$this->assertEqual(1, count($tables), 'The query contains just one table.');
// Clean up.
unset($GLOBALS['test_query']);
}
/**
* Test handling multiple conditions on multiple columns of one field for multiple languages.
*
* Tests both the result and the complexity of the query.
*/
function testFieldSqlStorageMultipleConditionsDifferentColumnsMultipleLanguages() {
field_test_entity_info_translatable('test_entity', TRUE);
// Create the multi-column shape field
$field_name = strtolower($this->randomName());
$field = array('field_name' => $field_name, 'type' => 'shape', 'cardinality' => 4, 'translatable' => TRUE);
$field = field_create_field($field);
$instance = array(
'field_name' => $field_name,
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'settings' => array(
// Prevent warning from field_test_field_load().
'test_hook_field_load' => FALSE,
),
);
$instance = field_create_instance($instance);
$entity = field_test_create_stub_entity(NULL, NULL);
$entity->{$field_name}[LANGUAGE_NONE][0] = array('shape' => 'A', 'color' => 'X');
$entity->{$field_name}['en'][0] = array('shape' => 'B', 'color' => 'Y');
field_test_entity_save($entity);
$entity = field_test_entity_test_load($entity->ftid);
$query = new EntityFieldQuery();
// This tag causes field_test_query_store_global_test_query_alter() to be
// invoked so that the query can be tested.
$query->addTag('store_global_test_query');
$query->entityCondition('entity_type', 'test_entity');
$query->entityCondition('bundle', 'test_bundle');
$query->fieldCondition($field_name, 'color', 'X', '=', NULL, LANGUAGE_NONE);
$query->fieldCondition($field_name, 'shape', 'B', '=', NULL, 'en');
$result = field_sql_storage_field_storage_query($query);
// Test the results.
$this->assertEqual(1, count($result), format_string('One result should be returned, got @count', array('@count' => count($result))));
// Test the complexity of the query.
$query = $GLOBALS['test_query'];
$this->assertNotNull($query, 'Precondition: the query should be available');
$tables = $query->getTables();
$this->assertEqual(2, count($tables), 'The query contains two tables.');
// Clean up.
unset($GLOBALS['test_query']);
}
}
name = List
description = Defines list field types. Use with Options to create selection lists.
package = Core
version = VERSION
core = 7.x
dependencies[] = field
dependencies[] = options
files[] = tests/list.test
; Information added by Drupal.org packaging script on 2023-03-15
version = "7.95"
project = "drupal"
datestamp = "1678902530"
array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
),
);
break;
case 'list_float':
$columns = array(
'value' => array(
'type' => 'float',
'not null' => FALSE,
),
);
break;
case 'list_integer':
case 'list_boolean':
$columns = array(
'value' => array(
'type' => 'int',
'not null' => FALSE,
),
);
break;
}
return array(
'columns' => $columns,
'indexes' => array(
'value' => array('value'),
),
);
}
/**
* Rename the list field types and change 'allowed_values' format.
*/
function list_update_7001() {
$fields = _update_7000_field_read_fields(array('module' => 'list'));
foreach ($fields as $field) {
$update = array();
// Translate the old string format into the new array format.
$allowed_values = $field['settings']['allowed_values'];
if (is_string($allowed_values)) {
$position_keys = ($field['type'] == 'list');
$allowed_values = _list_update_7001_extract_allowed_values($allowed_values, $position_keys);
// Additionally, float keys need to be disambiguated ('.5' is '0.5').
if ($field['type'] == 'list_number' && !empty($allowed_values)) {
$keys = array_map('_list_update_7001_float_string_cast', array_keys($allowed_values));
$allowed_values = array_combine($keys, array_values($allowed_values));
}
// Place the new setting in the existing serialized 'data' column.
$data = db_query("SELECT data FROM {field_config} WHERE id = :id", array(':id' => $field['id']))->fetchField();
$data = unserialize($data);
$data['settings']['allowed_values'] = $allowed_values;
$update['data'] = serialize($data);
}
// Rename field types.
$types = array('list' => 'list_integer', 'list_number' => 'list_float');
if (isset($types[$field['type']])) {
$update['type'] = $types[$field['type']];
}
// Save the new data.
if ($update) {
$query = db_update('field_config')
->condition('id', $field['id'])
->fields($update)
->execute();
}
}
}
/**
* Helper callback function to cast the array element.
*/
function _list_update_7001_float_string_cast($element) {
return (string) (float) $element;
}
/**
* Helper function for list_update_7001: extract allowed values from a string.
*
* This reproduces the parsing logic in use before D7 RC2.
*/
function _list_update_7001_extract_allowed_values($string, $position_keys) {
$values = array();
$list = explode("\n", $string);
$list = array_map('trim', $list);
$list = array_filter($list, 'strlen');
foreach ($list as $key => $value) {
// Check for a manually specified key.
if (strpos($value, '|') !== FALSE) {
list($key, $value) = explode('|', $value);
}
// Otherwise see if we need to use the value as the key. The "list" type
// will automatically convert non-keyed lines to integers.
elseif (!$position_keys) {
$key = $value;
}
$values[$key] = (isset($value) && $value !== '') ? $value : $key;
}
return $values;
}
/**
* @addtogroup updates-7.x-extra
* @{
*/
/**
* Re-apply list_update_7001() for deleted fields.
*/
function list_update_7002() {
// See http://drupal.org/node/1022924: list_update_7001() intitally
// overlooked deleted fields, which then caused fatal errors when the fields
// were being purged.
// list_update_7001() has the required checks to ensure it is reentrant, so
// it can simply be executed once more..
list_update_7001();
}
/**
* @} End of "addtogroup updates-7.x-extra".
*/
' . t('About') . '';
$output .= '' . t('The List module defines various fields for storing a list of items, for use with the Field module. Usually these items are entered through a select list, checkboxes, or radio buttons. See the Field module help page for more information about fields.', array('@field-help' => url('admin/help/field'))) . '
';
return $output;
}
}
/**
* Implements hook_field_info().
*/
function list_field_info() {
return array(
'list_integer' => array(
'label' => t('List (integer)'),
'description' => t("This field stores integer values from a list of allowed 'value => label' pairs, i.e. 'Lifetime in days': 1 => 1 day, 7 => 1 week, 31 => 1 month."),
'settings' => array('allowed_values' => array(), 'allowed_values_function' => ''),
'default_widget' => 'options_select',
'default_formatter' => 'list_default',
),
'list_float' => array(
'label' => t('List (float)'),
'description' => t("This field stores float values from a list of allowed 'value => label' pairs, i.e. 'Fraction': 0 => 0, .25 => 1/4, .75 => 3/4, 1 => 1."),
'settings' => array('allowed_values' => array(), 'allowed_values_function' => ''),
'default_widget' => 'options_select',
'default_formatter' => 'list_default',
),
'list_text' => array(
'label' => t('List (text)'),
'description' => t("This field stores text values from a list of allowed 'value => label' pairs, i.e. 'US States': IL => Illinois, IA => Iowa, IN => Indiana."),
'settings' => array('allowed_values' => array(), 'allowed_values_function' => ''),
'default_widget' => 'options_select',
'default_formatter' => 'list_default',
),
'list_boolean' => array(
'label' => t('Boolean'),
'description' => t('This field stores simple on/off or yes/no options.'),
'settings' => array('allowed_values' => array(), 'allowed_values_function' => ''),
'default_widget' => 'options_buttons',
'default_formatter' => 'list_default',
),
);
}
/**
* Implements hook_field_settings_form().
*/
function list_field_settings_form($field, $instance, $has_data) {
$settings = $field['settings'];
switch ($field['type']) {
case 'list_integer':
case 'list_float':
case 'list_text':
$form['allowed_values'] = array(
'#type' => 'textarea',
'#title' => t('Allowed values list'),
'#default_value' => empty($settings['allowed_values_function']) ? list_allowed_values_string($settings['allowed_values']) : array(),
'#rows' => 10,
'#element_validate' => array('list_allowed_values_setting_validate'),
'#field_has_data' => $has_data,
'#field' => $field,
'#field_type' => $field['type'],
'#access' => empty($settings['allowed_values_function']),
);
$description = '' . t('The possible values this field can contain. Enter one value per line, in the format key|label.');
if ($field['type'] == 'list_integer' || $field['type'] == 'list_float') {
$description .= ' ' . t('The key is the stored value, and must be numeric. The label will be used in displayed values and edit forms.');
$description .= ' ' . t('The label is optional: if a line contains a single number, it will be used as key and label.');
$description .= ' ' . t('Lists of labels are also accepted (one label per line), only if the field does not hold any values yet. Numeric keys will be automatically generated from the positions in the list.');
}
else {
$description .= ' ' . t('The key is the stored value. The label will be used in displayed values and edit forms.');
$description .= ' ' . t('The label is optional: if a line contains a single string, it will be used as key and label.');
}
$description .= '
';
$form['allowed_values']['#description'] = $description;
break;
case 'list_boolean':
$values = $settings['allowed_values'];
$off_value = array_shift($values);
$on_value = array_shift($values);
$form['allowed_values'] = array(
'#type' => 'value',
'#description' => '',
'#value_callback' => 'list_boolean_allowed_values_callback',
'#access' => empty($settings['allowed_values_function']),
);
$form['allowed_values']['on'] = array(
'#type' => 'textfield',
'#title' => t('On value'),
'#default_value' => $on_value,
'#required' => FALSE,
'#description' => t('If left empty, "1" will be used.'),
// Change #parents to make sure the element is not saved into field
// settings.
'#parents' => array('on'),
);
$form['allowed_values']['off'] = array(
'#type' => 'textfield',
'#title' => t('Off value'),
'#default_value' => $off_value,
'#required' => FALSE,
'#description' => t('If left empty, "0" will be used.'),
// Change #parents to make sure the element is not saved into field
// settings.
'#parents' => array('off'),
);
// Link the allowed value to the on / off elements to prepare for the rare
// case of an alter changing #parents.
$form['allowed_values']['#on_parents'] = &$form['allowed_values']['on']['#parents'];
$form['allowed_values']['#off_parents'] = &$form['allowed_values']['off']['#parents'];
break;
}
// Alter the description for allowed values depending on the widget type.
if ($instance['widget']['type'] == 'options_onoff') {
$form['allowed_values']['#description'] .= '' . t("For a 'single on/off checkbox' widget, define the 'off' value first, then the 'on' value in the Allowed values section. Note that the checkbox will be labeled with the label of the 'on' value.") . '
';
}
elseif ($instance['widget']['type'] == 'options_buttons') {
$form['allowed_values']['#description'] .= '' . t("The 'checkboxes/radio buttons' widget will display checkboxes if the Number of values option is greater than 1 for this field, otherwise radios will be displayed.") . '
';
}
$form['allowed_values']['#description'] .= '' . t('Allowed HTML tags in labels: @tags', array('@tags' => _field_filter_xss_display_allowed_tags())) . '
';
$form['allowed_values_function'] = array(
'#type' => 'value',
'#value' => $settings['allowed_values_function'],
);
$form['allowed_values_function_display'] = array(
'#type' => 'item',
'#title' => t('Allowed values list'),
'#markup' => t('The value of this field is being determined by the %function function and may not be changed.', array('%function' => $settings['allowed_values_function'])),
'#access' => !empty($settings['allowed_values_function']),
);
return $form;
}
/**
* Element validate callback; check that the entered values are valid.
*/
function list_allowed_values_setting_validate($element, &$form_state) {
$field = $element['#field'];
$has_data = $element['#field_has_data'];
$field_type = $field['type'];
$generate_keys = ($field_type == 'list_integer' || $field_type == 'list_float') && !$has_data;
$values = list_extract_allowed_values($element['#value'], $field['type'], $generate_keys);
if (!is_array($values)) {
form_error($element, t('Allowed values list: invalid input.'));
}
else {
// Check that keys are valid for the field type.
foreach ($values as $key => $value) {
if ($field_type == 'list_integer' && !preg_match('/^-?\d+$/', $key)) {
form_error($element, t('Allowed values list: keys must be integers.'));
break;
}
if ($field_type == 'list_float' && !is_numeric($key)) {
form_error($element, t('Allowed values list: each key must be a valid integer or decimal.'));
break;
}
elseif ($field_type == 'list_text' && drupal_strlen($key) > 255) {
form_error($element, t('Allowed values list: each key must be a string at most 255 characters long.'));
break;
}
}
// Prevent removing values currently in use.
if ($has_data) {
$lost_keys = array_diff(array_keys($field['settings']['allowed_values']), array_keys($values));
if (_list_values_in_use($field, $lost_keys)) {
form_error($element, t('Allowed values list: some values are being removed while currently in use.'));
}
}
form_set_value($element, $values, $form_state);
}
}
/**
* Form element #value_callback: assembles the allowed values for 'boolean' fields.
*/
function list_boolean_allowed_values_callback($element, $input, $form_state) {
$on = drupal_array_get_nested_value($form_state['input'], $element['#on_parents']);
$off = drupal_array_get_nested_value($form_state['input'], $element['#off_parents']);
return array($off, $on);
}
/**
* Implements hook_field_update_field().
*/
function list_field_update_field($field, $prior_field, $has_data) {
drupal_static_reset('list_allowed_values');
}
/**
* Returns the array of allowed values for a list field.
*
* The strings are not safe for output. Keys and values of the array should be
* sanitized through field_filter_xss() before being displayed.
*
* @param $field
* The field definition.
* @param $instance
* (optional) A field instance array. Defaults to NULL.
* @param $entity_type
* (optional) The type of entity; e.g. 'node' or 'user'. Defaults to NULL.
* @param $entity
* (optional) The entity object. Defaults to NULL.
*
* @return
* The array of allowed values. Keys of the array are the raw stored values
* (number or text), values of the array are the display labels.
*/
function list_allowed_values($field, $instance = NULL, $entity_type = NULL, $entity = NULL) {
$allowed_values = &drupal_static(__FUNCTION__, array());
if (!isset($allowed_values[$field['id']])) {
$function = $field['settings']['allowed_values_function'];
// If $cacheable is FALSE, then the allowed values are not statically
// cached. See list_test_dynamic_values_callback() for an example of
// generating dynamic and uncached values.
$cacheable = TRUE;
if (!empty($function) && function_exists($function)) {
$values = $function($field, $instance, $entity_type, $entity, $cacheable);
}
else {
$values = $field['settings']['allowed_values'];
}
if ($cacheable) {
$allowed_values[$field['id']] = $values;
}
else {
return $values;
}
}
return $allowed_values[$field['id']];
}
/**
* Parses a string of 'allowed values' into an array.
*
* @param $string
* The list of allowed values in string format described in
* list_allowed_values_string().
* @param $field_type
* The field type. Either 'list_number' or 'list_text'.
* @param $generate_keys
* Boolean value indicating whether to generate keys based on the position of
* the value if a key is not manually specified, and if the value cannot be
* used as a key. This should only be TRUE for fields of type 'list_number'.
*
* @return
* The array of extracted key/value pairs, or NULL if the string is invalid.
*
* @see list_allowed_values_string()
*/
function list_extract_allowed_values($string, $field_type, $generate_keys) {
$values = array();
$list = explode("\n", $string);
$list = array_map('trim', $list);
$list = array_filter($list, 'strlen');
$generated_keys = $explicit_keys = FALSE;
foreach ($list as $position => $text) {
$value = $key = FALSE;
// Check for an explicit key.
$matches = array();
if (preg_match('/(.*)\|(.*)/', $text, $matches)) {
$key = $matches[1];
$value = $matches[2];
$explicit_keys = TRUE;
}
// Otherwise see if we can use the value as the key. Detecting true integer
// strings takes a little trick.
elseif ($field_type == 'list_text'
|| ($field_type == 'list_float' && is_numeric($text))
|| ($field_type == 'list_integer' && is_numeric($text) && (float) $text == intval($text))) {
$key = $value = $text;
$explicit_keys = TRUE;
}
// Otherwise see if we can generate a key from the position.
elseif ($generate_keys) {
$key = (string) $position;
$value = $text;
$generated_keys = TRUE;
}
else {
return;
}
// Float keys are represented as strings and need to be disambiguated
// ('.5' is '0.5').
if ($field_type == 'list_float' && is_numeric($key)) {
$key = (string) (float) $key;
}
$values[$key] = $value;
}
// We generate keys only if the list contains no explicit key at all.
if ($explicit_keys && $generated_keys) {
return;
}
return $values;
}
/**
* Generates a string representation of an array of 'allowed values'.
*
* This string format is suitable for edition in a textarea.
*
* @param $values
* An array of values, where array keys are values and array values are
* labels.
*
* @return
* The string representation of the $values array:
* - Values are separated by a carriage return.
* - Each value is in the format "value|label" or "value".
*/
function list_allowed_values_string($values) {
$lines = array();
foreach ($values as $key => $value) {
$lines[] = "$key|$value";
}
return implode("\n", $lines);
}
/**
* Implements hook_field_update_forbid().
*/
function list_field_update_forbid($field, $prior_field, $has_data) {
if ($field['module'] == 'list' && $has_data) {
// Forbid any update that removes allowed values with actual data.
$lost_keys = array_diff(array_keys($prior_field['settings']['allowed_values']), array_keys($field['settings']['allowed_values']));
if (_list_values_in_use($field, $lost_keys)) {
throw new FieldUpdateForbiddenException(t('A list field (@field_name) with existing data cannot have its keys changed.', array('@field_name' => $field['field_name'])));
}
}
}
/**
* Checks if a list of values are being used in actual field values.
*/
function _list_values_in_use($field, $values) {
if ($values) {
$query = new EntityFieldQuery();
$found = $query
->fieldCondition($field['field_name'], 'value', $values)
->range(0, 1)
->execute();
return !empty($found);
}
return FALSE;
}
/**
* Implements hook_field_validate().
*
* Possible error codes:
* - 'list_illegal_value': The value is not part of the list of allowed values.
*/
function list_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
// Flatten the array before validating to account for optgroups.
$allowed_values = options_array_flatten(list_allowed_values($field, $instance, $entity_type, $entity));
foreach ($items as $delta => $item) {
if (!empty($item['value'])) {
if (!empty($allowed_values) && !isset($allowed_values[$item['value']])) {
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => 'list_illegal_value',
'message' => t('%name: illegal value.', array('%name' => $instance['label'])),
);
}
}
}
}
/**
* Implements hook_field_is_empty().
*/
function list_field_is_empty($item, $field) {
if (empty($item['value']) && (string) $item['value'] !== '0') {
return TRUE;
}
return FALSE;
}
/**
* Implements hook_field_widget_info_alter().
*
* The List module does not implement widgets of its own, but reuses the
* widgets defined in options.module.
*
* @see list_options_list()
*/
function list_field_widget_info_alter(&$info) {
$widgets = array(
'options_select' => array('list_integer', 'list_float', 'list_text'),
'options_buttons' => array('list_integer', 'list_float', 'list_text', 'list_boolean'),
'options_onoff' => array('list_boolean'),
);
foreach ($widgets as $widget => $field_types) {
$info[$widget]['field types'] = array_merge($info[$widget]['field types'], $field_types);
}
}
/**
* Implements hook_options_list().
*/
function list_options_list($field, $instance, $entity_type, $entity) {
return list_allowed_values($field, $instance, $entity_type, $entity);
}
/**
* Implements hook_field_formatter_info().
*/
function list_field_formatter_info() {
return array(
'list_default' => array(
'label' => t('Default'),
'field types' => array('list_integer', 'list_float', 'list_text', 'list_boolean'),
),
'list_key' => array(
'label' => t('Key'),
'field types' => array('list_integer', 'list_float', 'list_text', 'list_boolean'),
),
);
}
/**
* Implements hook_field_formatter_view().
*/
function list_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$element = array();
switch ($display['type']) {
case 'list_default':
$allowed_values = list_allowed_values($field, $instance, $entity_type, $entity);
foreach ($items as $delta => $item) {
if (isset($allowed_values[$item['value']])) {
$output = field_filter_xss($allowed_values[$item['value']]);
}
else {
// If no match was found in allowed values, fall back to the key.
$output = field_filter_xss($item['value']);
}
$element[$delta] = array('#markup' => $output);
}
break;
case 'list_key':
foreach ($items as $delta => $item) {
$element[$delta] = array('#markup' => field_filter_xss($item['value']));
}
break;
}
return $element;
}
'List field',
'description' => 'Test the List field type.',
'group' => 'Field types',
);
}
function setUp() {
parent::setUp('field_test');
$this->field_name = 'test_list';
$this->field = array(
'field_name' => $this->field_name,
'type' => 'list_integer',
'cardinality' => 1,
'settings' => array(
'allowed_values' => array(1 => 'One', 2 => 'Two', 3 => 'Three'),
),
);
$this->field = field_create_field($this->field);
$this->instance = array(
'field_name' => $this->field_name,
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'widget' => array(
'type' => 'options_buttons',
),
);
$this->instance = field_create_instance($this->instance);
}
/**
* Test that allowed values can be updated.
*/
function testUpdateAllowedValues() {
$langcode = LANGUAGE_NONE;
// All three options appear.
$entity = field_test_create_stub_entity();
$form = drupal_get_form('field_test_entity_form', $entity);
$this->assertTrue(!empty($form[$this->field_name][$langcode][1]), 'Option 1 exists');
$this->assertTrue(!empty($form[$this->field_name][$langcode][2]), 'Option 2 exists');
$this->assertTrue(!empty($form[$this->field_name][$langcode][3]), 'Option 3 exists');
// Use one of the values in an actual entity, and check that this value
// cannot be removed from the list.
$entity = field_test_create_stub_entity();
$entity->{$this->field_name}[$langcode][0] = array('value' => 1);
field_test_entity_save($entity);
$this->field['settings']['allowed_values'] = array(2 => 'Two');
try {
field_update_field($this->field);
$this->fail(t('Cannot update a list field to not include keys with existing data.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot update a list field to not include keys with existing data.'));
}
// Empty the value, so that we can actually remove the option.
$entity->{$this->field_name}[$langcode] = array();
field_test_entity_save($entity);
// Removed options do not appear.
$this->field['settings']['allowed_values'] = array(2 => 'Two');
field_update_field($this->field);
$entity = field_test_create_stub_entity();
$form = drupal_get_form('field_test_entity_form', $entity);
$this->assertTrue(empty($form[$this->field_name][$langcode][1]), 'Option 1 does not exist');
$this->assertTrue(!empty($form[$this->field_name][$langcode][2]), 'Option 2 exists');
$this->assertTrue(empty($form[$this->field_name][$langcode][3]), 'Option 3 does not exist');
// Completely new options appear.
$this->field['settings']['allowed_values'] = array(10 => 'Update', 20 => 'Twenty');
field_update_field($this->field);
$form = drupal_get_form('field_test_entity_form', $entity);
$this->assertTrue(empty($form[$this->field_name][$langcode][1]), 'Option 1 does not exist');
$this->assertTrue(empty($form[$this->field_name][$langcode][2]), 'Option 2 does not exist');
$this->assertTrue(empty($form[$this->field_name][$langcode][3]), 'Option 3 does not exist');
$this->assertTrue(!empty($form[$this->field_name][$langcode][10]), 'Option 10 exists');
$this->assertTrue(!empty($form[$this->field_name][$langcode][20]), 'Option 20 exists');
// Options are reset when a new field with the same name is created.
field_delete_field($this->field_name);
unset($this->field['id']);
$this->field['settings']['allowed_values'] = array(1 => 'One', 2 => 'Two', 3 => 'Three');
$this->field = field_create_field($this->field);
$this->instance = array(
'field_name' => $this->field_name,
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'widget' => array(
'type' => 'options_buttons',
),
);
$this->instance = field_create_instance($this->instance);
$entity = field_test_create_stub_entity();
$form = drupal_get_form('field_test_entity_form', $entity);
$this->assertTrue(!empty($form[$this->field_name][$langcode][1]), 'Option 1 exists');
$this->assertTrue(!empty($form[$this->field_name][$langcode][2]), 'Option 2 exists');
$this->assertTrue(!empty($form[$this->field_name][$langcode][3]), 'Option 3 exists');
}
}
/**
* Sets up a List field for testing allowed values functions.
*/
class ListDynamicValuesTestCase extends FieldTestCase {
protected $field;
protected $field_name;
protected $instance;
protected $test;
protected $entity;
function setUp() {
parent::setUp(array('list', 'field_test', 'list_test'));
$this->field_name = 'test_list';
$this->field = array(
'field_name' => $this->field_name,
'type' => 'list_text',
'cardinality' => 1,
'settings' => array(
'allowed_values_function' => 'list_test_dynamic_values_callback',
),
);
$this->field = field_create_field($this->field);
$this->instance = array(
'field_name' => $this->field_name,
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'required' => TRUE,
'widget' => array(
'type' => 'options_select',
),
);
$this->instance = field_create_instance($this->instance);
$this->test = array(
'id' => mt_rand(1, 10),
// Make sure this does not equal the ID so that
// list_test_dynamic_values_callback() always returns 4 values.
'vid' => mt_rand(20, 30),
'bundle' => 'test_bundle',
'label' => $this->randomName(),
);
$this->entity = call_user_func_array('field_test_create_stub_entity', $this->test);
}
}
/**
* Tests the List field allowed values function.
*/
class ListDynamicValuesValidationTestCase extends ListDynamicValuesTestCase {
public static function getInfo() {
return array(
'name' => 'List field dynamic values',
'description' => 'Test the List field allowed values function.',
'group' => 'Field types',
);
}
/**
* Test that allowed values function gets the entity.
*/
function testDynamicAllowedValues() {
// Verify that the test passes against every value we had.
foreach ($this->test as $key => $value) {
$this->entity->test_list[LANGUAGE_NONE][0]['value'] = $value;
try {
field_attach_validate('test_entity', $this->entity);
$this->pass("$key should pass");
}
catch (FieldValidationException $e) {
// This will display as an exception, no need for a separate error.
throw($e);
}
}
// Now verify that the test does not pass against anything else.
foreach ($this->test as $key => $value) {
$this->entity->test_list[LANGUAGE_NONE][0]['value'] = is_numeric($value) ? (100 - $value) : ('X' . $value);
$pass = FALSE;
try {
field_attach_validate('test_entity', $this->entity);
}
catch (FieldValidationException $e) {
$pass = TRUE;
}
$this->assertTrue($pass, $key . ' should not pass');
}
}
}
/**
* List module UI tests.
*/
class ListFieldUITestCase extends FieldTestCase {
protected $type;
protected $hyphen_type;
protected $field_name;
protected $admin_path;
public static function getInfo() {
return array(
'name' => 'List field UI',
'description' => 'Test the List field UI functionality.',
'group' => 'Field types',
);
}
function setUp() {
parent::setUp('field_test', 'field_ui');
// Create test user.
$admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer taxonomy', 'administer fields'));
$this->drupalLogin($admin_user);
// Create content type, with underscores.
$type_name = 'test_' . strtolower($this->randomName());
$type = $this->drupalCreateContentType(array('name' => $type_name, 'type' => $type_name));
$this->type = $type->type;
// Store a valid URL name, with hyphens instead of underscores.
$this->hyphen_type = str_replace('_', '-', $this->type);
}
/**
* List (integer) : test 'allowed values' input.
*/
function testListAllowedValuesInteger() {
$this->field_name = 'field_list_integer';
$this->createListField('list_integer');
// Flat list of textual values.
$string = "Zero\nOne";
$array = array('0' => 'Zero', '1' => 'One');
$this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are accepted.');
// Explicit integer keys.
$string = "0|Zero\n2|Two";
$array = array('0' => 'Zero', '2' => 'Two');
$this->assertAllowedValuesInput($string, $array, 'Integer keys are accepted.');
// Check that values can be added and removed.
$string = "0|Zero\n1|One";
$array = array('0' => 'Zero', '1' => 'One');
$this->assertAllowedValuesInput($string, $array, 'Values can be added and removed.');
// Non-integer keys.
$this->assertAllowedValuesInput("1.1|One", 'keys must be integers', 'Non integer keys are rejected.');
$this->assertAllowedValuesInput("abc|abc", 'keys must be integers', 'Non integer keys are rejected.');
// Mixed list of keyed and unkeyed values.
$this->assertAllowedValuesInput("Zero\n1|One", 'invalid input', 'Mixed lists are rejected.');
// Create a node with actual data for the field.
$settings = array(
'type' => $this->type,
$this->field_name => array(LANGUAGE_NONE => array(array('value' => 1))),
);
$node = $this->drupalCreateNode($settings);
// Check that a flat list of values is rejected once the field has data.
$this->assertAllowedValuesInput( "Zero\nOne", 'invalid input', 'Unkeyed lists are rejected once the field has data.');
// Check that values can be added but values in use cannot be removed.
$string = "0|Zero\n1|One\n2|Two";
$array = array('0' => 'Zero', '1' => 'One', '2' => 'Two');
$this->assertAllowedValuesInput($string, $array, 'Values can be added.');
$string = "0|Zero\n1|One";
$array = array('0' => 'Zero', '1' => 'One');
$this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
$this->assertAllowedValuesInput("0|Zero", 'some values are being removed while currently in use', 'Values in use cannot be removed.');
// Delete the node, remove the value.
node_delete($node->nid);
$string = "0|Zero";
$array = array('0' => 'Zero');
$this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
}
/**
* List (float) : test 'allowed values' input.
*/
function testListAllowedValuesFloat() {
$this->field_name = 'field_list_float';
$this->createListField('list_float');
// Flat list of textual values.
$string = "Zero\nOne";
$array = array('0' => 'Zero', '1' => 'One');
$this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are accepted.');
// Explicit numeric keys.
$string = "0|Zero\n.5|Point five";
$array = array('0' => 'Zero', '0.5' => 'Point five');
$this->assertAllowedValuesInput($string, $array, 'Integer keys are accepted.');
// Check that values can be added and removed.
$string = "0|Zero\n.5|Point five\n1.0|One";
$array = array('0' => 'Zero', '0.5' => 'Point five', '1' => 'One');
$this->assertAllowedValuesInput($string, $array, 'Values can be added and removed.');
// Non-numeric keys.
$this->assertAllowedValuesInput("abc|abc\n", 'each key must be a valid integer or decimal', 'Non numeric keys are rejected.');
// Mixed list of keyed and unkeyed values.
$this->assertAllowedValuesInput("Zero\n1|One\n", 'invalid input', 'Mixed lists are rejected.');
// Create a node with actual data for the field.
$settings = array(
'type' => $this->type,
$this->field_name => array(LANGUAGE_NONE => array(array('value' => .5))),
);
$node = $this->drupalCreateNode($settings);
// Check that a flat list of values is rejected once the field has data.
$this->assertAllowedValuesInput("Zero\nOne", 'invalid input', 'Unkeyed lists are rejected once the field has data.');
// Check that values can be added but values in use cannot be removed.
$string = "0|Zero\n.5|Point five\n2|Two";
$array = array('0' => 'Zero', '0.5' => 'Point five', '2' => 'Two');
$this->assertAllowedValuesInput($string, $array, 'Values can be added.');
$string = "0|Zero\n.5|Point five";
$array = array('0' => 'Zero', '0.5' => 'Point five');
$this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
$this->assertAllowedValuesInput("0|Zero", 'some values are being removed while currently in use', 'Values in use cannot be removed.');
// Delete the node, remove the value.
node_delete($node->nid);
$string = "0|Zero";
$array = array('0' => 'Zero');
$this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
}
/**
* List (text) : test 'allowed values' input.
*/
function testListAllowedValuesText() {
$this->field_name = 'field_list_text';
$this->createListField('list_text');
// Flat list of textual values.
$string = "Zero\nOne";
$array = array('Zero' => 'Zero', 'One' => 'One');
$this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are accepted.');
// Explicit keys.
$string = "zero|Zero\none|One";
$array = array('zero' => 'Zero', 'one' => 'One');
$this->assertAllowedValuesInput($string, $array, 'Explicit keys are accepted.');
// Check that values can be added and removed.
$string = "zero|Zero\ntwo|Two";
$array = array('zero' => 'Zero', 'two' => 'Two');
$this->assertAllowedValuesInput($string, $array, 'Values can be added and removed.');
// Mixed list of keyed and unkeyed values.
$string = "zero|Zero\nOne\n";
$array = array('zero' => 'Zero', 'One' => 'One');
$this->assertAllowedValuesInput($string, $array, 'Mixed lists are accepted.');
// Overly long keys.
$this->assertAllowedValuesInput("zero|Zero\n" . $this->randomName(256) . "|One", 'each key must be a string at most 255 characters long', 'Overly long keys are rejected.');
// Create a node with actual data for the field.
$settings = array(
'type' => $this->type,
$this->field_name => array(LANGUAGE_NONE => array(array('value' => 'One'))),
);
$node = $this->drupalCreateNode($settings);
// Check that flat lists of values are still accepted once the field has
// data.
$string = "Zero\nOne";
$array = array('Zero' => 'Zero', 'One' => 'One');
$this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are still accepted once the field has data.');
// Check that values can be added but values in use cannot be removed.
$string = "Zero\nOne\nTwo";
$array = array('Zero' => 'Zero', 'One' => 'One', 'Two' => 'Two');
$this->assertAllowedValuesInput($string, $array, 'Values can be added.');
$string = "Zero\nOne";
$array = array('Zero' => 'Zero', 'One' => 'One');
$this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
$this->assertAllowedValuesInput("Zero", 'some values are being removed while currently in use', 'Values in use cannot be removed.');
// Delete the node, remove the value.
node_delete($node->nid);
$string = "Zero";
$array = array('Zero' => 'Zero');
$this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
}
/**
* List (boolen) : test 'On/Off' values input.
*/
function testListAllowedValuesBoolean() {
$this->field_name = 'field_list_boolean';
$this->createListField('list_boolean');
// Check that the separate 'On' and 'Off' form fields work.
$on = $this->randomName();
$off = $this->randomName();
$allowed_values = array(1 => $on, 0 => $off);
$edit = array(
'on' => $on,
'off' => $off,
);
$this->drupalPost($this->admin_path, $edit, t('Save settings'));
$this->assertText("Saved field_list_boolean configuration.", "The 'On' and 'Off' form fields work for boolean fields.");
// Test the allowed_values on the field settings form.
$this->drupalGet($this->admin_path);
$this->assertFieldByName('on', $on, "The 'On' value is stored correctly.");
$this->assertFieldByName('off', $off, "The 'Off' value is stored correctly.");
$field = field_info_field($this->field_name);
$this->assertEqual($field['settings']['allowed_values'], $allowed_values, 'The allowed value is correct');
$this->assertFalse(isset($field['settings']['on']), 'The on value is not saved into settings');
$this->assertFalse(isset($field['settings']['off']), 'The off value is not saved into settings');
}
/**
* List (text) : test 'allowed values function' input.
*/
function testDynamicListAllowedValuesText() {
$this->field_name = 'field_list_text';
$this->createListField('list_text', array(
'allowed_values_function' => 'list_test_dynamic_values_callback',
'allowed_values' => '',
));
$this->drupalGet($this->admin_path);
}
/**
* Helper function to create list field of a given type.
*
* @param string $type
* 'list_integer', 'list_float', 'list_text' or 'list_boolean'
* @param array $settings
*
* @throws \FieldException
*/
protected function createListField($type, $settings = array()) {
// Create a test field and instance.
$field = array(
'field_name' => $this->field_name,
'type' => $type,
);
if (!empty($settings)) {
$field['settings'] = $settings;
}
field_create_field($field);
$instance = array(
'field_name' => $this->field_name,
'entity_type' => 'node',
'bundle' => $this->type,
);
field_create_instance($instance);
$this->admin_path = 'admin/structure/types/manage/' . $this->hyphen_type . '/fields/' . $this->field_name;
}
/**
* Tests a string input for the 'allowed values' form element.
*
* @param $input_string
* The input string, in the pipe-linefeed format expected by the form
* element.
* @param $result
* Either an expected resulting array in
* $field['settings']['allowed_values'], or an expected error message.
* @param $message
* Message to display.
*/
function assertAllowedValuesInput($input_string, $result, $message) {
$edit = array('field[settings][allowed_values]' => $input_string);
$this->drupalPost($this->admin_path, $edit, t('Save settings'));
if (is_string($result)) {
$this->assertText($result, $message);
}
else {
field_info_cache_clear();
$field = field_info_field($this->field_name);
$this->assertIdentical($field['settings']['allowed_values'], $result, $message);
}
}
}
name = "List test"
description = "Support module for the List module tests."
core = 7.x
package = Testing
version = VERSION
hidden = TRUE
; Information added by Drupal.org packaging script on 2023-03-15
version = "7.95"
project = "drupal"
datestamp = "1678902530"
array(
0 => 'Zero',
),
1 => 'One',
'Group 2' => array(
2 => 'Some & unescaped markup ',
),
);
return $values;
}
/**
* An entity-bound allowed values callback.
*/
function list_test_dynamic_values_callback($field, $instance, $entity_type, $entity, &$cacheable) {
$cacheable = FALSE;
// We need the values of the entity as keys.
return drupal_map_assoc(array_merge(array($entity->ftlabel), entity_extract_ids($entity_type, $entity)));
}
name = Number
description = Defines numeric field types.
package = Core
version = VERSION
core = 7.x
dependencies[] = field
files[] = number.test
; Information added by Drupal.org packaging script on 2023-03-15
version = "7.95"
project = "drupal"
datestamp = "1678902530"
array(
'type' => 'int',
'not null' => FALSE
),
);
break;
case 'number_float' :
$columns = array(
'value' => array(
'type' => 'float',
'not null' => FALSE
),
);
break;
case 'number_decimal' :
$columns = array(
'value' => array(
'type' => 'numeric',
'precision' => $field['settings']['precision'],
'scale' => $field['settings']['scale'],
'not null' => FALSE
),
);
break;
}
return array(
'columns' => $columns,
);
}
' . t('About') . '';
$output .= '' . t('The Number module defines various numeric field types for the Field module. Numbers can be in integer, decimal, or floating-point form, and they can be formatted when displayed. Number fields can be limited to a specific set of input values or to a range of values. See the Field module help page for more information about fields.', array('@field-help' => url('admin/help/field'))) . '
';
return $output;
}
}
/**
* Implements hook_field_info().
*/
function number_field_info() {
return array(
'number_integer' => array(
'label' => t('Integer'),
'description' => t('This field stores a number in the database as an integer.'),
'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''),
'default_widget' => 'number',
'default_formatter' => 'number_integer',
),
'number_decimal' => array(
'label' => t('Decimal'),
'description' => t('This field stores a number in the database in a fixed decimal format.'),
'settings' => array('precision' => 10, 'scale' => 2, 'decimal_separator' => '.'),
'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''),
'default_widget' => 'number',
'default_formatter' => 'number_decimal',
),
'number_float' => array(
'label' => t('Float'),
'description' => t('This field stores a number in the database in a floating point format.'),
'settings' => array('decimal_separator' => '.'),
'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''),
'default_widget' => 'number',
'default_formatter' => 'number_decimal',
),
);
}
/**
* Implements hook_field_settings_form().
*/
function number_field_settings_form($field, $instance, $has_data) {
$settings = $field['settings'];
$form = array();
if ($field['type'] == 'number_decimal') {
$form['precision'] = array(
'#type' => 'select',
'#title' => t('Precision'),
'#options' => drupal_map_assoc(range(10, 32)),
'#default_value' => $settings['precision'],
'#description' => t('The total number of digits to store in the database, including those to the right of the decimal.'),
'#disabled' => $has_data,
);
$form['scale'] = array(
'#type' => 'select',
'#title' => t('Scale'),
'#options' => drupal_map_assoc(range(0, 10)),
'#default_value' => $settings['scale'],
'#description' => t('The number of digits to the right of the decimal.'),
'#disabled' => $has_data,
);
}
if ($field['type'] == 'number_decimal' || $field['type'] == 'number_float') {
$form['decimal_separator'] = array(
'#type' => 'select',
'#title' => t('Decimal marker'),
'#options' => array('.' => t('Decimal point'), ',' => t('Comma')),
'#default_value' => $settings['decimal_separator'],
'#description' => t('The character users will input to mark the decimal point in forms.'),
);
}
return $form;
}
/**
* Implements hook_field_instance_settings_form().
*/
function number_field_instance_settings_form($field, $instance) {
$settings = $instance['settings'];
$form['min'] = array(
'#type' => 'textfield',
'#title' => t('Minimum'),
'#default_value' => $settings['min'],
'#description' => t('The minimum value that should be allowed in this field. Leave blank for no minimum.'),
'#element_validate' => array('element_validate_number'),
);
$form['max'] = array(
'#type' => 'textfield',
'#title' => t('Maximum'),
'#default_value' => $settings['max'],
'#description' => t('The maximum value that should be allowed in this field. Leave blank for no maximum.'),
'#element_validate' => array('element_validate_number'),
);
$form['prefix'] = array(
'#type' => 'textfield',
'#title' => t('Prefix'),
'#default_value' => $settings['prefix'],
'#size' => 60,
'#description' => t("Define a string that should be prefixed to the value, like '$ ' or '€ '. Leave blank for none. Separate singular and plural values with a pipe ('pound|pounds')."),
);
$form['suffix'] = array(
'#type' => 'textfield',
'#title' => t('Suffix'),
'#default_value' => $settings['suffix'],
'#size' => 60,
'#description' => t("Define a string that should be suffixed to the value, like ' m', ' kb/s'. Leave blank for none. Separate singular and plural values with a pipe ('pound|pounds')."),
);
return $form;
}
/**
* Implements hook_field_validate().
*
* Possible error codes:
* - 'number_min': The value is less than the allowed minimum value.
* - 'number_max': The value is greater than the allowed maximum value.
*/
function number_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
foreach ($items as $delta => $item) {
if ($item['value'] != '') {
if (is_numeric($instance['settings']['min']) && $item['value'] < $instance['settings']['min']) {
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => 'number_min',
'message' => t('%name: the value may be no less than %min.', array('%name' => $instance['label'], '%min' => $instance['settings']['min'])),
);
}
if (is_numeric($instance['settings']['max']) && $item['value'] > $instance['settings']['max']) {
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => 'number_max',
'message' => t('%name: the value may be no greater than %max.', array('%name' => $instance['label'], '%max' => $instance['settings']['max'])),
);
}
}
}
}
/**
* Implements hook_field_presave().
*/
function number_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
if ($field['type'] == 'number_decimal') {
// Let PHP round the value to ensure consistent behavior across storage
// backends.
foreach ($items as $delta => $item) {
if (isset($item['value'])) {
$items[$delta]['value'] = round($item['value'], $field['settings']['scale']);
}
}
}
if ($field['type'] == 'number_float') {
// Remove the decimal point from float values with decimal
// point but no decimal numbers.
foreach ($items as $delta => $item) {
if (isset($item['value'])) {
$items[$delta]['value'] = floatval($item['value']);
}
}
}
}
/**
* Implements hook_field_is_empty().
*/
function number_field_is_empty($item, $field) {
if (empty($item['value']) && (string) $item['value'] !== '0') {
return TRUE;
}
return FALSE;
}
/**
* Implements hook_field_formatter_info().
*/
function number_field_formatter_info() {
return array(
// The 'Default' formatter is different for integer fields on the one hand,
// and for decimal and float fields on the other hand, in order to be able
// to use different default values for the settings.
'number_integer' => array(
'label' => t('Default'),
'field types' => array('number_integer'),
'settings' => array(
'thousand_separator' => '',
// The 'decimal_separator' and 'scale' settings are not configurable
// through the UI, and will therefore keep their default values. They
// are only present so that the 'number_integer' and 'number_decimal'
// formatters can use the same code.
'decimal_separator' => '.',
'scale' => 0,
'prefix_suffix' => TRUE,
),
),
'number_decimal' => array(
'label' => t('Default'),
'field types' => array('number_decimal', 'number_float'),
'settings' => array(
'thousand_separator' => '',
'decimal_separator' => '.',
'scale' => 2,
'prefix_suffix' => TRUE,
),
),
'number_unformatted' => array(
'label' => t('Unformatted'),
'field types' => array('number_integer', 'number_decimal', 'number_float'),
),
);
}
/**
* Implements hook_field_formatter_settings_form().
*/
function number_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$element = array();
if ($display['type'] == 'number_decimal' || $display['type'] == 'number_integer') {
$options = array(
'' => t(''),
'.' => t('Decimal point'),
',' => t('Comma'),
' ' => t('Space'),
);
$element['thousand_separator'] = array(
'#type' => 'select',
'#title' => t('Thousand marker'),
'#options' => $options,
'#default_value' => $settings['thousand_separator'],
);
if ($display['type'] == 'number_decimal') {
$element['decimal_separator'] = array(
'#type' => 'select',
'#title' => t('Decimal marker'),
'#options' => array('.' => t('Decimal point'), ',' => t('Comma')),
'#default_value' => $settings['decimal_separator'],
);
$element['scale'] = array(
'#type' => 'select',
'#title' => t('Scale'),
'#options' => drupal_map_assoc(range(0, 10)),
'#default_value' => $settings['scale'],
'#description' => t('The number of digits to the right of the decimal.'),
);
}
$element['prefix_suffix'] = array(
'#type' => 'checkbox',
'#title' => t('Display prefix and suffix.'),
'#default_value' => $settings['prefix_suffix'],
);
}
return $element;
}
/**
* Implements hook_field_formatter_settings_summary().
*/
function number_field_formatter_settings_summary($field, $instance, $view_mode) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$summary = array();
if ($display['type'] == 'number_decimal' || $display['type'] == 'number_integer') {
$summary[] = number_format(1234.1234567890, $settings['scale'], $settings['decimal_separator'], $settings['thousand_separator']);
if ($settings['prefix_suffix']) {
$summary[] = t('Display with prefix and suffix.');
}
}
return implode(' ', $summary);
}
/**
* Implements hook_field_formatter_view().
*/
function number_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$element = array();
$settings = $display['settings'];
switch ($display['type']) {
case 'number_integer':
case 'number_decimal':
foreach ($items as $delta => $item) {
$output = number_format($item['value'], $settings['scale'], $settings['decimal_separator'], $settings['thousand_separator']);
if ($settings['prefix_suffix']) {
$prefixes = isset($instance['settings']['prefix']) ? array_map('field_filter_xss', explode('|', $instance['settings']['prefix'])) : array('');
$suffixes = isset($instance['settings']['suffix']) ? array_map('field_filter_xss', explode('|', $instance['settings']['suffix'])) : array('');
$prefix = (count($prefixes) > 1) ? format_plural($item['value'], $prefixes[0], $prefixes[1]) : $prefixes[0];
$suffix = (count($suffixes) > 1) ? format_plural($item['value'], $suffixes[0], $suffixes[1]) : $suffixes[0];
$output = $prefix . $output . $suffix;
}
$element[$delta] = array('#markup' => $output);
}
break;
case 'number_unformatted':
foreach ($items as $delta => $item) {
$element[$delta] = array('#markup' => $item['value']);
}
break;
}
return $element;
}
/**
* Implements hook_field_widget_info().
*/
function number_field_widget_info() {
return array(
'number' => array(
'label' => t('Text field'),
'field types' => array('number_integer', 'number_decimal', 'number_float'),
),
);
}
/**
* Implements hook_field_widget_form().
*/
function number_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
$value = isset($items[$delta]['value']) ? $items[$delta]['value'] : '';
// Substitute the decimal separator.
if ($field['type'] == 'number_decimal' || $field['type'] == 'number_float') {
$value = strtr($value, '.', $field['settings']['decimal_separator']);
}
$element += array(
'#type' => 'textfield',
'#default_value' => $value,
// Allow a slightly larger size that the field length to allow for some
// configurations where all characters won't fit in input field.
'#size' => $field['type'] == 'number_decimal' ? $field['settings']['precision'] + 4 : 12,
// Allow two extra characters for signed values and decimal separator.
'#maxlength' => $field['type'] == 'number_decimal' ? $field['settings']['precision'] + 2 : 10,
// Extract the number type from the field type name for easier validation.
'#number_type' => str_replace('number_', '', $field['type']),
);
// Add prefix and suffix.
if (!empty($instance['settings']['prefix'])) {
$prefixes = explode('|', $instance['settings']['prefix']);
$element['#field_prefix'] = field_filter_xss(array_pop($prefixes));
}
if (!empty($instance['settings']['suffix'])) {
$suffixes = explode('|', $instance['settings']['suffix']);
$element['#field_suffix'] = field_filter_xss(array_pop($suffixes));
}
$element['#element_validate'][] = 'number_field_widget_validate';
return array('value' => $element);
}
/**
* FAPI validation of an individual number element.
*/
function number_field_widget_validate($element, &$form_state) {
$field = field_widget_field($element, $form_state);
$instance = field_widget_instance($element, $form_state);
$type = $element['#number_type'];
$value = $element['#value'];
// Reject invalid characters.
if (!empty($value)) {
switch ($type) {
case 'float':
case 'decimal':
$regexp = '@([^-0-9\\' . $field['settings']['decimal_separator'] . '])|(.-)@';
$message = t('Only numbers and the decimal separator (@separator) allowed in %field.', array('%field' => $instance['label'], '@separator' => $field['settings']['decimal_separator']));
break;
case 'integer':
$regexp = '@([^-0-9])|(.-)@';
$message = t('Only numbers are allowed in %field.', array('%field' => $instance['label']));
break;
}
if ($value != preg_replace($regexp, '', $value)) {
form_error($element, $message);
}
else {
if ($type == 'decimal' || $type == 'float') {
// Verify that only one decimal separator exists in the field.
if (substr_count($value, $field['settings']['decimal_separator']) > 1) {
$message = t('%field: There should only be one decimal separator (@separator).',
array(
'%field' => t($instance['label']),
'@separator' => $field['settings']['decimal_separator'],
)
);
form_error($element, $message);
}
else {
// Substitute the decimal separator; things should be fine.
$value = strtr($value, $field['settings']['decimal_separator'], '.');
}
}
form_set_value($element, $value, $form_state);
}
}
}
/**
* Implements hook_field_widget_error().
*/
function number_field_widget_error($element, $error, $form, &$form_state) {
form_error($element['value'], $error['message']);
}
'Number field',
'description' => 'Test the creation of number fields.',
'group' => 'Field types'
);
}
function setUp() {
parent::setUp('field_test');
$this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content', 'administer content types', 'administer fields'));
$this->drupalLogin($this->web_user);
}
/**
* Test number_decimal field.
*/
function testNumberDecimalField() {
// Create a field with settings to validate.
$this->field = array(
'field_name' => drupal_strtolower($this->randomName()),
'type' => 'number_decimal',
'settings' => array(
'precision' => 8, 'scale' => 4, 'decimal_separator' => '.',
)
);
field_create_field($this->field);
$this->instance = array(
'field_name' => $this->field['field_name'],
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'widget' => array(
'type' => 'number',
),
'display' => array(
'default' => array(
'type' => 'number_decimal',
),
),
);
field_create_instance($this->instance);
// Display creation form.
$this->drupalGet('test-entity/add/test-bundle');
$langcode = LANGUAGE_NONE;
$this->assertFieldByName("{$this->field['field_name']}[$langcode][0][value]", '', 'Widget is displayed');
// Submit a signed decimal value within the allowed precision and scale.
$value = '-1234.5678';
$edit = array(
"{$this->field['field_name']}[$langcode][0][value]" => $value,
);
$this->drupalPost(NULL, $edit, t('Save'));
preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
$id = $match[1];
$this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created');
$this->assertRaw($value, 'Value is displayed.');
// Try to create entries with more than one decimal separator; assert fail.
$wrong_entries = array(
'3.14.159',
'0..45469',
'..4589',
'6.459.52',
'6.3..25',
);
foreach ($wrong_entries as $wrong_entry) {
$this->drupalGet('test-entity/add/test-bundle');
$edit = array(
"{$this->field['field_name']}[$langcode][0][value]" => $wrong_entry,
);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertText(
t('There should only be one decimal separator (@separator)',
array('@separator' => $this->field['settings']['decimal_separator'])),
'Correctly failed to save decimal value with more than one decimal point.'
);
}
// Try to create entries with minus sign not in the first position.
$wrong_entries = array(
'3-3',
'4-',
'1.3-',
'1.2-4',
'-10-10',
);
foreach ($wrong_entries as $wrong_entry) {
$this->drupalGet('test-entity/add/test-bundle');
$edit = array(
"{$this->field['field_name']}[$langcode][0][value]" => $wrong_entry,
);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertText(
t('Only numbers and the decimal separator (@separator) allowed in ',
array('@separator' => $this->field['settings']['decimal_separator'])),
'Correctly failed to save decimal value with minus sign in the wrong position.'
);
}
}
/**
* Test number_integer field.
*/
function testNumberIntegerField() {
// Display the "Add content type" form.
$this->drupalGet('admin/structure/types/add');
// Add a content type.
$name = $this->randomName();
$type = drupal_strtolower($name);
$edit = array('name' => $name, 'type' => $type);
$this->drupalPost(NULL, $edit, t('Save and add fields'));
// Add an integer field to the newly-created type.
$label = $this->randomName();
$field_name = drupal_strtolower($label);
$edit = array(
'fields[_add_new_field][label]'=> $label,
'fields[_add_new_field][field_name]' => $field_name,
'fields[_add_new_field][type]' => 'number_integer',
'fields[_add_new_field][widget_type]' => 'number',
);
$this->drupalPost(NULL, $edit, t('Save'));
// Set the formatter to "number_integer" and to "unformatted", and just
// check that the settings summary does not generate warnings.
$this->drupalGet("admin/structure/types/manage/$type/display");
$edit = array(
"fields[field_$field_name][type]" => 'number_integer',
);
$this->drupalPost(NULL, $edit, t('Save'));
$edit = array(
"fields[field_$field_name][type]" => 'number_unformatted',
);
$this->drupalPost(NULL, $edit, t('Save'));
}
/**
* Test number_float field.
*/
function testNumberFloatField() {
$this->field = array(
'field_name' => drupal_strtolower($this->randomName()),
'type' => 'number_float',
'settings' => array(
'precision' => 8, 'scale' => 4, 'decimal_separator' => '.',
)
);
field_create_field($this->field);
$this->instance = array(
'field_name' => $this->field['field_name'],
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'widget' => array(
'type' => 'number',
),
'display' => array(
'default' => array(
'type' => 'number_decimal',
),
),
);
field_create_instance($this->instance);
$langcode = LANGUAGE_NONE;
$value = array(
'9.' => '9',
'.' => '0',
'123.55' => '123.55',
'.55' => '0.55',
'-0.55' => '-0.55',
);
foreach($value as $key => $value) {
$edit = array(
"{$this->field['field_name']}[$langcode][0][value]" => $key,
);
$this->drupalPost('test-entity/add/test-bundle', $edit, t('Save'));
$this->assertNoText("PDOException");
$this->assertRaw($value, 'Correct value is displayed.');
}
}
}
t('Zero'),
1 => t('One'),
2 => t('Two'),
3 => t('Three'),
);
// Sample structure with groups. Only one level of nesting is allowed. This
// is only supported by the 'options_select' widget. Other widgets will
// flatten the array.
$options = array(
t('First group') => array(
0 => t('Zero'),
),
t('Second group') => array(
1 => t('One'),
2 => t('Two'),
),
3 => t('Three'),
);
// In actual implementations, the array of options will most probably depend
// on properties of the field. Example from taxonomy.module:
$options = array();
foreach ($field['settings']['allowed_values'] as $tree) {
$terms = taxonomy_get_tree($tree['vid'], $tree['parent']);
if ($terms) {
foreach ($terms as $term) {
$options[$term->tid] = str_repeat('-', $term->depth) . $term->name;
}
}
}
return $options;
}
name = Options
description = Defines selection, check box and radio button widgets for text and numeric fields.
package = Core
version = VERSION
core = 7.x
dependencies[] = field
files[] = options.test
; Information added by Drupal.org packaging script on 2023-03-15
version = "7.95"
project = "drupal"
datestamp = "1678902530"
' . t('About') . '';
$output .= '' . t('The Options module defines checkbox, selection, and other input widgets for the Field module. See the Field module help page for more information about fields.', array('@field-help' => url('admin/help/field'))) . '
';
return $output;
}
}
/**
* Implements hook_theme().
*/
function options_theme() {
return array(
'options_none' => array(
'variables' => array('instance' => NULL, 'option' => NULL),
),
);
}
/**
* Implements hook_field_widget_info().
*
* Field type modules willing to use those widgets should:
* - Use hook_field_widget_info_alter() to append their field own types to the
* list of types supported by the widgets,
* - Implement hook_options_list() to provide the list of options.
* See list.module.
*/
function options_field_widget_info() {
return array(
'options_select' => array(
'label' => t('Select list'),
'field types' => array(),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_CUSTOM,
),
),
'options_buttons' => array(
'label' => t('Check boxes/radio buttons'),
'field types' => array(),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_CUSTOM,
),
),
'options_onoff' => array(
'label' => t('Single on/off checkbox'),
'field types' => array(),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_CUSTOM,
),
'settings' => array('display_label' => 0),
),
);
}
/**
* Implements hook_field_widget_form().
*/
function options_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
// Abstract over the actual field columns, to allow different field types to
// reuse those widgets.
$value_key = key($field['columns']);
$type = str_replace('options_', '', $instance['widget']['type']);
$multiple = $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED;
$required = $element['#required'];
$has_value = isset($items[0][$value_key]);
$properties = _options_properties($type, $multiple, $required, $has_value);
$entity_type = $element['#entity_type'];
$entity = $element['#entity'];
// Prepare the list of options.
$options = _options_get_options($field, $instance, $properties, $entity_type, $entity);
// Put current field values in shape.
$default_value = _options_storage_to_form($items, $options, $value_key, $properties);
switch ($type) {
case 'select':
$element += array(
'#type' => 'select',
'#default_value' => $default_value,
// Do not display a 'multiple' select box if there is only one option.
'#multiple' => $multiple && count($options) > 1,
'#options' => $options,
);
break;
case 'buttons':
// If required and there is one single option, preselect it.
if ($required && count($options) == 1) {
reset($options);
$default_value = array(key($options));
}
// If this is a single-value field, take the first default value, or
// default to NULL so that the form element is properly recognized as
// not having a default value.
if (!$multiple) {
$default_value = $default_value ? reset($default_value) : NULL;
}
$element += array(
'#type' => $multiple ? 'checkboxes' : 'radios',
// Radio buttons need a scalar value.
'#default_value' => $default_value,
'#options' => $options,
);
break;
case 'onoff':
$keys = array_keys($options);
$off_value = array_shift($keys);
$on_value = array_shift($keys);
$element += array(
'#type' => 'checkbox',
'#default_value' => (isset($default_value[0]) && $default_value[0] == $on_value) ? 1 : 0,
'#on_value' => $on_value,
'#off_value' => $off_value,
);
// Override the title from the incoming $element.
$element['#title'] = isset($options[$on_value]) ? $options[$on_value] : '';
if ($instance['widget']['settings']['display_label']) {
$element['#title'] = $instance['label'];
}
break;
}
$element += array(
'#value_key' => $value_key,
'#element_validate' => array('options_field_widget_validate'),
'#properties' => $properties,
);
return $element;
}
/**
* Implements hook_field_widget_settings_form().
*/
function options_field_widget_settings_form($field, $instance) {
$form = array();
if ($instance['widget']['type'] == 'options_onoff') {
$form['display_label'] = array(
'#type' => 'checkbox',
'#title' => t('Use field label instead of the "On value" as label'),
'#default_value' => $instance['widget']['settings']['display_label'],
'#weight' => -1,
);
}
return $form;
}
/**
* Form element validation handler for options element.
*/
function options_field_widget_validate($element, &$form_state) {
if ($element['#required'] && $element['#value'] == '_none') {
form_error($element, t('!name field is required.', array('!name' => $element['#title'])));
}
// Transpose selections from field => delta to delta => field, turning
// multiple selected options into multiple parent elements.
$items = _options_form_to_storage($element);
form_set_value($element, $items, $form_state);
}
/**
* Describes the preparation steps required by each widget.
*/
function _options_properties($type, $multiple, $required, $has_value) {
$base = array(
'filter_xss' => FALSE,
'strip_tags' => FALSE,
'strip_tags_and_unescape' => FALSE,
'empty_option' => FALSE,
'optgroups' => FALSE,
);
$properties = array();
switch ($type) {
case 'select':
$properties = array(
// Select boxes do not support any HTML tag.
'strip_tags_and_unescape' => TRUE,
'optgroups' => TRUE,
);
if ($multiple) {
// Multiple select: add a 'none' option for non-required fields.
if (!$required) {
$properties['empty_option'] = 'option_none';
}
}
else {
// Single select: add a 'none' option for non-required fields,
// and a 'select a value' option for required fields that do not come
// with a value selected.
if (!$required) {
$properties['empty_option'] = 'option_none';
}
elseif (!$has_value) {
$properties['empty_option'] = 'option_select';
}
}
break;
case 'buttons':
$properties = array(
'filter_xss' => TRUE,
);
// Add a 'none' option for non-required radio buttons.
if (!$required && !$multiple) {
$properties['empty_option'] = 'option_none';
}
break;
case 'onoff':
$properties = array(
'filter_xss' => TRUE,
);
break;
}
return $properties + $base;
}
/**
* Collects the options for a field.
*/
function _options_get_options($field, $instance, $properties, $entity_type, $entity) {
// Get the list of options.
$options = (array) module_invoke($field['module'], 'options_list', $field, $instance, $entity_type, $entity);
// Sanitize the options.
_options_prepare_options($options, $properties);
if (!$properties['optgroups']) {
$options = options_array_flatten($options);
}
if ($properties['empty_option']) {
$label = theme('options_none', array('instance' => $instance, 'option' => $properties['empty_option']));
$options = array('_none' => $label) + $options;
}
return $options;
}
/**
* Sanitizes the options.
*
* The function is recursive to support optgroups.
*/
function _options_prepare_options(&$options, $properties) {
foreach ($options as $value => $label) {
// Recurse for optgroups.
if (is_array($label)) {
_options_prepare_options($options[$value], $properties);
}
else {
// The 'strip_tags' option is deprecated. Use 'strip_tags_and_unescape'
// when plain text is required (and where the output will be run through
// check_plain() before being inserted back into HTML) or 'filter_xss'
// when HTML is required.
if ($properties['strip_tags']) {
$options[$value] = strip_tags($label);
}
if ($properties['strip_tags_and_unescape']) {
$options[$value] = decode_entities(strip_tags($label));
}
if ($properties['filter_xss']) {
$options[$value] = field_filter_xss($label);
}
}
}
}
/**
* Transforms stored field values into the format the widgets need.
*/
function _options_storage_to_form($items, $options, $column, $properties) {
$items_transposed = options_array_transpose($items);
$values = (isset($items_transposed[$column]) && is_array($items_transposed[$column])) ? $items_transposed[$column] : array();
// Discard values that are not in the current list of options. Flatten the
// array if needed.
if ($properties['optgroups']) {
$options = options_array_flatten($options);
}
$values = array_values(array_intersect($values, array_keys($options)));
return $values;
}
/**
* Transforms submitted form values into field storage format.
*/
function _options_form_to_storage($element) {
$values = array_values((array) $element['#value']);
$properties = $element['#properties'];
// On/off checkbox: transform '0 / 1' into the 'on / off' values.
if ($element['#type'] == 'checkbox') {
$values = array($values[0] ? $element['#on_value'] : $element['#off_value']);
}
// Filter out the 'none' option. Use a strict comparison, because
// 0 == 'any string'.
if ($properties['empty_option']) {
$index = array_search('_none', $values, TRUE);
if ($index !== FALSE) {
unset($values[$index]);
}
}
// Make sure we populate at least an empty value.
if (empty($values)) {
$values = array(NULL);
}
$result = options_array_transpose(array($element['#value_key'] => $values));
return $result;
}
/**
* Manipulates a 2D array to reverse rows and columns.
*
* The default data storage for fields is delta first, column names second.
* This is sometimes inconvenient for field modules, so this function can be
* used to present the data in an alternate format.
*
* @param $array
* The array to be transposed. It must be at least two-dimensional, and
* the subarrays must all have the same keys or behavior is undefined.
* @return
* The transposed array.
*/
function options_array_transpose($array) {
$result = array();
if (is_array($array)) {
foreach ($array as $key1 => $value1) {
if (is_array($value1)) {
foreach ($value1 as $key2 => $value2) {
if (!isset($result[$key2])) {
$result[$key2] = array();
}
$result[$key2][$key1] = $value2;
}
}
}
}
return $result;
}
/**
* Flattens an array of allowed values.
*
* @param $array
* A single or multidimensional array.
* @return
* A flattened array.
*/
function options_array_flatten($array) {
$result = array();
if (is_array($array)) {
foreach ($array as $key => $value) {
if (is_array($value)) {
$result += options_array_flatten($value);
}
else {
$result[$key] = $value;
}
}
}
return $result;
}
/**
* Implements hook_field_widget_error().
*/
function options_field_widget_error($element, $error, $form, &$form_state) {
form_error($element, $error['message']);
}
/**
* Returns HTML for the label for the empty value for options that are not required.
*
* The default theme will display N/A for a radio list and '- None -' for a select.
*
* @param $variables
* An associative array containing:
* - instance: An array representing the widget requesting the options.
*
* @ingroup themeable
*/
function theme_options_none($variables) {
$instance = $variables['instance'];
$option = $variables['option'];
$output = '';
switch ($instance['widget']['type']) {
case 'options_buttons':
$output = t('N/A');
break;
case 'options_select':
$output = ($option == 'option_none' ? t('- None -') : t('- Select a value -'));
break;
}
return $output;
}
'Options widgets',
'description' => "Test the Options widgets.",
'group' => 'Field types'
);
}
function setUp() {
parent::setUp('field_test', 'list_test');
// Field with cardinality 1.
$this->card_1 = array(
'field_name' => 'card_1',
'type' => 'list_integer',
'cardinality' => 1,
'settings' => array(
'allowed_values' => array(
// Make sure that 0 works as an option.
0 => 'Zero',
1 => 'One',
// Make sure that option text is properly sanitized.
2 => 'Some & unescaped markup ',
// Make sure that HTML entities in option text are not double-encoded.
3 => 'Some HTML encoded markup with < & >',
),
),
);
$this->card_1 = field_create_field($this->card_1);
// Field with cardinality 2.
$this->card_2 = array(
'field_name' => 'card_2',
'type' => 'list_integer',
'cardinality' => 2,
'settings' => array(
'allowed_values' => array(
// Make sure that 0 works as an option.
0 => 'Zero',
1 => 'One',
// Make sure that option text is properly sanitized.
2 => 'Some & unescaped markup ',
),
),
);
$this->card_2 = field_create_field($this->card_2);
// Boolean field.
$this->bool = array(
'field_name' => 'bool',
'type' => 'list_boolean',
'cardinality' => 1,
'settings' => array(
'allowed_values' => array(
// Make sure that 1 works as a 'on' value'.
1 => 'Zero',
// Make sure that option text is properly sanitized.
0 => 'Some & unescaped markup ',
),
),
);
$this->bool = field_create_field($this->bool);
// Create a web user.
$this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content', 'administer fields'));
$this->drupalLogin($this->web_user);
}
/**
* Tests the 'options_buttons' widget (single select).
*/
function testRadioButtons() {
// Create an instance of the 'single value' field.
$instance = array(
'field_name' => $this->card_1['field_name'],
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'widget' => array(
'type' => 'options_buttons',
),
);
$instance = field_create_instance($instance);
$langcode = LANGUAGE_NONE;
// Create an entity.
$entity_init = field_test_create_stub_entity();
$entity = clone $entity_init;
$entity->is_new = TRUE;
field_test_entity_save($entity);
// With no field data, no buttons are checked.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertNoFieldChecked("edit-card-1-$langcode-0");
$this->assertNoFieldChecked("edit-card-1-$langcode-1");
$this->assertNoFieldChecked("edit-card-1-$langcode-2");
$this->assertRaw('Some dangerous & unescaped markup ', 'Option text was properly filtered.');
// Select first option.
$edit = array("card_1[$langcode]" => 0);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_1', $langcode, array(0));
// Check that the selected button is checked.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertFieldChecked("edit-card-1-$langcode-0");
$this->assertNoFieldChecked("edit-card-1-$langcode-1");
$this->assertNoFieldChecked("edit-card-1-$langcode-2");
// Unselect option.
$edit = array("card_1[$langcode]" => '_none');
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_1', $langcode, array());
// Check that required radios with one option is auto-selected.
$this->card_1['settings']['allowed_values'] = array(99 => 'Only allowed value');
field_update_field($this->card_1);
$instance['required'] = TRUE;
field_update_instance($instance);
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertFieldChecked("edit-card-1-$langcode-99");
}
/**
* Tests the 'options_buttons' widget (multiple select).
*/
function testCheckBoxes() {
// Create an instance of the 'multiple values' field.
$instance = array(
'field_name' => $this->card_2['field_name'],
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'widget' => array(
'type' => 'options_buttons',
),
);
$instance = field_create_instance($instance);
$langcode = LANGUAGE_NONE;
// Create an entity.
$entity_init = field_test_create_stub_entity();
$entity = clone $entity_init;
$entity->is_new = TRUE;
field_test_entity_save($entity);
// Display form: with no field data, nothing is checked.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertNoFieldChecked("edit-card-2-$langcode-0");
$this->assertNoFieldChecked("edit-card-2-$langcode-1");
$this->assertNoFieldChecked("edit-card-2-$langcode-2");
$this->assertRaw('Some dangerous & unescaped markup ', 'Option text was properly filtered.');
// Submit form: select first and third options.
$edit = array(
"card_2[$langcode][0]" => TRUE,
"card_2[$langcode][1]" => FALSE,
"card_2[$langcode][2]" => TRUE,
);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_2', $langcode, array(0, 2));
// Display form: check that the right options are selected.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertFieldChecked("edit-card-2-$langcode-0");
$this->assertNoFieldChecked("edit-card-2-$langcode-1");
$this->assertFieldChecked("edit-card-2-$langcode-2");
// Submit form: select only first option.
$edit = array(
"card_2[$langcode][0]" => TRUE,
"card_2[$langcode][1]" => FALSE,
"card_2[$langcode][2]" => FALSE,
);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_2', $langcode, array(0));
// Display form: check that the right options are selected.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertFieldChecked("edit-card-2-$langcode-0");
$this->assertNoFieldChecked("edit-card-2-$langcode-1");
$this->assertNoFieldChecked("edit-card-2-$langcode-2");
// Submit form: select the three options while the field accepts only 2.
$edit = array(
"card_2[$langcode][0]" => TRUE,
"card_2[$langcode][1]" => TRUE,
"card_2[$langcode][2]" => TRUE,
);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertText('this field cannot hold more than 2 values', 'Validation error was displayed.');
// Submit form: uncheck all options.
$edit = array(
"card_2[$langcode][0]" => FALSE,
"card_2[$langcode][1]" => FALSE,
"card_2[$langcode][2]" => FALSE,
);
$this->drupalPost(NULL, $edit, t('Save'));
// Check that the value was saved.
$this->assertFieldValues($entity_init, 'card_2', $langcode, array());
// Required checkbox with one option is auto-selected.
$this->card_2['settings']['allowed_values'] = array(99 => 'Only allowed value');
field_update_field($this->card_2);
$instance['required'] = TRUE;
field_update_instance($instance);
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertFieldChecked("edit-card-2-$langcode-99");
}
/**
* Tests the 'options_select' widget (single select).
*/
function testSelectListSingle() {
// Create an instance of the 'single value' field.
$instance = array(
'field_name' => $this->card_1['field_name'],
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'required' => TRUE,
'widget' => array(
'type' => 'options_select',
),
);
$instance = field_create_instance($instance);
$langcode = LANGUAGE_NONE;
// Create an entity.
$entity_init = field_test_create_stub_entity();
$entity = clone $entity_init;
$entity->is_new = TRUE;
field_test_entity_save($entity);
// Display form.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
// A required field without any value has a "none" option.
$this->assertTrue($this->xpath('//select[@id=:id]//option[@value="_none" and text()=:label]', array(':id' => 'edit-card-1-' . $langcode, ':label' => t('- Select a value -'))), 'A required select list has a "Select a value" choice.');
// With no field data, nothing is selected.
$this->assertNoOptionSelected("edit-card-1-$langcode", '_none');
$this->assertNoOptionSelected("edit-card-1-$langcode", 0);
$this->assertNoOptionSelected("edit-card-1-$langcode", 1);
$this->assertNoOptionSelected("edit-card-1-$langcode", 2);
$this->assertRaw('Some dangerous & unescaped markup', 'Option text was properly filtered.');
$this->assertRaw('Some HTML encoded markup with < & >', 'HTML entities in option text were properly handled and not double-encoded');
// Submit form: select invalid 'none' option.
$edit = array("card_1[$langcode]" => '_none');
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertRaw(t('!title field is required.', array('!title' => $instance['field_name'])), 'Cannot save a required field when selecting "none" from the select list.');
// Submit form: select first option.
$edit = array("card_1[$langcode]" => 0);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_1', $langcode, array(0));
// Display form: check that the right options are selected.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
// A required field with a value has no 'none' option.
$this->assertFalse($this->xpath('//select[@id=:id]//option[@value="_none"]', array(':id' => 'edit-card-1-' . $langcode)), 'A required select list with an actual value has no "none" choice.');
$this->assertOptionSelected("edit-card-1-$langcode", 0);
$this->assertNoOptionSelected("edit-card-1-$langcode", 1);
$this->assertNoOptionSelected("edit-card-1-$langcode", 2);
// Make the field non required.
$instance['required'] = FALSE;
field_update_instance($instance);
// Display form.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
// A non-required field has a 'none' option.
$this->assertTrue($this->xpath('//select[@id=:id]//option[@value="_none" and text()=:label]', array(':id' => 'edit-card-1-' . $langcode, ':label' => t('- None -'))), 'A non-required select list has a "None" choice.');
// Submit form: Unselect the option.
$edit = array("card_1[$langcode]" => '_none');
$this->drupalPost('test-entity/manage/' . $entity->ftid . '/edit', $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_1', $langcode, array());
// Test optgroups.
$this->card_1['settings']['allowed_values'] = array();
$this->card_1['settings']['allowed_values_function'] = 'list_test_allowed_values_callback';
field_update_field($this->card_1);
// Display form: with no field data, nothing is selected
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertNoOptionSelected("edit-card-1-$langcode", 0);
$this->assertNoOptionSelected("edit-card-1-$langcode", 1);
$this->assertNoOptionSelected("edit-card-1-$langcode", 2);
$this->assertRaw('Some dangerous & unescaped markup', 'Option text was properly filtered.');
$this->assertRaw('Group 1', 'Option groups are displayed.');
// Submit form: select first option.
$edit = array("card_1[$langcode]" => 0);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_1', $langcode, array(0));
// Display form: check that the right options are selected.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertOptionSelected("edit-card-1-$langcode", 0);
$this->assertNoOptionSelected("edit-card-1-$langcode", 1);
$this->assertNoOptionSelected("edit-card-1-$langcode", 2);
// Submit form: Unselect the option.
$edit = array("card_1[$langcode]" => '_none');
$this->drupalPost('test-entity/manage/' . $entity->ftid . '/edit', $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_1', $langcode, array());
// Submit form: select the option from optgroup.
$edit = array("card_1[$langcode]" => 2);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_1', $langcode, array(2));
}
/**
* Tests the 'options_select' widget (multiple select).
*/
function testSelectListMultiple() {
// Create an instance of the 'multiple values' field.
$instance = array(
'field_name' => $this->card_2['field_name'],
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'widget' => array(
'type' => 'options_select',
),
);
$instance = field_create_instance($instance);
$langcode = LANGUAGE_NONE;
// Create an entity.
$entity_init = field_test_create_stub_entity();
$entity = clone $entity_init;
$entity->is_new = TRUE;
field_test_entity_save($entity);
// Display form: with no field data, nothing is selected.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertNoOptionSelected("edit-card-2-$langcode", 0);
$this->assertNoOptionSelected("edit-card-2-$langcode", 1);
$this->assertNoOptionSelected("edit-card-2-$langcode", 2);
$this->assertRaw('Some dangerous & unescaped markup', 'Option text was properly filtered.');
// Submit form: select first and third options.
$edit = array("card_2[$langcode][]" => array(0 => 0, 2 => 2));
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_2', $langcode, array(0, 2));
// Display form: check that the right options are selected.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertOptionSelected("edit-card-2-$langcode", 0);
$this->assertNoOptionSelected("edit-card-2-$langcode", 1);
$this->assertOptionSelected("edit-card-2-$langcode", 2);
// Submit form: select only first option.
$edit = array("card_2[$langcode][]" => array(0 => 0));
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_2', $langcode, array(0));
// Display form: check that the right options are selected.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertOptionSelected("edit-card-2-$langcode", 0);
$this->assertNoOptionSelected("edit-card-2-$langcode", 1);
$this->assertNoOptionSelected("edit-card-2-$langcode", 2);
// Submit form: select the three options while the field accepts only 2.
$edit = array("card_2[$langcode][]" => array(0 => 0, 1 => 1, 2 => 2));
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertText('this field cannot hold more than 2 values', 'Validation error was displayed.');
// Submit form: uncheck all options.
$edit = array("card_2[$langcode][]" => array());
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_2', $langcode, array());
// Test the 'None' option.
// Check that the 'none' option has no effect if actual options are selected
// as well.
$edit = array("card_2[$langcode][]" => array('_none' => '_none', 0 => 0));
$this->drupalPost('test-entity/manage/' . $entity->ftid . '/edit', $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_2', $langcode, array(0));
// Check that selecting the 'none' option empties the field.
$edit = array("card_2[$langcode][]" => array('_none' => '_none'));
$this->drupalPost('test-entity/manage/' . $entity->ftid . '/edit', $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_2', $langcode, array());
// A required select list does not have an empty key.
$instance['required'] = TRUE;
field_update_instance($instance);
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertFalse($this->xpath('//select[@id=:id]//option[@value=""]', array(':id' => 'edit-card-2-' . $langcode)), 'A required select list does not have an empty key.');
// We do not have to test that a required select list with one option is
// auto-selected because the browser does it for us.
// Test optgroups.
// Use a callback function defining optgroups.
$this->card_2['settings']['allowed_values'] = array();
$this->card_2['settings']['allowed_values_function'] = 'list_test_allowed_values_callback';
field_update_field($this->card_2);
$instance['required'] = FALSE;
field_update_instance($instance);
// Display form: with no field data, nothing is selected.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertNoOptionSelected("edit-card-2-$langcode", 0);
$this->assertNoOptionSelected("edit-card-2-$langcode", 1);
$this->assertNoOptionSelected("edit-card-2-$langcode", 2);
$this->assertRaw('Some dangerous & unescaped markup', 'Option text was properly filtered.');
$this->assertRaw('Group 1', 'Option groups are displayed.');
// Submit form: select first option.
$edit = array("card_2[$langcode][]" => array(0 => 0));
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_2', $langcode, array(0));
// Display form: check that the right options are selected.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertOptionSelected("edit-card-2-$langcode", 0);
$this->assertNoOptionSelected("edit-card-2-$langcode", 1);
$this->assertNoOptionSelected("edit-card-2-$langcode", 2);
// Submit form: Unselect the option.
$edit = array("card_2[$langcode][]" => array('_none' => '_none'));
$this->drupalPost('test-entity/manage/' . $entity->ftid . '/edit', $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_2', $langcode, array());
}
/**
* Tests the 'options_onoff' widget.
*/
function testOnOffCheckbox() {
// Create an instance of the 'boolean' field.
$instance = array(
'field_name' => $this->bool['field_name'],
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'widget' => array(
'type' => 'options_onoff',
),
);
$instance = field_create_instance($instance);
$langcode = LANGUAGE_NONE;
// Create an entity.
$entity_init = field_test_create_stub_entity();
$entity = clone $entity_init;
$entity->is_new = TRUE;
field_test_entity_save($entity);
// Display form: with no field data, option is unchecked.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertNoFieldChecked("edit-bool-$langcode");
$this->assertRaw('Some dangerous & unescaped markup ', 'Option text was properly filtered.');
// Submit form: check the option.
$edit = array("bool[$langcode]" => TRUE);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'bool', $langcode, array(0));
// Display form: check that the right options are selected.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertFieldChecked("edit-bool-$langcode");
// Submit form: uncheck the option.
$edit = array("bool[$langcode]" => FALSE);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'bool', $langcode, array(1));
// Display form: with 'off' value, option is unchecked.
$this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit');
$this->assertNoFieldChecked("edit-bool-$langcode");
// Create admin user.
$admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer taxonomy', 'administer fields'));
$this->drupalLogin($admin_user);
// Create a test field instance.
$fieldUpdate = $this->bool;
$fieldUpdate['settings']['allowed_values'] = array(0 => 0, 1 => 'MyOnValue');
field_update_field($fieldUpdate);
$instance = array(
'field_name' => $this->bool['field_name'],
'entity_type' => 'node',
'bundle' => 'page',
'widget' => array(
'type' => 'options_onoff',
'module' => 'options',
),
);
field_create_instance($instance);
// Go to the edit page and check if the default settings works as expected
$fieldEditUrl = 'admin/structure/types/manage/page/fields/bool';
$this->drupalGet($fieldEditUrl);
$this->assertText(
'Use field label instead of the "On value" as label ',
'Display setting checkbox available.'
);
$this->assertFieldByXPath(
'*//label[@for="edit-' . $this->bool['field_name'] . '-und" and text()="MyOnValue "]',
TRUE,
'Default case shows "On value"'
);
// Enable setting
$edit = array('instance[widget][settings][display_label]' => 1);
// Save the new Settings
$this->drupalPost($fieldEditUrl, $edit, t('Save settings'));
// Go again to the edit page and check if the setting
// is stored and has the expected effect
$this->drupalGet($fieldEditUrl);
$this->assertText(
'Use field label instead of the "On value" as label ',
'Display setting checkbox is available'
);
$this->assertFieldChecked(
'edit-instance-widget-settings-display-label',
'Display settings checkbox checked'
);
$this->assertFieldByXPath(
'*//label[@for="edit-' . $this->bool['field_name'] . '-und" and text()="' . $this->bool['field_name'] . ' "]',
TRUE,
'Display label changes label of the checkbox'
);
}
}
/**
* Test an options select on a list field with a dynamic allowed values function.
*/
class OptionsSelectDynamicValuesTestCase extends ListDynamicValuesTestCase {
public static function getInfo() {
return array(
'name' => 'Options select dynamic values',
'description' => 'Test an options select on a list field with a dynamic allowed values function.',
'group' => 'Field types',
);
}
/**
* Tests the 'options_select' widget (single select).
*/
function testSelectListDynamic() {
// Create an entity.
$this->entity->is_new = TRUE;
field_test_entity_save($this->entity);
// Create a web user.
$web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content'));
$this->drupalLogin($web_user);
// Display form.
$this->drupalGet('test-entity/manage/' . $this->entity->ftid . '/edit');
$options = $this->xpath('//select[@id="edit-test-list-und"]/option');
$this->assertEqual(count($options), count($this->test) + 1);
foreach ($options as $option) {
$value = (string) $option['value'];
if ($value != '_none') {
$this->assertTrue(array_search($value, $this->test));
}
}
}
}
name = Text
description = Defines simple text field types.
package = Core
version = VERSION
core = 7.x
dependencies[] = field
files[] = text.test
required = TRUE
; Information added by Drupal.org packaging script on 2023-03-15
version = "7.95"
project = "drupal"
datestamp = "1678902530"
array(
'type' => 'varchar',
'length' => $field['settings']['max_length'],
'not null' => FALSE,
),
);
break;
case 'text_long':
$columns = array(
'value' => array(
'type' => 'text',
'size' => 'big',
'not null' => FALSE,
),
);
break;
case 'text_with_summary':
$columns = array(
'value' => array(
'type' => 'text',
'size' => 'big',
'not null' => FALSE,
),
'summary' => array(
'type' => 'text',
'size' => 'big',
'not null' => FALSE,
),
);
break;
}
$columns += array(
'format' => array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
),
);
return array(
'columns' => $columns,
'indexes' => array(
'format' => array('format'),
),
'foreign keys' => array(
'format' => array(
'table' => 'filter_format',
'columns' => array('format' => 'format'),
),
),
);
}
/**
* Change text field 'format' columns into varchar.
*/
function text_update_7000() {
$spec = array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
);
$fields = _update_7000_field_read_fields(array(
'module' => 'text',
'storage_type' => 'field_sql_storage',
));
foreach ($fields as $field) {
if ($field['deleted']) {
$table = "field_deleted_data_{$field['id']}";
$revision_table = "field_deleted_revision_{$field['id']}";
}
else {
$table = "field_data_{$field['field_name']}";
$revision_table = "field_revision_{$field['field_name']}";
}
$column = $field['field_name'] . '_' . 'format';
db_change_field($table, $column, $column, $spec);
db_change_field($revision_table, $column, $column, $spec);
}
}
(function ($) {
/**
* Auto-hide summary textarea if empty and show hide and unhide links.
*/
Drupal.behaviors.textSummary = {
attach: function (context, settings) {
$('.text-summary', context).once('text-summary', function () {
var $widget = $(this).closest('div.field-type-text-with-summary');
var $summaries = $widget.find('div.text-summary-wrapper');
$summaries.once('text-summary-wrapper').each(function(index) {
var $summary = $(this);
var $summaryLabel = $summary.find('label').first();
var $full = $widget.find('.text-full').eq(index).closest('.form-item');
var $fullLabel = $full.find('label').first();
// Create a placeholder label when the field cardinality is
// unlimited or greater than 1.
if ($fullLabel.length == 0) {
$fullLabel = $(' ').prependTo($full);
}
// Setup the edit/hide summary link.
var $link = $('(' + Drupal.t('Hide summary') + ' ) ');
var $a = $link.find('a');
var toggleClick = true;
$link.bind('click', function (e) {
if (toggleClick) {
$summary.hide();
$a.html(Drupal.t('Edit summary'));
$link.appendTo($fullLabel);
}
else {
$summary.show();
$a.html(Drupal.t('Hide summary'));
$link.appendTo($summaryLabel);
}
toggleClick = !toggleClick;
return false;
}).appendTo($summaryLabel);
// If no summary is set, hide the summary field.
if ($(this).find('.text-summary').val() == '') {
$link.click();
}
});
});
}
};
})(jQuery);
' . t('About') . '';
$output .= '' . t("The Text module defines various text field types for the Field module. A text field may contain plain text only, or optionally, may use Drupal's text filters to securely manage HTML output. Text input fields may be either a single line (text field), multiple lines (text area), or for greater input control, a select box, checkbox, or radio buttons. If desired, the field can be validated, so that it is limited to a set of allowed values. See the Field module help page for more information about fields.", array('@field-help' => url('admin/help/field'), '@filter-help' => url('admin/help/filter'))) . '
';
return $output;
}
}
/**
* Implements hook_field_info().
*
* Field settings:
* - max_length: the maximum length for a varchar field.
* Instance settings:
* - text_processing: whether text input filters should be used.
* - display_summary: whether the summary field should be displayed.
* When empty and not displayed the summary will take its value from the
* trimmed value of the main text field.
*/
function text_field_info() {
return array(
'text' => array(
'label' => t('Text'),
'description' => t('This field stores varchar text in the database.'),
'settings' => array('max_length' => 255),
'instance_settings' => array('text_processing' => 0),
'default_widget' => 'text_textfield',
'default_formatter' => 'text_default',
),
'text_long' => array(
'label' => t('Long text'),
'description' => t('This field stores long text in the database.'),
'instance_settings' => array('text_processing' => 0),
'default_widget' => 'text_textarea',
'default_formatter' => 'text_default',
),
'text_with_summary' => array(
'label' => t('Long text and summary'),
'description' => t('This field stores long text in the database along with optional summary text.'),
'instance_settings' => array('text_processing' => 1, 'display_summary' => 0),
'default_widget' => 'text_textarea_with_summary',
'default_formatter' => 'text_default',
),
);
}
/**
* Implements hook_field_settings_form().
*/
function text_field_settings_form($field, $instance, $has_data) {
$settings = $field['settings'];
$form = array();
if ($field['type'] == 'text') {
$form['max_length'] = array(
'#type' => 'textfield',
'#title' => t('Maximum length'),
'#default_value' => $settings['max_length'],
'#required' => TRUE,
'#description' => t('The maximum length of the field in characters.'),
'#element_validate' => array('element_validate_integer_positive'),
// @todo: If $has_data, add a validate handler that only allows
// max_length to increase.
'#disabled' => $has_data,
);
}
return $form;
}
/**
* Implements hook_field_instance_settings_form().
*/
function text_field_instance_settings_form($field, $instance) {
$settings = $instance['settings'];
$form['text_processing'] = array(
'#type' => 'radios',
'#title' => t('Text processing'),
'#default_value' => $settings['text_processing'],
'#options' => array(
t('Plain text'),
t('Filtered text (user selects text format)'),
),
);
if ($field['type'] == 'text_with_summary') {
$form['display_summary'] = array(
'#type' => 'checkbox',
'#title' => t('Summary input'),
'#default_value' => $settings['display_summary'],
'#description' => t('This allows authors to input an explicit summary, to be displayed instead of the automatically trimmed text when using the "Summary or trimmed" display type.'),
);
}
return $form;
}
/**
* Implements hook_field_validate().
*
* Possible error codes:
* - 'text_value_max_length': The value exceeds the maximum length.
* - 'text_summary_max_length': The summary exceeds the maximum length.
*/
function text_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
foreach ($items as $delta => $item) {
// @todo Length is counted separately for summary and value, so the maximum
// length can be exceeded very easily.
foreach (array('value', 'summary') as $column) {
if (!empty($item[$column])) {
if (!empty($field['settings']['max_length']) && drupal_strlen($item[$column]) > $field['settings']['max_length']) {
switch ($column) {
case 'value':
$message = t('%name: the text may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length']));
break;
case 'summary':
$message = t('%name: the summary may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length']));
break;
}
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => "text_{$column}_length",
'message' => $message,
);
}
}
}
}
}
/**
* Implements hook_field_load().
*
* Where possible, generate the sanitized version of each field early so that
* it is cached in the field cache. This avoids looking up from the filter cache
* separately.
*
* @see text_field_formatter_view()
*/
function text_field_load($entity_type, $entities, $field, $instances, $langcode, &$items) {
foreach ($entities as $id => $entity) {
foreach ($items[$id] as $delta => $item) {
// Only process items with a cacheable format, the rest will be handled
// by formatters if needed.
if (empty($instances[$id]['settings']['text_processing']) || filter_format_allowcache($item['format'])) {
$items[$id][$delta]['safe_value'] = isset($item['value']) ? _text_sanitize($instances[$id], $langcode, $item, 'value') : '';
if ($field['type'] == 'text_with_summary') {
$items[$id][$delta]['safe_summary'] = isset($item['summary']) ? _text_sanitize($instances[$id], $langcode, $item, 'summary') : '';
}
}
}
}
}
/**
* Implements hook_field_is_empty().
*/
function text_field_is_empty($item, $field) {
if (!isset($item['value']) || $item['value'] === '') {
return !isset($item['summary']) || $item['summary'] === '';
}
return FALSE;
}
/**
* Implements hook_field_formatter_info().
*/
function text_field_formatter_info() {
return array(
'text_default' => array(
'label' => t('Default'),
'field types' => array('text', 'text_long', 'text_with_summary'),
),
'text_plain' => array(
'label' => t('Plain text'),
'field types' => array('text', 'text_long', 'text_with_summary'),
),
// The text_trimmed formatter displays the trimmed version of the
// full element of the field. It is intended to be used with text
// and text_long fields. It also works with text_with_summary
// fields though the text_summary_or_trimmed formatter makes more
// sense for that field type.
'text_trimmed' => array(
'label' => t('Trimmed'),
'field types' => array('text', 'text_long', 'text_with_summary'),
'settings' => array('trim_length' => 600),
),
// The 'summary or trimmed' field formatter for text_with_summary
// fields displays returns the summary element of the field or, if
// the summary is empty, the trimmed version of the full element
// of the field.
'text_summary_or_trimmed' => array(
'label' => t('Summary or trimmed'),
'field types' => array('text_with_summary'),
'settings' => array('trim_length' => 600),
),
);
}
/**
* Implements hook_field_formatter_settings_form().
*/
function text_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$element = array();
if (strpos($display['type'], '_trimmed') !== FALSE) {
$element['trim_length'] = array(
'#title' => t('Trimmed limit'),
'#type' => 'textfield',
'#field_suffix' => t('characters'),
'#size' => 10,
'#default_value' => $settings['trim_length'],
'#element_validate' => array('element_validate_integer_positive'),
'#description' => t('If the summary is not set, the trimmed %label field will be shorter than this character limit.', array('%label' => $instance['label'])),
'#required' => TRUE,
);
}
return $element;
}
/**
* Implements hook_field_formatter_settings_summary().
*/
function text_field_formatter_settings_summary($field, $instance, $view_mode) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$summary = '';
if (strpos($display['type'], '_trimmed') !== FALSE) {
$summary = t('Trimmed limit: @trim_length characters', array('@trim_length' => $settings['trim_length']));
}
return $summary;
}
/**
* Implements hook_field_formatter_view().
*/
function text_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$element = array();
switch ($display['type']) {
case 'text_default':
case 'text_trimmed':
foreach ($items as $delta => $item) {
$output = _text_sanitize($instance, $langcode, $item, 'value');
if ($display['type'] == 'text_trimmed') {
$output = text_summary($output, $instance['settings']['text_processing'] ? $item['format'] : NULL, $display['settings']['trim_length']);
}
$element[$delta] = array('#markup' => $output);
}
break;
case 'text_summary_or_trimmed':
foreach ($items as $delta => $item) {
if (!empty($item['summary'])) {
$output = _text_sanitize($instance, $langcode, $item, 'summary');
}
else {
$output = _text_sanitize($instance, $langcode, $item, 'value');
$output = text_summary($output, $instance['settings']['text_processing'] ? $item['format'] : NULL, $display['settings']['trim_length']);
}
$element[$delta] = array('#markup' => $output);
}
break;
case 'text_plain':
foreach ($items as $delta => $item) {
$element[$delta] = array('#markup' => strip_tags($item['value']));
}
break;
}
return $element;
}
/**
* Sanitizes the 'value' or 'summary' data of a text value.
*
* Depending on whether the field instance uses text processing, data is run
* through check_plain() or check_markup().
*
* @param $instance
* The instance definition.
* @param $langcode
* The language associated to $item.
* @param $item
* The field value to sanitize.
* @param $column
* The column to sanitize (either 'value' or 'summary').
*
* @return
* The sanitized string.
*/
function _text_sanitize($instance, $langcode, $item, $column) {
// If the value uses a cacheable text format, text_field_load() precomputes
// the sanitized string.
if (isset($item["safe_$column"])) {
return $item["safe_$column"];
}
return $instance['settings']['text_processing'] ? check_markup($item[$column], $item['format'], $langcode) : check_plain($item[$column]);
}
/**
* Generate a trimmed, formatted version of a text field value.
*
* If the end of the summary is not indicated using the delimiter
* then we generate the summary automatically, trying to end it at a sensible
* place such as the end of a paragraph, a line break, or the end of a
* sentence (in that order of preference).
*
* @param $text
* The content for which a summary will be generated.
* @param $format
* The format of the content.
* If the PHP filter is present and $text contains PHP code, we do not
* split it up to prevent parse errors.
* If the line break filter is present then we treat newlines embedded in
* $text as line breaks.
* If the htmlcorrector filter is present, it will be run on the generated
* summary (if different from the incoming $text).
* @param $size
* The desired character length of the summary. If omitted, the default
* value will be used. Ignored if the special delimiter is present
* in $text.
* @return
* The generated summary.
*/
function text_summary($text, $format = NULL, $size = NULL) {
// If the input text is NULL, return unchanged.
if (is_null($text)) {
return NULL;
}
if (!isset($size)) {
// What used to be called 'teaser' is now called 'summary', but
// the variable 'teaser_length' is preserved for backwards compatibility.
$size = variable_get('teaser_length', 600);
}
// Find where the delimiter is in the body
$delimiter = strpos($text, '');
// If the size is zero, and there is no delimiter, the entire body is the summary.
if ($size == 0 && $delimiter === FALSE) {
return $text;
}
// If a valid delimiter has been specified, use it to chop off the summary.
if ($delimiter !== FALSE) {
return substr($text, 0, $delimiter);
}
// We check for the presence of the PHP evaluator filter in the current
// format. If the body contains PHP code, we do not split it up to prevent
// parse errors.
if (isset($format)) {
$filters = filter_list_format($format);
if (isset($filters['php_code']) && $filters['php_code']->status && strpos($text, '') !== FALSE) {
return $text;
}
}
// If we have a short body, the entire body is the summary.
if (drupal_strlen($text) <= $size) {
return $text;
}
// If the delimiter has not been specified, try to split at paragraph or
// sentence boundaries.
// The summary may not be longer than maximum length specified. Initial slice.
$summary = truncate_utf8($text, $size);
// Store the actual length of the UTF8 string -- which might not be the same
// as $size.
$max_rpos = strlen($summary);
// How much to cut off the end of the summary so that it doesn't end in the
// middle of a paragraph, sentence, or word.
// Initialize it to maximum in order to find the minimum.
$min_rpos = $max_rpos;
// Store the reverse of the summary. We use strpos on the reversed needle and
// haystack for speed and convenience.
$reversed = strrev($summary);
// Build an array of arrays of break points grouped by preference.
$break_points = array();
// A paragraph near the end of sliced summary is most preferable.
$break_points[] = array('' => 0);
// If no complete paragraph then treat line breaks as paragraphs.
$line_breaks = array(' ' => 6, ' ' => 4);
// Newline only indicates a line break if line break converter
// filter is present.
if (isset($filters['filter_autop'])) {
$line_breaks["\n"] = 1;
}
$break_points[] = $line_breaks;
// If the first paragraph is too long, split at the end of a sentence.
$break_points[] = array('. ' => 1, '! ' => 1, '? ' => 1, '。' => 0, '؟ ' => 1);
// Iterate over the groups of break points until a break point is found.
foreach ($break_points as $points) {
// Look for each break point, starting at the end of the summary.
foreach ($points as $point => $offset) {
// The summary is already reversed, but the break point isn't.
$rpos = strpos($reversed, strrev($point));
if ($rpos !== FALSE) {
$min_rpos = min($rpos + $offset, $min_rpos);
}
}
// If a break point was found in this group, slice and stop searching.
if ($min_rpos !== $max_rpos) {
// Don't slice with length 0. Length must be <0 to slice from RHS.
$summary = ($min_rpos === 0) ? $summary : substr($summary, 0, 0 - $min_rpos);
break;
}
}
// If the htmlcorrector filter is present, apply it to the generated summary.
if (isset($filters['filter_htmlcorrector'])) {
$summary = _filter_htmlcorrector($summary);
}
return $summary;
}
/**
* Implements hook_field_widget_info().
*/
function text_field_widget_info() {
return array(
'text_textfield' => array(
'label' => t('Text field'),
'field types' => array('text'),
'settings' => array('size' => 60),
),
'text_textarea' => array(
'label' => t('Text area (multiple rows)'),
'field types' => array('text_long'),
'settings' => array('rows' => 5),
),
'text_textarea_with_summary' => array(
'label' => t('Text area with a summary'),
'field types' => array('text_with_summary'),
'settings' => array('rows' => 20, 'summary_rows' => 5),
),
);
}
/**
* Implements hook_field_widget_settings_form().
*/
function text_field_widget_settings_form($field, $instance) {
$widget = $instance['widget'];
$settings = $widget['settings'];
if ($widget['type'] == 'text_textfield') {
$form['size'] = array(
'#type' => 'textfield',
'#title' => t('Size of textfield'),
'#default_value' => $settings['size'],
'#required' => TRUE,
'#element_validate' => array('element_validate_integer_positive'),
);
}
else {
$form['rows'] = array(
'#type' => 'textfield',
'#title' => t('Rows'),
'#default_value' => $settings['rows'],
'#required' => TRUE,
'#element_validate' => array('element_validate_integer_positive'),
);
}
return $form;
}
/**
* Implements hook_field_widget_form().
*/
function text_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
$summary_widget = array();
$main_widget = array();
switch ($instance['widget']['type']) {
case 'text_textfield':
$main_widget = $element + array(
'#type' => 'textfield',
'#default_value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : NULL,
'#size' => $instance['widget']['settings']['size'],
'#maxlength' => $field['settings']['max_length'],
'#attributes' => array('class' => array('text-full')),
);
break;
case 'text_textarea_with_summary':
$display = !empty($items[$delta]['summary']) || !empty($instance['settings']['display_summary']);
$summary_widget = array(
'#type' => $display ? 'textarea' : 'value',
'#default_value' => isset($items[$delta]['summary']) ? $items[$delta]['summary'] : NULL,
'#title' => t('Summary'),
'#rows' => $instance['widget']['settings']['summary_rows'],
'#description' => t('Leave blank to use trimmed value of full text as the summary.'),
'#attached' => array(
'js' => array(drupal_get_path('module', 'text') . '/text.js'),
),
'#attributes' => array('class' => array('text-summary')),
'#prefix' => '',
'#suffix' => '
',
'#weight' => -10,
);
// Fall through to the next case.
case 'text_textarea':
$main_widget = $element + array(
'#type' => 'textarea',
'#default_value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : NULL,
'#rows' => $instance['widget']['settings']['rows'],
'#attributes' => array('class' => array('text-full')),
);
break;
}
if ($main_widget) {
// Conditionally alter the form element's type if text processing is enabled.
if ($instance['settings']['text_processing']) {
$element = $main_widget;
$element['#type'] = 'text_format';
$element['#format'] = isset($items[$delta]['format']) ? $items[$delta]['format'] : NULL;
$element['#base_type'] = $main_widget['#type'];
}
else {
$element['value'] = $main_widget;
}
}
if ($summary_widget) {
$element['summary'] = $summary_widget;
}
return $element;
}
/**
* Implements hook_field_widget_error().
*/
function text_field_widget_error($element, $error, $form, &$form_state) {
switch ($error['error']) {
case 'text_summary_max_length':
$error_element = $element[$element['#columns'][1]];
break;
default:
$error_element = $element[$element['#columns'][0]];
break;
}
form_error($error_element, $error['message']);
}
/**
* Implements hook_field_prepare_translation().
*/
function text_field_prepare_translation($entity_type, $entity, $field, $instance, $langcode, &$items, $source_entity, $source_langcode) {
// If the translating user is not permitted to use the assigned text format,
// we must not expose the source values.
$field_name = $field['field_name'];
if (!empty($source_entity->{$field_name}[$source_langcode])) {
$formats = filter_formats();
foreach ($source_entity->{$field_name}[$source_langcode] as $delta => $item) {
$format_id = $item['format'];
if (!empty($format_id) && !filter_access($formats[$format_id])) {
unset($items[$delta]);
}
}
}
}
/**
* Implements hook_filter_format_update().
*/
function text_filter_format_update($format) {
field_cache_clear();
}
/**
* Implements hook_filter_format_disable().
*/
function text_filter_format_disable($format) {
field_cache_clear();
}
'Text field',
'description' => "Test the creation of text fields.",
'group' => 'Field types'
);
}
function setUp() {
parent::setUp('field_test');
$this->admin_user = $this->drupalCreateUser(array('administer filters'));
$this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content'));
$this->drupalLogin($this->web_user);
}
// Test fields.
/**
* Test text field validation.
*/
function testTextFieldValidation() {
// Create a field with settings to validate.
$max_length = 3;
$this->field = array(
'field_name' => drupal_strtolower($this->randomName()),
'type' => 'text',
'settings' => array(
'max_length' => $max_length,
)
);
field_create_field($this->field);
$this->instance = array(
'field_name' => $this->field['field_name'],
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'widget' => array(
'type' => 'text_textfield',
),
'display' => array(
'default' => array(
'type' => 'text_default',
),
),
);
field_create_instance($this->instance);
// Test valid and invalid values with field_attach_validate().
$entity = field_test_create_stub_entity();
$langcode = LANGUAGE_NONE;
for ($i = 0; $i <= $max_length + 2; $i++) {
$entity->{$this->field['field_name']}[$langcode][0]['value'] = str_repeat('x', $i);
try {
field_attach_validate('test_entity', $entity);
$this->assertTrue($i <= $max_length, "Length $i does not cause validation error when max_length is $max_length");
}
catch (FieldValidationException $e) {
$this->assertTrue($i > $max_length, "Length $i causes validation error when max_length is $max_length");
}
}
}
/**
* Test widgets.
*/
function testTextfieldWidgets() {
$this->_testTextfieldWidgets('text', 'text_textfield');
$this->_testTextfieldWidgets('text_long', 'text_textarea');
}
/**
* Helper function for testTextfieldWidgets().
*/
function _testTextfieldWidgets($field_type, $widget_type) {
// Setup a field and instance
$entity_type = 'test_entity';
$this->field_name = drupal_strtolower($this->randomName());
$this->field = array('field_name' => $this->field_name, 'type' => $field_type);
field_create_field($this->field);
$this->instance = array(
'field_name' => $this->field_name,
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'label' => $this->randomName() . '_label',
'settings' => array(
'text_processing' => TRUE,
),
'widget' => array(
'type' => $widget_type,
),
'display' => array(
'full' => array(
'type' => 'text_default',
),
),
);
field_create_instance($this->instance);
$langcode = LANGUAGE_NONE;
// Display creation form.
$this->drupalGet('test-entity/add/test-bundle');
$this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget is displayed');
$this->assertNoFieldByName("{$this->field_name}[$langcode][0][format]", '1', 'Format selector is not displayed');
// Submit with some value.
$value = $this->randomName();
$edit = array(
"{$this->field_name}[$langcode][0][value]" => $value,
);
$this->drupalPost(NULL, $edit, t('Save'));
preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
$id = $match[1];
$this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created');
// Display the entity.
$entity = field_test_entity_test_load($id);
$entity->content = field_attach_view($entity_type, $entity, 'full');
$this->content = drupal_render($entity->content);
$this->assertText($value, 'Filtered tags are not displayed');
}
/**
* Test widgets + 'formatted_text' setting.
*/
function testTextfieldWidgetsFormatted() {
$this->_testTextfieldWidgetsFormatted('text', 'text_textfield');
$this->_testTextfieldWidgetsFormatted('text_long', 'text_textarea');
}
/**
* Helper function for testTextfieldWidgetsFormatted().
*/
function _testTextfieldWidgetsFormatted($field_type, $widget_type) {
// Setup a field and instance
$entity_type = 'test_entity';
$this->field_name = drupal_strtolower($this->randomName());
$this->field = array('field_name' => $this->field_name, 'type' => $field_type);
field_create_field($this->field);
$this->instance = array(
'field_name' => $this->field_name,
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'label' => $this->randomName() . '_label',
'settings' => array(
'text_processing' => TRUE,
),
'widget' => array(
'type' => $widget_type,
),
'display' => array(
'full' => array(
'type' => 'text_default',
),
),
);
field_create_instance($this->instance);
$langcode = LANGUAGE_NONE;
// Disable all text formats besides the plain text fallback format.
$this->drupalLogin($this->admin_user);
foreach (filter_formats() as $format) {
if ($format->format != filter_fallback_format()) {
$this->drupalPost('admin/config/content/formats/' . $format->format . '/disable', array(), t('Disable'));
}
}
$this->drupalLogin($this->web_user);
// Display the creation form. Since the user only has access to one format,
// no format selector will be displayed.
$this->drupalGet('test-entity/add/test-bundle');
$this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget is displayed');
$this->assertNoFieldByName("{$this->field_name}[$langcode][0][format]", '', 'Format selector is not displayed');
// Submit with data that should be filtered.
$value = '' . $this->randomName() . ' ';
$edit = array(
"{$this->field_name}[$langcode][0][value]" => $value,
);
$this->drupalPost(NULL, $edit, t('Save'));
preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
$id = $match[1];
$this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created');
// Display the entity.
$entity = field_test_entity_test_load($id);
$entity->content = field_attach_view($entity_type, $entity, 'full');
$this->content = drupal_render($entity->content);
$this->assertNoRaw($value, 'HTML tags are not displayed.');
$this->assertRaw(check_plain($value), 'Escaped HTML is displayed correctly.');
// Create a new text format that does not escape HTML, and grant the user
// access to it.
$this->drupalLogin($this->admin_user);
$edit = array(
'format' => drupal_strtolower($this->randomName()),
'name' => $this->randomName(),
);
$this->drupalPost('admin/config/content/formats/add', $edit, t('Save configuration'));
filter_formats_reset();
$this->checkPermissions(array(), TRUE);
$format = filter_format_load($edit['format']);
$format_id = $format->format;
$permission = filter_permission_name($format);
$rid = max(array_keys($this->web_user->roles));
user_role_grant_permissions($rid, array($permission));
$this->drupalLogin($this->web_user);
// Display edition form.
// We should now have a 'text format' selector.
$this->drupalGet('test-entity/manage/' . $id . '/edit');
$this->assertFieldByName("{$this->field_name}[$langcode][0][value]", NULL, 'Widget is displayed');
$this->assertFieldByName("{$this->field_name}[$langcode][0][format]", NULL, 'Format selector is displayed');
// Edit and change the text format to the new one that was created.
$edit = array(
"{$this->field_name}[$langcode][0][format]" => $format_id,
);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)), 'Entity was updated');
// Display the entity.
$entity = field_test_entity_test_load($id);
$entity->content = field_attach_view($entity_type, $entity, 'full');
$this->content = drupal_render($entity->content);
$this->assertRaw($value, 'Value is displayed unfiltered');
}
}
class TextSummaryTestCase extends DrupalWebTestCase {
protected $article_creator;
public static function getInfo() {
return array(
'name' => 'Text summary',
'description' => 'Test text_summary() with different strings and lengths.',
'group' => 'Field types',
);
}
function setUp() {
parent::setUp();
$this->article_creator = $this->drupalCreateUser(array('create article content', 'edit own article content'));
}
/**
* Tests an edge case where the first sentence is a question and
* subsequent sentences are not. This edge case is documented at
* http://drupal.org/node/180425.
*/
function testFirstSentenceQuestion() {
$text = 'A question? A sentence. Another sentence.';
$expected = 'A question? A sentence.';
$this->callTextSummary($text, $expected, NULL, 30);
}
/**
* Test summary with long example.
*/
function testLongSentence() {
$text = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' . // 125
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ' . // 108
'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ' . // 103
'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'; // 110
$expected = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' .
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ' .
'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.';
// First three sentences add up to: 336, so add one for space and then 3 to get half-way into next word.
$this->callTextSummary($text, $expected, NULL, 340);
}
/**
* Test various summary length edge cases.
*/
function testLength() {
// This string tests a number of edge cases.
$text = "\nHi\n
\n\nfolks\n \n!\n
";
// The summaries we expect text_summary() to return when $size is the index
// of each array item.
// Using no text format:
$expected = array(
"\nHi\n
\n\nfolks\n \n!\n
",
"<",
"",
"
\n",
"
\nH",
"
\nHi",
"
\nHi\n",
"
\nHi\n<",
"
\nHi\n",
"
\nHi\n
\nHi\n",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
\n\nfolks\n \n!\n
",
"\nHi\n
\n\nfolks\n \n!\n
",
"\nHi\n
\n\nfolks\n \n!\n
",
);
// And using a text format WITH the line-break and htmlcorrector filters.
$expected_lb = array(
"\nHi\n
\n\nfolks\n \n!\n
",
"",
"
",
"
",
"
",
"
",
"
",
"\nHi
",
"\nHi
",
"\nHi
",
"\nHi
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
",
"\nHi\n
\n\nfolks\n \n!\n
",
"\nHi\n
\n\nfolks\n \n!\n
",
"\nHi\n
\n\nfolks\n \n!\n
",
);
// Test text_summary() for different sizes.
for ($i = 0; $i <= 37; $i++) {
$this->callTextSummary($text, $expected[$i], NULL, $i);
$this->callTextSummary($text, $expected_lb[$i], 'plain_text', $i);
$this->callTextSummary($text, $expected_lb[$i], 'filtered_html', $i);
}
}
/**
* Test for the NULL value.
*/
function testNullSentence() {
$summary = text_summary(NULL);
$this->assertNull($summary, 'text_summary() casts returned null');
}
/**
* Calls text_summary() and asserts that the expected teaser is returned.
*/
function callTextSummary($text, $expected, $format = NULL, $size = NULL) {
$summary = text_summary($text, $format, $size);
$this->assertIdentical($summary, $expected, format_string('Generated summary "@summary" matches expected "@expected".', array('@summary' => $summary, '@expected' => $expected)));
}
/**
* Test sending only summary.
*/
function testOnlyTextSummary() {
// Login as article creator.
$this->drupalLogin($this->article_creator);
// Create article with summary but empty body.
$summary = $this->randomName();
$edit = array(
"title" => $this->randomName(),
"body[und][0][summary]" => $summary,
);
$this->drupalPost('node/add/article', $edit, t('Save'));
$node = $this->drupalGetNodeByTitle($edit['title']);
$this->assertIdentical($node->body['und'][0]['summary'], $summary, 'Article with with summary and no body has been submitted.');
}
}
class TextTranslationTestCase extends DrupalWebTestCase {
protected $format;
protected $admin;
protected $translator;
public static function getInfo() {
return array(
'name' => 'Text translation',
'description' => 'Check if the text field is correctly prepared for translation.',
'group' => 'Field types',
);
}
function setUp() {
parent::setUp('locale', 'translation');
$full_html_format = filter_format_load('full_html');
$this->format = $full_html_format->format;
$this->admin = $this->drupalCreateUser(array(
'administer languages',
'administer content types',
'access administration pages',
'bypass node access',
'administer fields',
filter_permission_name($full_html_format),
));
$this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content'));
// Enable an additional language.
$this->drupalLogin($this->admin);
$edit = array('langcode' => 'fr');
$this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
// Set "Article" content type to use multilingual support with translation.
$edit = array('language_content_type' => 2);
$this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
$this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Article')), 'Article content type has been updated.');
}
/**
* Test that a plaintext textfield widget is correctly populated.
*/
function testTextField() {
// Disable text processing for body.
$edit = array('instance[settings][text_processing]' => 0);
$this->drupalPost('admin/structure/types/manage/article/fields/body', $edit, t('Save settings'));
// Login as translator.
$this->drupalLogin($this->translator);
// Create content.
$langcode = LANGUAGE_NONE;
$body = $this->randomName();
$edit = array(
"title" => $this->randomName(),
"language" => 'en',
"body[$langcode][0][value]" => $body,
);
// Translate the article in french.
$this->drupalPost('node/add/article', $edit, t('Save'));
$node = $this->drupalGetNodeByTitle($edit['title']);
$this->drupalGet("node/$node->nid/translate");
$this->clickLink(t('add translation'));
$this->assertFieldByXPath("//textarea[@name='body[$langcode][0][value]']", $body, 'The textfield widget is populated.');
}
/**
* Check that user that does not have access the field format cannot see the
* source value when creating a translation.
*/
function testTextFieldFormatted() {
// Make node body multiple.
$edit = array('field[cardinality]' => -1);
$this->drupalPost('admin/structure/types/manage/article/fields/body', $edit, t('Save settings'));
$this->drupalGet('node/add/article');
$this->assertFieldByXPath("//input[@name='body_add_more']", t('Add another item'), 'Body field cardinality set to multiple.');
$body = array(
$this->randomName(),
$this->randomName(),
);
// Create an article with the first body input format set to "Full HTML".
$title = $this->randomName();
$edit = array(
'title' => $title,
'language' => 'en',
);
$this->drupalPost('node/add/article', $edit, t('Save'));
// Populate the body field: the first item gets the "Full HTML" input
// format, the second one "Filtered HTML".
$formats = array('full_html', 'filtered_html');
$langcode = LANGUAGE_NONE;
foreach ($body as $delta => $value) {
$edit = array(
"body[$langcode][$delta][value]" => $value,
"body[$langcode][$delta][format]" => array_shift($formats),
);
$this->drupalPost('node/1/edit', $edit, t('Save'));
$this->assertText($body[$delta], format_string('The body field with delta @delta has been saved.', array('@delta' => $delta)));
}
// Login as translator.
$this->drupalLogin($this->translator);
// Translate the article in french.
$node = $this->drupalGetNodeByTitle($title);
$this->drupalGet("node/$node->nid/translate");
$this->clickLink(t('add translation'));
$this->assertNoText($body[0], format_string('The body field with delta @delta is hidden.', array('@delta' => 0)));
$this->assertText($body[1], format_string('The body field with delta @delta is shown.', array('@delta' => 1)));
}
}
default_storage);
}
/**
* Generate random values for a field_test field.
*
* @param $cardinality
* Number of values to generate.
* @return
* An array of random values, in the format expected for field values.
*/
function _generateTestFieldValues($cardinality) {
$values = array();
for ($i = 0; $i < $cardinality; $i++) {
// field_test fields treat 0 as 'empty value'.
$values[$i]['value'] = mt_rand(1, 127);
}
return $values;
}
/**
* Assert that a field has the expected values in an entity.
*
* This function only checks a single column in the field values.
*
* @param $entity
* The entity to test.
* @param $field_name
* The name of the field to test
* @param $langcode
* The language code for the values.
* @param $expected_values
* The array of expected values.
* @param $column
* (Optional) the name of the column to check.
*/
function assertFieldValues($entity, $field_name, $langcode, $expected_values, $column = 'value') {
$e = clone $entity;
field_attach_load('test_entity', array($e->ftid => $e));
$values = isset($e->{$field_name}[$langcode]) ? $e->{$field_name}[$langcode] : array();
$this->assertEqual(count($values), count($expected_values), 'Expected number of values were saved.');
foreach ($expected_values as $key => $value) {
$this->assertEqual($values[$key][$column], $value, format_string('Value @value was saved correctly.', array('@value' => $value)));
}
}
}
class FieldAttachTestCase extends FieldTestCase {
protected $field_name;
protected $field;
protected $field_id;
protected $instance;
protected $field_name_2;
protected $field_2;
protected $field_id_2;
protected $instance_2;
function setUp() {
// Since this is a base class for many test cases, support the same
// flexibility that DrupalWebTestCase::setUp() has for the modules to be
// passed in as either an array or a variable number of string arguments.
$modules = func_get_args();
if (isset($modules[0]) && is_array($modules[0])) {
$modules = $modules[0];
}
if (!in_array('field_test', $modules)) {
$modules[] = 'field_test';
}
parent::setUp($modules);
$this->createFieldWithInstance();
}
/**
* Create a field and an instance of it.
*
* @param string $suffix
* (optional) A string that should only contain characters that are valid in
* PHP variable names as well.
*/
function createFieldWithInstance($suffix = '') {
$field_name = 'field_name' . $suffix;
$field = 'field' . $suffix;
$field_id = 'field_id' . $suffix;
$instance = 'instance' . $suffix;
$this->$field_name = drupal_strtolower($this->randomName() . '_field_name' . $suffix);
$this->$field = array('field_name' => $this->$field_name, 'type' => 'test_field', 'cardinality' => 4);
$this->$field = field_create_field($this->$field);
$this->$field_id = $this->{$field}['id'];
$this->$instance = array(
'field_name' => $this->$field_name,
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'label' => $this->randomName() . '_label',
'description' => $this->randomName() . '_description',
'weight' => mt_rand(0, 127),
'settings' => array(
'test_instance_setting' => $this->randomName(),
),
'widget' => array(
'type' => 'test_field_widget',
'label' => 'Test Field',
'settings' => array(
'test_widget_setting' => $this->randomName(),
)
)
);
field_create_instance($this->$instance);
}
}
/**
* Unit test class for storage-related field_attach_* functions.
*
* All field_attach_* test work with all field_storage plugins and
* all hook_field_attach_pre_{load,insert,update}() hooks.
*/
class FieldAttachStorageTestCase extends FieldAttachTestCase {
public static function getInfo() {
return array(
'name' => 'Field attach tests (storage-related)',
'description' => 'Test storage-related Field Attach API functions.',
'group' => 'Field API',
);
}
/**
* Check field values insert, update and load.
*
* Works independently of the underlying field storage backend. Inserts or
* updates random field data and then loads and verifies the data.
*/
function testFieldAttachSaveLoad() {
// Configure the instance so that we test hook_field_load() (see
// field_test_field_load() in field_test.module).
$this->instance['settings']['test_hook_field_load'] = TRUE;
field_update_instance($this->instance);
$langcode = LANGUAGE_NONE;
$entity_type = 'test_entity';
$values = array();
// TODO : test empty values filtering and "compression" (store consecutive deltas).
// Preparation: create three revisions and store them in $revision array.
for ($revision_id = 0; $revision_id < 3; $revision_id++) {
$revision[$revision_id] = field_test_create_stub_entity(0, $revision_id, $this->instance['bundle']);
// Note: we try to insert one extra value.
$values[$revision_id] = $this->_generateTestFieldValues($this->field['cardinality'] + 1);
$current_revision = $revision_id;
// If this is the first revision do an insert.
if (!$revision_id) {
$revision[$revision_id]->{$this->field_name}[$langcode] = $values[$revision_id];
field_attach_insert($entity_type, $revision[$revision_id]);
}
else {
// Otherwise do an update.
$revision[$revision_id]->{$this->field_name}[$langcode] = $values[$revision_id];
field_attach_update($entity_type, $revision[$revision_id]);
}
}
// Confirm current revision loads the correct data.
$entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
field_attach_load($entity_type, array(0 => $entity));
// Number of values per field loaded equals the field cardinality.
$this->assertEqual(count($entity->{$this->field_name}[$langcode]), $this->field['cardinality'], 'Current revision: expected number of values');
for ($delta = 0; $delta < $this->field['cardinality']; $delta++) {
// The field value loaded matches the one inserted or updated.
$this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['value'] , $values[$current_revision][$delta]['value'], format_string('Current revision: expected value %delta was found.', array('%delta' => $delta)));
// The value added in hook_field_load() is found.
$this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['additional_key'], 'additional_value', format_string('Current revision: extra information for value %delta was found', array('%delta' => $delta)));
}
// Confirm each revision loads the correct data.
foreach (array_keys($revision) as $revision_id) {
$entity = field_test_create_stub_entity(0, $revision_id, $this->instance['bundle']);
field_attach_load_revision($entity_type, array(0 => $entity));
// Number of values per field loaded equals the field cardinality.
$this->assertEqual(count($entity->{$this->field_name}[$langcode]), $this->field['cardinality'], format_string('Revision %revision_id: expected number of values.', array('%revision_id' => $revision_id)));
for ($delta = 0; $delta < $this->field['cardinality']; $delta++) {
// The field value loaded matches the one inserted or updated.
$this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['value'], $values[$revision_id][$delta]['value'], format_string('Revision %revision_id: expected value %delta was found.', array('%revision_id' => $revision_id, '%delta' => $delta)));
// The value added in hook_field_load() is found.
$this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['additional_key'], 'additional_value', format_string('Revision %revision_id: extra information for value %delta was found', array('%revision_id' => $revision_id, '%delta' => $delta)));
}
}
}
/**
* Test the 'multiple' load feature.
*/
function testFieldAttachLoadMultiple() {
$entity_type = 'test_entity';
$langcode = LANGUAGE_NONE;
// Define 2 bundles.
$bundles = array(
1 => 'test_bundle_1',
2 => 'test_bundle_2',
);
field_test_create_bundle($bundles[1]);
field_test_create_bundle($bundles[2]);
// Define 3 fields:
// - field_1 is in bundle_1 and bundle_2,
// - field_2 is in bundle_1,
// - field_3 is in bundle_2.
$field_bundles_map = array(
1 => array(1, 2),
2 => array(1),
3 => array(2),
);
for ($i = 1; $i <= 3; $i++) {
$field_names[$i] = 'field_' . $i;
$field = array('field_name' => $field_names[$i], 'type' => 'test_field');
$field = field_create_field($field);
$field_ids[$i] = $field['id'];
foreach ($field_bundles_map[$i] as $bundle) {
$instance = array(
'field_name' => $field_names[$i],
'entity_type' => 'test_entity',
'bundle' => $bundles[$bundle],
'settings' => array(
// Configure the instance so that we test hook_field_load()
// (see field_test_field_load() in field_test.module).
'test_hook_field_load' => TRUE,
),
);
field_create_instance($instance);
}
}
// Create one test entity per bundle, with random values.
foreach ($bundles as $index => $bundle) {
$entities[$index] = field_test_create_stub_entity($index, $index, $bundle);
$entity = clone($entities[$index]);
$instances = field_info_instances('test_entity', $bundle);
foreach ($instances as $field_name => $instance) {
$values[$index][$field_name] = mt_rand(1, 127);
$entity->$field_name = array($langcode => array(array('value' => $values[$index][$field_name])));
}
field_attach_insert($entity_type, $entity);
}
// Check that a single load correctly loads field values for both entities.
field_attach_load($entity_type, $entities);
foreach ($entities as $index => $entity) {
$instances = field_info_instances($entity_type, $bundles[$index]);
foreach ($instances as $field_name => $instance) {
// The field value loaded matches the one inserted.
$this->assertEqual($entity->{$field_name}[$langcode][0]['value'], $values[$index][$field_name], format_string('Entity %index: expected value was found.', array('%index' => $index)));
// The value added in hook_field_load() is found.
$this->assertEqual($entity->{$field_name}[$langcode][0]['additional_key'], 'additional_value', format_string('Entity %index: extra information was found', array('%index' => $index)));
}
}
// Check that the single-field load option works.
$entity = field_test_create_stub_entity(1, 1, $bundles[1]);
field_attach_load($entity_type, array(1 => $entity), FIELD_LOAD_CURRENT, array('field_id' => $field_ids[1]));
$this->assertEqual($entity->{$field_names[1]}[$langcode][0]['value'], $values[1][$field_names[1]], format_string('Entity %index: expected value was found.', array('%index' => 1)));
$this->assertEqual($entity->{$field_names[1]}[$langcode][0]['additional_key'], 'additional_value', format_string('Entity %index: extra information was found', array('%index' => 1)));
$this->assert(!isset($entity->{$field_names[2]}), format_string('Entity %index: field %field_name is not loaded.', array('%index' => 2, '%field_name' => $field_names[2])));
$this->assert(!isset($entity->{$field_names[3]}), format_string('Entity %index: field %field_name is not loaded.', array('%index' => 3, '%field_name' => $field_names[3])));
}
/**
* Test saving and loading fields using different storage backends.
*/
function testFieldAttachSaveLoadDifferentStorage() {
$entity_type = 'test_entity';
$langcode = LANGUAGE_NONE;
// Create two fields using different storage backends, and their instances.
$fields = array(
array(
'field_name' => 'field_1',
'type' => 'test_field',
'cardinality' => 4,
'storage' => array('type' => 'field_sql_storage')
),
array(
'field_name' => 'field_2',
'type' => 'test_field',
'cardinality' => 4,
'storage' => array('type' => 'field_test_storage')
),
);
foreach ($fields as $field) {
field_create_field($field);
$instance = array(
'field_name' => $field['field_name'],
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
);
field_create_instance($instance);
}
$entity_init = field_test_create_stub_entity();
// Create entity and insert random values.
$entity = clone($entity_init);
$values = array();
foreach ($fields as $field) {
$values[$field['field_name']] = $this->_generateTestFieldValues($this->field['cardinality']);
$entity->{$field['field_name']}[$langcode] = $values[$field['field_name']];
}
field_attach_insert($entity_type, $entity);
// Check that values are loaded as expected.
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
foreach ($fields as $field) {
$this->assertEqual($values[$field['field_name']], $entity->{$field['field_name']}[$langcode], format_string('%storage storage: expected values were found.', array('%storage' => $field['storage']['type'])));
}
}
/**
* Test storage details alteration.
*
* @see field_test_storage_details_alter()
*/
function testFieldStorageDetailsAlter() {
$field_name = 'field_test_change_my_details';
$field = array(
'field_name' => $field_name,
'type' => 'test_field',
'cardinality' => 4,
'storage' => array('type' => 'field_test_storage'),
);
$field = field_create_field($field);
$instance = array(
'field_name' => $field_name,
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
);
field_create_instance($instance);
$field = field_info_field($instance['field_name']);
$instance = field_info_instance($instance['entity_type'], $instance['field_name'], $instance['bundle']);
// The storage details are indexed by a storage engine type.
$this->assertTrue(array_key_exists('drupal_variables', $field['storage']['details']), 'The storage type is Drupal variables.');
$details = $field['storage']['details']['drupal_variables'];
// The field_test storage details are indexed by variable name. The details
// are altered, so moon and mars are correct for this test.
$this->assertTrue(array_key_exists('moon', $details[FIELD_LOAD_CURRENT]), 'Moon is available in the instance array.');
$this->assertTrue(array_key_exists('mars', $details[FIELD_LOAD_REVISION]), 'Mars is available in the instance array.');
// Test current and revision storage details together because the columns
// are the same.
foreach ((array) $field['columns'] as $column_name => $attributes) {
$this->assertEqual($details[FIELD_LOAD_CURRENT]['moon'][$column_name], $column_name, format_string('Column name %value matches the definition in %bin.', array('%value' => $column_name, '%bin' => 'moon[FIELD_LOAD_CURRENT]')));
$this->assertEqual($details[FIELD_LOAD_REVISION]['mars'][$column_name], $column_name, format_string('Column name %value matches the definition in %bin.', array('%value' => $column_name, '%bin' => 'mars[FIELD_LOAD_REVISION]')));
}
}
/**
* Tests insert and update with missing or NULL fields.
*/
function testFieldAttachSaveMissingData() {
$entity_type = 'test_entity';
$entity_init = field_test_create_stub_entity();
$langcode = LANGUAGE_NONE;
// Insert: Field is missing.
$entity = clone($entity_init);
field_attach_insert($entity_type, $entity);
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
$this->assertTrue(empty($entity->{$this->field_name}), 'Insert: missing field results in no value saved');
// Insert: Field is NULL.
field_cache_clear();
$entity = clone($entity_init);
$entity->{$this->field_name} = NULL;
field_attach_insert($entity_type, $entity);
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
$this->assertTrue(empty($entity->{$this->field_name}), 'Insert: NULL field results in no value saved');
// Add some real data.
field_cache_clear();
$entity = clone($entity_init);
$values = $this->_generateTestFieldValues(1);
$entity->{$this->field_name}[$langcode] = $values;
field_attach_insert($entity_type, $entity);
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
$this->assertEqual($entity->{$this->field_name}[$langcode], $values, 'Field data saved');
// Update: Field is missing. Data should survive.
field_cache_clear();
$entity = clone($entity_init);
field_attach_update($entity_type, $entity);
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
$this->assertEqual($entity->{$this->field_name}[$langcode], $values, 'Update: missing field leaves existing values in place');
// Update: Field is NULL. Data should be wiped.
field_cache_clear();
$entity = clone($entity_init);
$entity->{$this->field_name} = NULL;
field_attach_update($entity_type, $entity);
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
$this->assertTrue(empty($entity->{$this->field_name}), 'Update: NULL field removes existing values');
// Re-add some data.
field_cache_clear();
$entity = clone($entity_init);
$values = $this->_generateTestFieldValues(1);
$entity->{$this->field_name}[$langcode] = $values;
field_attach_update($entity_type, $entity);
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
$this->assertEqual($entity->{$this->field_name}[$langcode], $values, 'Field data saved');
// Update: Field is empty array. Data should be wiped.
field_cache_clear();
$entity = clone($entity_init);
$entity->{$this->field_name} = array();
field_attach_update($entity_type, $entity);
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
$this->assertTrue(empty($entity->{$this->field_name}), 'Update: empty array removes existing values');
}
/**
* Test insert with missing or NULL fields, with default value.
*/
function testFieldAttachSaveMissingDataDefaultValue() {
// Add a default value function.
$this->instance['default_value_function'] = 'field_test_default_value';
field_update_instance($this->instance);
$entity_type = 'test_entity';
$entity_init = field_test_create_stub_entity();
$langcode = LANGUAGE_NONE;
// Insert: Field is NULL.
$entity = clone($entity_init);
$entity->{$this->field_name}[$langcode] = NULL;
field_attach_insert($entity_type, $entity);
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
$this->assertTrue(empty($entity->{$this->field_name}[$langcode]), 'Insert: NULL field results in no value saved');
// Insert: Field is missing.
field_cache_clear();
$entity = clone($entity_init);
field_attach_insert($entity_type, $entity);
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
$values = field_test_default_value($entity_type, $entity, $this->field, $this->instance);
$this->assertEqual($entity->{$this->field_name}[$langcode], $values, 'Insert: missing field results in default value saved');
}
/**
* Test field_has_data().
*/
function testFieldHasData() {
$entity_type = 'test_entity';
$langcode = LANGUAGE_NONE;
$field_name = 'field_1';
$field = array('field_name' => $field_name, 'type' => 'test_field');
$field = field_create_field($field);
$this->assertFalse(field_has_data($field), "No data should be detected.");
$instance = array(
'field_name' => $field_name,
'entity_type' => 'test_entity',
'bundle' => 'test_bundle'
);
$instance = field_create_instance($instance);
$table = _field_sql_storage_tablename($field);
$revision_table = _field_sql_storage_revision_tablename($field);
$columns = array('entity_type', 'entity_id', 'revision_id', 'delta', 'language', $field_name . '_value');
$eid = 0;
// Insert values into the field revision table.
$query = db_insert($revision_table)->fields($columns);
$query->values(array($entity_type, $eid, 0, 0, $langcode, 1));
$query->execute();
$this->assertTrue(field_has_data($field), "Revision data only should be detected.");
$field_name = 'field_2';
$field = array('field_name' => $field_name, 'type' => 'test_field');
$field = field_create_field($field);
$this->assertFalse(field_has_data($field), "No data should be detected.");
$instance = array(
'field_name' => $field_name,
'entity_type' => 'test_entity',
'bundle' => 'test_bundle'
);
$instance = field_create_instance($instance);
$table = _field_sql_storage_tablename($field);
$revision_table = _field_sql_storage_revision_tablename($field);
$columns = array('entity_type', 'entity_id', 'revision_id', 'delta', 'language', $field_name . '_value');
$eid = 1;
// Insert values into the field table.
$query = db_insert($table)->fields($columns);
$query->values(array($entity_type, $eid, 0, 0, $langcode, 1));
$query->execute();
$this->assertTrue(field_has_data($field), "Values only in field table should be detected.");
}
/**
* Test field_attach_delete().
*/
function testFieldAttachDelete() {
$entity_type = 'test_entity';
$langcode = LANGUAGE_NONE;
$rev[0] = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
// Create revision 0
$values = $this->_generateTestFieldValues($this->field['cardinality']);
$rev[0]->{$this->field_name}[$langcode] = $values;
field_attach_insert($entity_type, $rev[0]);
// Create revision 1
$rev[1] = field_test_create_stub_entity(0, 1, $this->instance['bundle']);
$rev[1]->{$this->field_name}[$langcode] = $values;
field_attach_update($entity_type, $rev[1]);
// Create revision 2
$rev[2] = field_test_create_stub_entity(0, 2, $this->instance['bundle']);
$rev[2]->{$this->field_name}[$langcode] = $values;
field_attach_update($entity_type, $rev[2]);
// Confirm each revision loads
foreach (array_keys($rev) as $vid) {
$read = field_test_create_stub_entity(0, $vid, $this->instance['bundle']);
field_attach_load_revision($entity_type, array(0 => $read));
$this->assertEqual(count($read->{$this->field_name}[$langcode]), $this->field['cardinality'], "The test entity revision $vid has {$this->field['cardinality']} values.");
}
// Delete revision 1, confirm the other two still load.
field_attach_delete_revision($entity_type, $rev[1]);
foreach (array(0, 2) as $vid) {
$read = field_test_create_stub_entity(0, $vid, $this->instance['bundle']);
field_attach_load_revision($entity_type, array(0 => $read));
$this->assertEqual(count($read->{$this->field_name}[$langcode]), $this->field['cardinality'], "The test entity revision $vid has {$this->field['cardinality']} values.");
}
// Confirm the current revision still loads
$read = field_test_create_stub_entity(0, 2, $this->instance['bundle']);
field_attach_load($entity_type, array(0 => $read));
$this->assertEqual(count($read->{$this->field_name}[$langcode]), $this->field['cardinality'], "The test entity current revision has {$this->field['cardinality']} values.");
// Delete all field data, confirm nothing loads
field_attach_delete($entity_type, $rev[2]);
foreach (array(0, 1, 2) as $vid) {
$read = field_test_create_stub_entity(0, $vid, $this->instance['bundle']);
field_attach_load_revision($entity_type, array(0 => $read));
$this->assertIdentical($read->{$this->field_name}, array(), "The test entity revision $vid is deleted.");
}
$read = field_test_create_stub_entity(0, 2, $this->instance['bundle']);
field_attach_load($entity_type, array(0 => $read));
$this->assertIdentical($read->{$this->field_name}, array(), 'The test entity current revision is deleted.');
}
/**
* Test field_attach_create_bundle() and field_attach_rename_bundle().
*/
function testFieldAttachCreateRenameBundle() {
// Create a new bundle. This has to be initiated by the module so that its
// hook_entity_info() is consistent.
$new_bundle = 'test_bundle_' . drupal_strtolower($this->randomName());
field_test_create_bundle($new_bundle);
// Add an instance to that bundle.
$this->instance['bundle'] = $new_bundle;
field_create_instance($this->instance);
// Save an entity with data in the field.
$entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
$langcode = LANGUAGE_NONE;
$values = $this->_generateTestFieldValues($this->field['cardinality']);
$entity->{$this->field_name}[$langcode] = $values;
$entity_type = 'test_entity';
field_attach_insert($entity_type, $entity);
// Verify the field data is present on load.
$entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
field_attach_load($entity_type, array(0 => $entity));
$this->assertEqual(count($entity->{$this->field_name}[$langcode]), $this->field['cardinality'], "Data is retrieved for the new bundle");
// Rename the bundle. This has to be initiated by the module so that its
// hook_entity_info() is consistent.
$new_bundle = 'test_bundle_' . drupal_strtolower($this->randomName());
field_test_rename_bundle($this->instance['bundle'], $new_bundle);
// Check that the instance definition has been updated.
$this->instance = field_info_instance($entity_type, $this->field_name, $new_bundle);
$this->assertIdentical($this->instance['bundle'], $new_bundle, "Bundle name has been updated in the instance.");
// Verify the field data is present on load.
$entity = field_test_create_stub_entity(0, 0, $new_bundle);
field_attach_load($entity_type, array(0 => $entity));
$this->assertEqual(count($entity->{$this->field_name}[$langcode]), $this->field['cardinality'], "Bundle name has been updated in the field storage");
}
/**
* Test field_attach_delete_bundle().
*/
function testFieldAttachDeleteBundle() {
// Create a new bundle. This has to be initiated by the module so that its
// hook_entity_info() is consistent.
$new_bundle = 'test_bundle_' . drupal_strtolower($this->randomName());
field_test_create_bundle($new_bundle);
// Add an instance to that bundle.
$this->instance['bundle'] = $new_bundle;
field_create_instance($this->instance);
// Create a second field for the test bundle
$field_name = drupal_strtolower($this->randomName() . '_field_name');
$field = array('field_name' => $field_name, 'type' => 'test_field', 'cardinality' => 1);
field_create_field($field);
$instance = array(
'field_name' => $field_name,
'entity_type' => 'test_entity',
'bundle' => $this->instance['bundle'],
'label' => $this->randomName() . '_label',
'description' => $this->randomName() . '_description',
'weight' => mt_rand(0, 127),
// test_field has no instance settings
'widget' => array(
'type' => 'test_field_widget',
'settings' => array(
'size' => mt_rand(0, 255))));
field_create_instance($instance);
// Save an entity with data for both fields
$entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
$langcode = LANGUAGE_NONE;
$values = $this->_generateTestFieldValues($this->field['cardinality']);
$entity->{$this->field_name}[$langcode] = $values;
$entity->{$field_name}[$langcode] = $this->_generateTestFieldValues(1);
field_attach_insert('test_entity', $entity);
// Verify the fields are present on load
$entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
field_attach_load('test_entity', array(0 => $entity));
$this->assertEqual(count($entity->{$this->field_name}[$langcode]), 4, 'First field got loaded');
$this->assertEqual(count($entity->{$field_name}[$langcode]), 1, 'Second field got loaded');
// Delete the bundle. This has to be initiated by the module so that its
// hook_entity_info() is consistent.
field_test_delete_bundle($this->instance['bundle']);
// Verify no data gets loaded
$entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
field_attach_load('test_entity', array(0 => $entity));
$this->assertFalse(isset($entity->{$this->field_name}[$langcode]), 'No data for first field');
$this->assertFalse(isset($entity->{$field_name}[$langcode]), 'No data for second field');
// Verify that the instances are gone
$this->assertFalse(field_read_instance('test_entity', $this->field_name, $this->instance['bundle']), "First field is deleted");
$this->assertFalse(field_read_instance('test_entity', $field_name, $instance['bundle']), "Second field is deleted");
}
}
/**
* Unit test class for non-storage related field_attach_* functions.
*/
class FieldAttachOtherTestCase extends FieldAttachTestCase {
protected $instance2;
public static function getInfo() {
return array(
'name' => 'Field attach tests (other)',
'description' => 'Test other Field Attach API functions.',
'group' => 'Field API',
);
}
/**
* Test field_attach_view() and field_attach_prepare_view().
*/
function testFieldAttachView() {
$this->createFieldWithInstance('_2');
$entity_type = 'test_entity';
$entity_init = field_test_create_stub_entity();
$langcode = LANGUAGE_NONE;
$options = array('field_name' => $this->field_name_2);
// Populate values to be displayed.
$values = $this->_generateTestFieldValues($this->field['cardinality']);
$entity_init->{$this->field_name}[$langcode] = $values;
$values_2 = $this->_generateTestFieldValues($this->field_2['cardinality']);
$entity_init->{$this->field_name_2}[$langcode] = $values_2;
// Simple formatter, label displayed.
$entity = clone($entity_init);
$formatter_setting = $this->randomName();
$this->instance['display'] = array(
'full' => array(
'label' => 'above',
'type' => 'field_test_default',
'settings' => array(
'test_formatter_setting' => $formatter_setting,
)
),
);
field_update_instance($this->instance);
$formatter_setting_2 = $this->randomName();
$this->instance_2['display'] = array(
'full' => array(
'label' => 'above',
'type' => 'field_test_default',
'settings' => array(
'test_formatter_setting' => $formatter_setting_2,
)
),
);
field_update_instance($this->instance_2);
// View all fields.
field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full');
$entity->content = field_attach_view($entity_type, $entity, 'full');
$output = drupal_render($entity->content);
$this->content = $output;
$this->assertRaw($this->instance['label'], "First field's label is displayed.");
foreach ($values as $delta => $value) {
$this->content = $output;
$this->assertRaw("$formatter_setting|{$value['value']}", "Value $delta is displayed, formatter settings are applied.");
}
$this->assertRaw($this->instance_2['label'], "Second field's label is displayed.");
foreach ($values_2 as $delta => $value) {
$this->content = $output;
$this->assertRaw("$formatter_setting_2|{$value['value']}", "Value $delta is displayed, formatter settings are applied.");
}
// View single field (the second field).
field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full', $langcode, $options);
$entity->content = field_attach_view($entity_type, $entity, 'full', $langcode, $options);
$output = drupal_render($entity->content);
$this->content = $output;
$this->assertNoRaw($this->instance['label'], "First field's label is not displayed.");
foreach ($values as $delta => $value) {
$this->content = $output;
$this->assertNoRaw("$formatter_setting|{$value['value']}", "Value $delta is displayed, formatter settings are applied.");
}
$this->assertRaw($this->instance_2['label'], "Second field's label is displayed.");
foreach ($values_2 as $delta => $value) {
$this->content = $output;
$this->assertRaw("$formatter_setting_2|{$value['value']}", "Value $delta is displayed, formatter settings are applied.");
}
// Label hidden.
$entity = clone($entity_init);
$this->instance['display']['full']['label'] = 'hidden';
field_update_instance($this->instance);
field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full');
$entity->content = field_attach_view($entity_type, $entity, 'full');
$output = drupal_render($entity->content);
$this->content = $output;
$this->assertNoRaw($this->instance['label'], "Hidden label: label is not displayed.");
// Field hidden.
$entity = clone($entity_init);
$this->instance['display'] = array(
'full' => array(
'label' => 'above',
'type' => 'hidden',
),
);
field_update_instance($this->instance);
field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full');
$entity->content = field_attach_view($entity_type, $entity, 'full');
$output = drupal_render($entity->content);
$this->content = $output;
$this->assertNoRaw($this->instance['label'], "Hidden field: label is not displayed.");
foreach ($values as $delta => $value) {
$this->assertNoRaw("$formatter_setting|{$value['value']}", "Hidden field: value $delta is not displayed.");
}
// Multiple formatter.
$entity = clone($entity_init);
$formatter_setting = $this->randomName();
$this->instance['display'] = array(
'full' => array(
'label' => 'above',
'type' => 'field_test_multiple',
'settings' => array(
'test_formatter_setting_multiple' => $formatter_setting,
)
),
);
field_update_instance($this->instance);
field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full');
$entity->content = field_attach_view($entity_type, $entity, 'full');
$output = drupal_render($entity->content);
$display = $formatter_setting;
foreach ($values as $delta => $value) {
$display .= "|$delta:{$value['value']}";
}
$this->content = $output;
$this->assertRaw($display, "Multiple formatter: all values are displayed, formatter settings are applied.");
// Test a formatter that uses hook_field_formatter_prepare_view().
$entity = clone($entity_init);
$formatter_setting = $this->randomName();
$this->instance['display'] = array(
'full' => array(
'label' => 'above',
'type' => 'field_test_with_prepare_view',
'settings' => array(
'test_formatter_setting_additional' => $formatter_setting,
)
),
);
field_update_instance($this->instance);
field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full');
$entity->content = field_attach_view($entity_type, $entity, 'full');
$output = drupal_render($entity->content);
$this->content = $output;
foreach ($values as $delta => $value) {
$this->content = $output;
$expected = $formatter_setting . '|' . $value['value'] . '|' . ($value['value'] + 1);
$this->assertRaw($expected, "Value $delta is displayed, formatter settings are applied.");
}
// TODO:
// - check display order with several fields
// Preprocess template.
$variables = array();
field_attach_preprocess($entity_type, $entity, $entity->content, $variables);
$result = TRUE;
foreach ($values as $delta => $item) {
if ($variables[$this->field_name][$delta]['value'] !== $item['value']) {
$result = FALSE;
break;
}
}
$this->assertTrue($result, format_string('Variable $@field_name correctly populated.', array('@field_name' => $this->field_name)));
}
/**
* Tests the 'multiple entity' behavior of field_attach_prepare_view().
*/
function testFieldAttachPrepareViewMultiple() {
$entity_type = 'test_entity';
$langcode = LANGUAGE_NONE;
// Set the instance to be hidden.
$this->instance['display']['full']['type'] = 'hidden';
field_update_instance($this->instance);
// Set up a second instance on another bundle, with a formatter that uses
// hook_field_formatter_prepare_view().
field_test_create_bundle('test_bundle_2');
$formatter_setting = $this->randomName();
$this->instance2 = $this->instance;
$this->instance2['bundle'] = 'test_bundle_2';
$this->instance2['display']['full'] = array(
'type' => 'field_test_with_prepare_view',
'settings' => array(
'test_formatter_setting_additional' => $formatter_setting,
)
);
field_create_instance($this->instance2);
// Create one entity in each bundle.
$entity1_init = field_test_create_stub_entity(1, 1, 'test_bundle');
$values1 = $this->_generateTestFieldValues($this->field['cardinality']);
$entity1_init->{$this->field_name}[$langcode] = $values1;
$entity2_init = field_test_create_stub_entity(2, 2, 'test_bundle_2');
$values2 = $this->_generateTestFieldValues($this->field['cardinality']);
$entity2_init->{$this->field_name}[$langcode] = $values2;
// Run prepare_view, and check that the entities come out as expected.
$entity1 = clone($entity1_init);
$entity2 = clone($entity2_init);
field_attach_prepare_view($entity_type, array($entity1->ftid => $entity1, $entity2->ftid => $entity2), 'full');
$this->assertFalse(isset($entity1->{$this->field_name}[$langcode][0]['additional_formatter_value']), 'Entity 1 did not run through the prepare_view hook.');
$this->assertTrue(isset($entity2->{$this->field_name}[$langcode][0]['additional_formatter_value']), 'Entity 2 ran through the prepare_view hook.');
// Same thing, reversed order.
$entity1 = clone($entity1_init);
$entity2 = clone($entity2_init);
field_attach_prepare_view($entity_type, array($entity2->ftid => $entity2, $entity1->ftid => $entity1), 'full');
$this->assertFalse(isset($entity1->{$this->field_name}[$langcode][0]['additional_formatter_value']), 'Entity 1 did not run through the prepare_view hook.');
$this->assertTrue(isset($entity2->{$this->field_name}[$langcode][0]['additional_formatter_value']), 'Entity 2 ran through the prepare_view hook.');
}
/**
* Test field cache.
*/
function testFieldAttachCache() {
// Initialize random values and a test entity.
$entity_init = field_test_create_stub_entity(1, 1, $this->instance['bundle']);
$langcode = LANGUAGE_NONE;
$values = $this->_generateTestFieldValues($this->field['cardinality']);
// Non-cacheable entity type.
$entity_type = 'test_entity';
$cid = "field:$entity_type:{$entity_init->ftid}";
// Check that no initial cache entry is present.
$this->assertFalse(cache_get($cid, 'cache_field'), 'Non-cached: no initial cache entry');
// Save, and check that no cache entry is present.
$entity = clone($entity_init);
$entity->{$this->field_name}[$langcode] = $values;
field_attach_insert($entity_type, $entity);
$this->assertFalse(cache_get($cid, 'cache_field'), 'Non-cached: no cache entry on insert');
// Load, and check that no cache entry is present.
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
$this->assertFalse(cache_get($cid, 'cache_field'), 'Non-cached: no cache entry on load');
// Cacheable entity type.
$entity_type = 'test_cacheable_entity';
$cid = "field:$entity_type:{$entity_init->ftid}";
$instance = $this->instance;
$instance['entity_type'] = $entity_type;
field_create_instance($instance);
// Check that no initial cache entry is present.
$this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no initial cache entry');
// Save, and check that no cache entry is present.
$entity = clone($entity_init);
$entity->{$this->field_name}[$langcode] = $values;
field_attach_insert($entity_type, $entity);
$this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry on insert');
// Load a single field, and check that no cache entry is present.
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity), FIELD_LOAD_CURRENT, array('field_id' => $this->field_id));
$cache = cache_get($cid, 'cache_field');
$this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry on loading a single field');
// Load, and check that a cache entry is present with the expected values.
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
$cache = cache_get($cid, 'cache_field');
$this->assertEqual($cache->data[$this->field_name][$langcode], $values, 'Cached: correct cache entry on load');
// Update with different values, and check that the cache entry is wiped.
$values = $this->_generateTestFieldValues($this->field['cardinality']);
$entity = clone($entity_init);
$entity->{$this->field_name}[$langcode] = $values;
field_attach_update($entity_type, $entity);
$this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry on update');
// Load, and check that a cache entry is present with the expected values.
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
$cache = cache_get($cid, 'cache_field');
$this->assertEqual($cache->data[$this->field_name][$langcode], $values, 'Cached: correct cache entry on load');
// Create a new revision, and check that the cache entry is wiped.
$entity_init = field_test_create_stub_entity(1, 2, $this->instance['bundle']);
$values = $this->_generateTestFieldValues($this->field['cardinality']);
$entity = clone($entity_init);
$entity->{$this->field_name}[$langcode] = $values;
field_attach_update($entity_type, $entity);
$cache = cache_get($cid, 'cache_field');
$this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry on new revision creation');
// Load, and check that a cache entry is present with the expected values.
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
$cache = cache_get($cid, 'cache_field');
$this->assertEqual($cache->data[$this->field_name][$langcode], $values, 'Cached: correct cache entry on load');
// Delete, and check that the cache entry is wiped.
field_attach_delete($entity_type, $entity);
$this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry after delete');
}
/**
* Test field_attach_validate().
*
* Verify that field_attach_validate() invokes the correct
* hook_field_validate.
*/
function testFieldAttachValidate() {
$this->createFieldWithInstance('_2');
$entity_type = 'test_entity';
$entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
$langcode = LANGUAGE_NONE;
// Set up all but one values of the first field to generate errors.
$values = array();
for ($delta = 0; $delta < $this->field['cardinality']; $delta++) {
$values[$delta]['value'] = -1;
}
// Arrange for item 1 not to generate an error
$values[1]['value'] = 1;
$entity->{$this->field_name}[$langcode] = $values;
// Set up all values of the second field to generate errors.
$values_2 = array();
for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) {
$values_2[$delta]['value'] = -1;
}
$entity->{$this->field_name_2}[$langcode] = $values_2;
// Validate all fields.
try {
field_attach_validate($entity_type, $entity);
}
catch (FieldValidationException $e) {
$errors = $e->errors;
}
foreach ($values as $delta => $value) {
if ($value['value'] != 1) {
$this->assertIdentical($errors[$this->field_name][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on first field's value $delta");
$this->assertEqual(count($errors[$this->field_name][$langcode][$delta]), 1, "Only one error set on first field's value $delta");
unset($errors[$this->field_name][$langcode][$delta]);
}
else {
$this->assertFalse(isset($errors[$this->field_name][$langcode][$delta]), "No error set on first field's value $delta");
}
}
foreach ($values_2 as $delta => $value) {
$this->assertIdentical($errors[$this->field_name_2][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on second field's value $delta");
$this->assertEqual(count($errors[$this->field_name_2][$langcode][$delta]), 1, "Only one error set on second field's value $delta");
unset($errors[$this->field_name_2][$langcode][$delta]);
}
$this->assertEqual(count($errors[$this->field_name][$langcode]), 0, 'No extraneous errors set for first field');
$this->assertEqual(count($errors[$this->field_name_2][$langcode]), 0, 'No extraneous errors set for second field');
// Validate a single field.
$options = array('field_name' => $this->field_name_2);
try {
field_attach_validate($entity_type, $entity, $options);
}
catch (FieldValidationException $e) {
$errors = $e->errors;
}
foreach ($values_2 as $delta => $value) {
$this->assertIdentical($errors[$this->field_name_2][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on second field's value $delta");
$this->assertEqual(count($errors[$this->field_name_2][$langcode][$delta]), 1, "Only one error set on second field's value $delta");
unset($errors[$this->field_name_2][$langcode][$delta]);
}
$this->assertFalse(isset($errors[$this->field_name]), 'No validation errors are set for the first field, despite it having errors');
$this->assertEqual(count($errors[$this->field_name_2][$langcode]), 0, 'No extraneous errors set for second field');
// Check that cardinality is validated.
$entity->{$this->field_name_2}[$langcode] = $this->_generateTestFieldValues($this->field_2['cardinality'] + 1);
// When validating all fields.
try {
field_attach_validate($entity_type, $entity);
}
catch (FieldValidationException $e) {
$errors = $e->errors;
}
$this->assertEqual($errors[$this->field_name_2][$langcode][0][0]['error'], 'field_cardinality', 'Cardinality validation failed.');
// When validating a single field (the second field).
try {
field_attach_validate($entity_type, $entity, $options);
}
catch (FieldValidationException $e) {
$errors = $e->errors;
}
$this->assertEqual($errors[$this->field_name_2][$langcode][0][0]['error'], 'field_cardinality', 'Cardinality validation failed.');
}
/**
* Test field_attach_form().
*
* This could be much more thorough, but it does verify that the correct
* widgets show up.
*/
function testFieldAttachForm() {
$this->createFieldWithInstance('_2');
$entity_type = 'test_entity';
$entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
$langcode = LANGUAGE_NONE;
// When generating form for all fields.
$form = array();
$form_state = form_state_defaults();
field_attach_form($entity_type, $entity, $form, $form_state);
$this->assertEqual($form[$this->field_name][$langcode]['#title'], $this->instance['label'], "First field's form title is {$this->instance['label']}");
$this->assertEqual($form[$this->field_name_2][$langcode]['#title'], $this->instance_2['label'], "Second field's form title is {$this->instance_2['label']}");
for ($delta = 0; $delta < $this->field['cardinality']; $delta++) {
// field_test_widget uses 'textfield'
$this->assertEqual($form[$this->field_name][$langcode][$delta]['value']['#type'], 'textfield', "First field's form delta $delta widget is textfield");
}
for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) {
// field_test_widget uses 'textfield'
$this->assertEqual($form[$this->field_name_2][$langcode][$delta]['value']['#type'], 'textfield', "Second field's form delta $delta widget is textfield");
}
// When generating form for a single field (the second field).
$options = array('field_name' => $this->field_name_2);
$form = array();
$form_state = form_state_defaults();
field_attach_form($entity_type, $entity, $form, $form_state, NULL, $options);
$this->assertFalse(isset($form[$this->field_name]), 'The first field does not exist in the form');
$this->assertEqual($form[$this->field_name_2][$langcode]['#title'], $this->instance_2['label'], "Second field's form title is {$this->instance_2['label']}");
for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) {
// field_test_widget uses 'textfield'
$this->assertEqual($form[$this->field_name_2][$langcode][$delta]['value']['#type'], 'textfield', "Second field's form delta $delta widget is textfield");
}
}
/**
* Test field_attach_submit().
*/
function testFieldAttachSubmit() {
$this->createFieldWithInstance('_2');
$entity_type = 'test_entity';
$entity_init = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
$langcode = LANGUAGE_NONE;
// Build the form for all fields.
$form = array();
$form_state = form_state_defaults();
field_attach_form($entity_type, $entity_init, $form, $form_state);
// Simulate incoming values.
// First field.
$values = array();
$weights = array();
for ($delta = 0; $delta < $this->field['cardinality']; $delta++) {
$values[$delta]['value'] = mt_rand(1, 127);
// Assign random weight.
do {
$weight = mt_rand(0, $this->field['cardinality']);
} while (in_array($weight, $weights));
$weights[$delta] = $weight;
$values[$delta]['_weight'] = $weight;
}
// Leave an empty value. 'field_test' fields are empty if empty().
$values[1]['value'] = 0;
// Second field.
$values_2 = array();
$weights_2 = array();
for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) {
$values_2[$delta]['value'] = mt_rand(1, 127);
// Assign random weight.
do {
$weight = mt_rand(0, $this->field_2['cardinality']);
} while (in_array($weight, $weights_2));
$weights_2[$delta] = $weight;
$values_2[$delta]['_weight'] = $weight;
}
// Leave an empty value. 'field_test' fields are empty if empty().
$values_2[1]['value'] = 0;
// Pretend the form has been built.
drupal_prepare_form('field_test_entity_form', $form, $form_state);
drupal_process_form('field_test_entity_form', $form, $form_state);
$form_state['values'][$this->field_name][$langcode] = $values;
$form_state['values'][$this->field_name_2][$langcode] = $values_2;
// Call field_attach_submit() for all fields.
$entity = clone($entity_init);
field_attach_submit($entity_type, $entity, $form, $form_state);
asort($weights);
asort($weights_2);
$expected_values = array();
$expected_values_2 = array();
foreach ($weights as $key => $value) {
if ($key != 1) {
$expected_values[] = array('value' => $values[$key]['value']);
}
}
$this->assertIdentical($entity->{$this->field_name}[$langcode], $expected_values, 'Submit filters empty values');
foreach ($weights_2 as $key => $value) {
if ($key != 1) {
$expected_values_2[] = array('value' => $values_2[$key]['value']);
}
}
$this->assertIdentical($entity->{$this->field_name_2}[$langcode], $expected_values_2, 'Submit filters empty values');
// Call field_attach_submit() for a single field (the second field).
$options = array('field_name' => $this->field_name_2);
$entity = clone($entity_init);
field_attach_submit($entity_type, $entity, $form, $form_state, $options);
$expected_values_2 = array();
foreach ($weights_2 as $key => $value) {
if ($key != 1) {
$expected_values_2[] = array('value' => $values_2[$key]['value']);
}
}
$this->assertFalse(isset($entity->{$this->field_name}), 'The first field does not exist in the entity object');
$this->assertIdentical($entity->{$this->field_name_2}[$langcode], $expected_values_2, 'Submit filters empty values');
}
}
class FieldInfoTestCase extends FieldTestCase {
public static function getInfo() {
return array(
'name' => 'Field info tests',
'description' => 'Get information about existing fields, instances and bundles.',
'group' => 'Field API',
);
}
function setUp() {
parent::setUp('field_test');
}
/**
* Test that field types and field definitions are correcly cached.
*/
function testFieldInfo() {
// Test that field_test module's fields, widgets, and formatters show up.
$field_test_info = field_test_field_info();
// We need to account for the existence of user_field_info_alter().
foreach (array_keys($field_test_info) as $name) {
$field_test_info[$name]['instance_settings']['user_register_form'] = FALSE;
}
$info = field_info_field_types();
foreach ($field_test_info as $t_key => $field_type) {
foreach ($field_type as $key => $val) {
$this->assertEqual($info[$t_key][$key], $val, format_string('Field type %t_key key %key is %value', array('%t_key' => $t_key, '%key' => $key, '%value' => print_r($val, TRUE))));
}
$this->assertEqual($info[$t_key]['module'], 'field_test', "Field type field_test module appears");
}
$formatter_info = field_test_field_formatter_info();
$info = field_info_formatter_types();
foreach ($formatter_info as $f_key => $formatter) {
foreach ($formatter as $key => $val) {
$this->assertEqual($info[$f_key][$key], $val, format_string('Formatter type %f_key key %key is %value', array('%f_key' => $f_key, '%key' => $key, '%value' => print_r($val, TRUE))));
}
$this->assertEqual($info[$f_key]['module'], 'field_test', "Formatter type field_test module appears");
}
$widget_info = field_test_field_widget_info();
$info = field_info_widget_types();
foreach ($widget_info as $w_key => $widget) {
foreach ($widget as $key => $val) {
$this->assertEqual($info[$w_key][$key], $val, format_string('Widget type %w_key key %key is %value', array('%w_key' => $w_key, '%key' => $key, '%value' => print_r($val, TRUE))));
}
$this->assertEqual($info[$w_key]['module'], 'field_test', "Widget type field_test module appears");
}
$storage_info = field_test_field_storage_info();
$info = field_info_storage_types();
foreach ($storage_info as $s_key => $storage) {
foreach ($storage as $key => $val) {
$this->assertEqual($info[$s_key][$key], $val, format_string('Storage type %s_key key %key is %value', array('%s_key' => $s_key, '%key' => $key, '%value' => print_r($val, TRUE))));
}
$this->assertEqual($info[$s_key]['module'], 'field_test', "Storage type field_test module appears");
}
// Verify that no unexpected instances exist.
$instances = field_info_instances('test_entity');
$expected = array('test_bundle' => array());
$this->assertIdentical($instances, $expected, format_string("field_info_instances('test_entity') returns %expected.", array('%expected' => var_export($expected, TRUE))));
$instances = field_info_instances('test_entity', 'test_bundle');
$this->assertIdentical($instances, array(), "field_info_instances('test_entity', 'test_bundle') returns an empty array.");
// Create a field, verify it shows up.
$core_fields = field_info_fields();
$field = array(
'field_name' => drupal_strtolower($this->randomName()),
'type' => 'test_field',
);
field_create_field($field);
$fields = field_info_fields();
$this->assertEqual(count($fields), count($core_fields) + 1, 'One new field exists');
$this->assertEqual($fields[$field['field_name']]['field_name'], $field['field_name'], 'info fields contains field name');
$this->assertEqual($fields[$field['field_name']]['type'], $field['type'], 'info fields contains field type');
$this->assertEqual($fields[$field['field_name']]['module'], 'field_test', 'info fields contains field module');
$settings = array('test_field_setting' => 'dummy test string');
foreach ($settings as $key => $val) {
$this->assertEqual($fields[$field['field_name']]['settings'][$key], $val, format_string('Field setting %key has correct default value %value', array('%key' => $key, '%value' => $val)));
}
$this->assertEqual($fields[$field['field_name']]['cardinality'], 1, 'info fields contains cardinality 1');
$this->assertEqual($fields[$field['field_name']]['active'], 1, 'info fields contains active 1');
// Create an instance, verify that it shows up
$instance = array(
'field_name' => $field['field_name'],
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'label' => $this->randomName(),
'description' => $this->randomName(),
'weight' => mt_rand(0, 127),
// test_field has no instance settings
'widget' => array(
'type' => 'test_field_widget',
'settings' => array(
'test_setting' => 999)));
field_create_instance($instance);
$info = entity_get_info('test_entity');
$instances = field_info_instances('test_entity', $instance['bundle']);
$this->assertEqual(count($instances), 1, format_string('One instance shows up in info when attached to a bundle on a @label.', array(
'@label' => $info['label']
)));
$this->assertTrue($instance < $instances[$instance['field_name']], 'Instance appears in info correctly');
// Test a valid entity type but an invalid bundle.
$instances = field_info_instances('test_entity', 'invalid_bundle');
$this->assertIdentical($instances, array(), "field_info_instances('test_entity', 'invalid_bundle') returns an empty array.");
// Test invalid entity type and bundle.
$instances = field_info_instances('invalid_entity', $instance['bundle']);
$this->assertIdentical($instances, array(), "field_info_instances('invalid_entity', 'test_bundle') returns an empty array.");
// Test invalid entity type, no bundle provided.
$instances = field_info_instances('invalid_entity');
$this->assertIdentical($instances, array(), "field_info_instances('invalid_entity') returns an empty array.");
// Test with an entity type that has no bundles.
$instances = field_info_instances('user');
$expected = array('user' => array());
$this->assertIdentical($instances, $expected, format_string("field_info_instances('user') returns %expected.", array('%expected' => var_export($expected, TRUE))));
$instances = field_info_instances('user', 'user');
$this->assertIdentical($instances, array(), "field_info_instances('user', 'user') returns an empty array.");
// Test that querying for invalid entity types does not add entries in the
// list returned by field_info_instances().
field_info_cache_clear();
field_info_instances('invalid_entity', 'invalid_bundle');
// Simulate new request by clearing static caches.
drupal_static_reset();
field_info_instances('invalid_entity', 'invalid_bundle');
$instances = field_info_instances();
$this->assertFalse(isset($instances['invalid_entity']), 'field_info_instances() does not contain entries for the invalid entity type that was queried before');
}
/**
* Test that cached field definitions are ready for current runtime context.
*/
function testFieldPrepare() {
$field_definition = array(
'field_name' => 'field',
'type' => 'test_field',
);
field_create_field($field_definition);
// Simulate a stored field definition missing a field setting (e.g. a
// third-party module adding a new field setting has been enabled, and
// existing fields do not know the setting yet).
$data = db_query('SELECT data FROM {field_config} WHERE field_name = :field_name', array(':field_name' => $field_definition['field_name']))->fetchField();
$data = unserialize($data);
$data['settings'] = array();
db_update('field_config')
->fields(array('data' => serialize($data)))
->condition('field_name', $field_definition['field_name'])
->execute();
field_cache_clear();
// Read the field back.
$field = field_info_field($field_definition['field_name']);
// Check that all expected settings are in place.
$field_type = field_info_field_types($field_definition['type']);
$this->assertIdentical($field['settings'], $field_type['settings'], 'All expected default field settings are present.');
}
/**
* Test that cached instance definitions are ready for current runtime context.
*/
function testInstancePrepare() {
$field_definition = array(
'field_name' => 'field',
'type' => 'test_field',
);
field_create_field($field_definition);
$instance_definition = array(
'field_name' => $field_definition['field_name'],
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
);
field_create_instance($instance_definition);
// Simulate a stored instance definition missing various settings (e.g. a
// third-party module adding instance, widget or display settings has been
// enabled, but existing instances do not know the new settings).
$data = db_query('SELECT data FROM {field_config_instance} WHERE field_name = :field_name AND bundle = :bundle', array(':field_name' => $instance_definition['field_name'], ':bundle' => $instance_definition['bundle']))->fetchField();
$data = unserialize($data);
$data['settings'] = array();
$data['widget']['settings'] = 'unavailable_widget';
$data['widget']['settings'] = array();
$data['display']['default']['type'] = 'unavailable_formatter';
$data['display']['default']['settings'] = array();
db_update('field_config_instance')
->fields(array('data' => serialize($data)))
->condition('field_name', $instance_definition['field_name'])
->condition('bundle', $instance_definition['bundle'])
->execute();
field_cache_clear();
// Read the instance back.
$instance = field_info_instance($instance_definition['entity_type'], $instance_definition['field_name'], $instance_definition['bundle']);
// Check that all expected instance settings are in place.
$field_type = field_info_field_types($field_definition['type']);
$this->assertIdentical($instance['settings'], $field_type['instance_settings'] , 'All expected instance settings are present.');
// Check that the default widget is used and expected settings are in place.
$this->assertIdentical($instance['widget']['type'], $field_type['default_widget'], 'Unavailable widget replaced with default widget.');
$widget_type = field_info_widget_types($instance['widget']['type']);
$this->assertIdentical($instance['widget']['settings'], $widget_type['settings'] , 'All expected widget settings are present.');
// Check that display settings are set for the 'default' mode.
$display = $instance['display']['default'];
$this->assertIdentical($display['type'], $field_type['default_formatter'], "Formatter is set for the 'default' view mode");
$formatter_type = field_info_formatter_types($display['type']);
$this->assertIdentical($display['settings'], $formatter_type['settings'] , "Formatter settings are set for the 'default' view mode");
}
/**
* Test that instances on disabled entity types are filtered out.
*/
function testInstanceDisabledEntityType() {
// For this test the field type and the entity type must be exposed by
// different modules.
$field_definition = array(
'field_name' => 'field',
'type' => 'test_field',
);
field_create_field($field_definition);
$instance_definition = array(
'field_name' => 'field',
'entity_type' => 'comment',
'bundle' => 'comment_node_article',
);
field_create_instance($instance_definition);
// Disable coment module. This clears field_info cache.
module_disable(array('comment'));
$this->assertNull(field_info_instance('comment', 'field', 'comment_node_article'), 'No instances are returned on disabled entity types.');
}
/**
* Test field_info_field_map().
*/
function testFieldMap() {
// We will overlook fields created by the 'standard' install profile.
$exclude = field_info_field_map();
// Create a new bundle for 'test_entity' entity type.
field_test_create_bundle('test_bundle_2');
// Create a couple fields.
$fields = array(
array(
'field_name' => 'field_1',
'type' => 'test_field',
),
array(
'field_name' => 'field_2',
'type' => 'hidden_test_field',
),
);
foreach ($fields as $field) {
field_create_field($field);
}
// Create a couple instances.
$instances = array(
array(
'field_name' => 'field_1',
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
),
array(
'field_name' => 'field_1',
'entity_type' => 'test_entity',
'bundle' => 'test_bundle_2',
),
array(
'field_name' => 'field_2',
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
),
array(
'field_name' => 'field_2',
'entity_type' => 'test_cacheable_entity',
'bundle' => 'test_bundle',
),
);
foreach ($instances as $instance) {
field_create_instance($instance);
}
$expected = array(
'field_1' => array(
'type' => 'test_field',
'bundles' => array(
'test_entity' => array('test_bundle', 'test_bundle_2'),
),
),
'field_2' => array(
'type' => 'hidden_test_field',
'bundles' => array(
'test_entity' => array('test_bundle'),
'test_cacheable_entity' => array('test_bundle'),
),
),
);
// Check that the field map is correct.
$map = field_info_field_map();
$map = array_diff_key($map, $exclude);
$this->assertEqual($map, $expected);
}
/**
* Test that the field_info settings convenience functions work.
*/
function testSettingsInfo() {
$info = field_test_field_info();
// We need to account for the existence of user_field_info_alter().
foreach (array_keys($info) as $name) {
$info[$name]['instance_settings']['user_register_form'] = FALSE;
}
foreach ($info as $type => $data) {
$this->assertIdentical(field_info_field_settings($type), $data['settings'], format_string("field_info_field_settings returns %type's field settings", array('%type' => $type)));
$this->assertIdentical(field_info_instance_settings($type), $data['instance_settings'], format_string("field_info_field_settings returns %type's field instance settings", array('%type' => $type)));
}
$info = field_test_field_widget_info();
foreach ($info as $type => $data) {
$this->assertIdentical(field_info_widget_settings($type), $data['settings'], format_string("field_info_widget_settings returns %type's widget settings", array('%type' => $type)));
}
$info = field_test_field_formatter_info();
foreach ($info as $type => $data) {
$this->assertIdentical(field_info_formatter_settings($type), $data['settings'], format_string("field_info_formatter_settings returns %type's formatter settings", array('%type' => $type)));
}
}
/**
* Tests that the field info cache can be built correctly.
*/
function testFieldInfoCache() {
// Create a test field and ensure it's in the array returned by
// field_info_fields().
$field_name = drupal_strtolower($this->randomName());
$field = array(
'field_name' => $field_name,
'type' => 'test_field',
);
field_create_field($field);
$fields = field_info_fields();
$this->assertTrue(isset($fields[$field_name]), 'The test field is initially found in the array returned by field_info_fields().');
// Now rebuild the field info cache, and set a variable which will cause
// the cache to be cleared while it's being rebuilt; see
// field_test_entity_info(). Ensure the test field is still in the returned
// array.
field_info_cache_clear();
variable_set('field_test_clear_info_cache_in_hook_entity_info', TRUE);
$fields = field_info_fields();
$this->assertTrue(isset($fields[$field_name]), 'The test field is found in the array returned by field_info_fields() even if its cache is cleared while being rebuilt.');
}
}
class FieldFormTestCase extends FieldTestCase {
protected $field_single;
protected $field_multiple;
protected $field_unlimited;
protected $field;
protected $field_name;
protected $instance;
public static function getInfo() {
return array(
'name' => 'Field form tests',
'description' => 'Test Field form handling.',
'group' => 'Field API',
);
}
function setUp() {
parent::setUp('field_test');
$web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content'));
$this->drupalLogin($web_user);
$this->field_single = array('field_name' => 'field_single', 'type' => 'test_field');
$this->field_multiple = array('field_name' => 'field_multiple', 'type' => 'test_field', 'cardinality' => 4);
$this->field_unlimited = array('field_name' => 'field_unlimited', 'type' => 'test_field', 'cardinality' => FIELD_CARDINALITY_UNLIMITED);
$this->instance = array(
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'label' => $this->randomName() . '_label',
'description' => $this->randomName() . '_description',
'weight' => mt_rand(0, 127),
'settings' => array(
'test_instance_setting' => $this->randomName(),
),
'widget' => array(
'type' => 'test_field_widget',
'label' => 'Test Field',
'settings' => array(
'test_widget_setting' => $this->randomName(),
)
)
);
}
function testFieldFormSingle() {
$this->field = $this->field_single;
$this->field_name = $this->field['field_name'];
$this->instance['field_name'] = $this->field_name;
field_create_field($this->field);
field_create_instance($this->instance);
$langcode = LANGUAGE_NONE;
// Display creation form.
$this->drupalGet('test-entity/add/test-bundle');
$this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget is displayed');
$this->assertNoField("{$this->field_name}[$langcode][1][value]", 'No extraneous widget is displayed');
// TODO : check that the widget is populated with default value ?
// Submit with invalid value (field-level validation).
$edit = array("{$this->field_name}[$langcode][0][value]" => -1);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertRaw(t('%name does not accept the value -1.', array('%name' => $this->instance['label'])), 'Field validation fails with invalid input.');
// TODO : check that the correct field is flagged for error.
// Create an entity
$value = mt_rand(1, 127);
$edit = array("{$this->field_name}[$langcode][0][value]" => $value);
$this->drupalPost(NULL, $edit, t('Save'));
preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
$id = $match[1];
$this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created');
$entity = field_test_entity_test_load($id);
$this->assertEqual($entity->{$this->field_name}[$langcode][0]['value'], $value, 'Field value was saved');
// Display edit form.
$this->drupalGet('test-entity/manage/' . $id . '/edit');
$this->assertFieldByName("{$this->field_name}[$langcode][0][value]", $value, 'Widget is displayed with the correct default value');
$this->assertNoField("{$this->field_name}[$langcode][1][value]", 'No extraneous widget is displayed');
// Update the entity.
$value = mt_rand(1, 127);
$edit = array("{$this->field_name}[$langcode][0][value]" => $value);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)), 'Entity was updated');
$entity = field_test_entity_test_load($id);
$this->assertEqual($entity->{$this->field_name}[$langcode][0]['value'], $value, 'Field value was updated');
// Empty the field.
$value = '';
$edit = array("{$this->field_name}[$langcode][0][value]" => $value);
$this->drupalPost('test-entity/manage/' . $id . '/edit', $edit, t('Save'));
$this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)), 'Entity was updated');
$entity = field_test_entity_test_load($id);
$this->assertIdentical($entity->{$this->field_name}, array(), 'Field was emptied');
}
function testFieldFormSingleRequired() {
$this->field = $this->field_single;
$this->field_name = $this->field['field_name'];
$this->instance['field_name'] = $this->field_name;
$this->instance['required'] = TRUE;
field_create_field($this->field);
field_create_instance($this->instance);
$langcode = LANGUAGE_NONE;
// Submit with missing required value.
$edit = array();
$this->drupalPost('test-entity/add/test-bundle', $edit, t('Save'));
$this->assertRaw(t('!name field is required.', array('!name' => $this->instance['label'])), 'Required field with no value fails validation');
// Create an entity
$value = mt_rand(1, 127);
$edit = array("{$this->field_name}[$langcode][0][value]" => $value);
$this->drupalPost(NULL, $edit, t('Save'));
preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
$id = $match[1];
$this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created');
$entity = field_test_entity_test_load($id);
$this->assertEqual($entity->{$this->field_name}[$langcode][0]['value'], $value, 'Field value was saved');
// Edit with missing required value.
$value = '';
$edit = array("{$this->field_name}[$langcode][0][value]" => $value);
$this->drupalPost('test-entity/manage/' . $id . '/edit', $edit, t('Save'));
$this->assertRaw(t('!name field is required.', array('!name' => $this->instance['label'])), 'Required field with no value fails validation');
}
// function testFieldFormMultiple() {
// $this->field = $this->field_multiple;
// $this->field_name = $this->field['field_name'];
// $this->instance['field_name'] = $this->field_name;
// field_create_field($this->field);
// field_create_instance($this->instance);
// }
function testFieldFormUnlimited() {
$this->field = $this->field_unlimited;
$this->field_name = $this->field['field_name'];
$this->instance['field_name'] = $this->field_name;
field_create_field($this->field);
field_create_instance($this->instance);
$langcode = LANGUAGE_NONE;
// Display creation form -> 1 widget.
$this->drupalGet('test-entity/add/test-bundle');
$this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget 1 is displayed');
$this->assertNoField("{$this->field_name}[$langcode][1][value]", 'No extraneous widget is displayed');
// Press 'add more' button -> 2 widgets.
$this->drupalPost(NULL, array(), t('Add another item'));
$this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget 1 is displayed');
$this->assertFieldByName("{$this->field_name}[$langcode][1][value]", '', 'New widget is displayed');
$this->assertNoField("{$this->field_name}[$langcode][2][value]", 'No extraneous widget is displayed');
// TODO : check that non-field inpurs are preserved ('title')...
// Yet another time so that we can play with more values -> 3 widgets.
$this->drupalPost(NULL, array(), t('Add another item'));
// Prepare values and weights.
$count = 3;
$delta_range = $count - 1;
$values = $weights = $pattern = $expected_values = $edit = array();
for ($delta = 0; $delta <= $delta_range; $delta++) {
// Assign unique random values and weights.
do {
$value = mt_rand(1, 127);
} while (in_array($value, $values));
do {
$weight = mt_rand(-$delta_range, $delta_range);
} while (in_array($weight, $weights));
$edit["$this->field_name[$langcode][$delta][value]"] = $value;
$edit["$this->field_name[$langcode][$delta][_weight]"] = $weight;
// We'll need three slightly different formats to check the values.
$values[$delta] = $value;
$weights[$delta] = $weight;
$field_values[$weight]['value'] = (string) $value;
$pattern[$weight] = " ]*value=\"$value\" [^>]*";
}
// Press 'add more' button -> 4 widgets
$this->drupalPost(NULL, $edit, t('Add another item'));
for ($delta = 0; $delta <= $delta_range; $delta++) {
$this->assertFieldByName("$this->field_name[$langcode][$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value");
$this->assertFieldByName("$this->field_name[$langcode][$delta][_weight]", $weights[$delta], "Widget $delta has the right weight");
}
ksort($pattern);
$pattern = implode('.*', array_values($pattern));
$this->assertPattern("|$pattern|s", 'Widgets are displayed in the correct order');
$this->assertFieldByName("$this->field_name[$langcode][$delta][value]", '', "New widget is displayed");
$this->assertFieldByName("$this->field_name[$langcode][$delta][_weight]", $delta, "New widget has the right weight");
$this->assertNoField("$this->field_name[$langcode][" . ($delta + 1) . '][value]', 'No extraneous widget is displayed');
// Submit the form and create the entity.
$this->drupalPost(NULL, $edit, t('Save'));
preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
$id = $match[1];
$this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created');
$entity = field_test_entity_test_load($id);
ksort($field_values);
$field_values = array_values($field_values);
$this->assertIdentical($entity->{$this->field_name}[$langcode], $field_values, 'Field values were saved in the correct order');
// Display edit form: check that the expected number of widgets is
// displayed, with correct values change values, reorder, leave an empty
// value in the middle.
// Submit: check that the entity is updated with correct values
// Re-submit: check that the field can be emptied.
// Test with several multiple fields in a form
}
/**
* Tests the position of the required label.
*/
public function testFieldFormUnlimitedRequired() {
$this->field = $this->field_unlimited;
$this->field_name = $this->field['field_name'];
$this->instance['field_name'] = $this->field_name;
$this->instance['required'] = TRUE;
field_create_field($this->field);
field_create_instance($this->instance);
// Display creation form -> 1 widget.
$this->drupalGet('test-entity/add/test-bundle');
$result = $this->xpath("//label[not(contains(@class, 'element-invisible')) and contains(text(), :label)]/span/text()", array(':label' => $this->instance['label']));
$this->assertEqual($result[0], '*', 'Required symbol added to field label.');
// Check that the label of the field input is visually hidden and contains
// the field title and an indication of the delta for a11y.
$result = $this->xpath("//label[contains(@class, 'element-invisible') and contains(text(), :label)]/span/text()", array(':label' => $this->instance['label']));
$this->assertEqual($result[0], '*', 'Required symbol and field label are visually hidden.');
}
/**
* Tests widget handling of multiple required radios.
*/
function testFieldFormMultivalueWithRequiredRadio() {
// Create a multivalue test field.
$this->field = $this->field_unlimited;
$this->field_name = $this->field['field_name'];
$this->instance['field_name'] = $this->field_name;
field_create_field($this->field);
field_create_instance($this->instance);
$langcode = LANGUAGE_NONE;
// Add a required radio field.
field_create_field(array(
'field_name' => 'required_radio_test',
'type' => 'list_text',
'settings' => array(
'allowed_values' => array('yes' => 'yes', 'no' => 'no'),
),
));
field_create_instance(array(
'field_name' => 'required_radio_test',
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'required' => TRUE,
'widget' => array(
'type' => 'options_buttons',
),
));
// Display creation form.
$this->drupalGet('test-entity/add/test-bundle');
// Press the 'Add more' button.
$this->drupalPost(NULL, array(), t('Add another item'));
// Verify that no error is thrown by the radio element.
$this->assertNoFieldByXpath('//div[contains(@class, "error")]', FALSE, 'No error message is displayed.');
// Verify that the widget is added.
$this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget 1 is displayed');
$this->assertFieldByName("{$this->field_name}[$langcode][1][value]", '', 'New widget is displayed');
$this->assertNoField("{$this->field_name}[$langcode][2][value]", 'No extraneous widget is displayed');
}
function testFieldFormJSAddMore() {
$this->field = $this->field_unlimited;
$this->field_name = $this->field['field_name'];
$this->instance['field_name'] = $this->field_name;
field_create_field($this->field);
field_create_instance($this->instance);
$langcode = LANGUAGE_NONE;
// Display creation form -> 1 widget.
$this->drupalGet('test-entity/add/test-bundle');
// Press 'add more' button a couple times -> 3 widgets.
// drupalPostAJAX() will not work iteratively, so we add those through
// non-JS submission.
$this->drupalPost(NULL, array(), t('Add another item'));
$this->drupalPost(NULL, array(), t('Add another item'));
// Prepare values and weights.
$count = 3;
$delta_range = $count - 1;
$values = $weights = $pattern = $expected_values = $edit = array();
for ($delta = 0; $delta <= $delta_range; $delta++) {
// Assign unique random values and weights.
do {
$value = mt_rand(1, 127);
} while (in_array($value, $values));
do {
$weight = mt_rand(-$delta_range, $delta_range);
} while (in_array($weight, $weights));
$edit["$this->field_name[$langcode][$delta][value]"] = $value;
$edit["$this->field_name[$langcode][$delta][_weight]"] = $weight;
// We'll need three slightly different formats to check the values.
$values[$delta] = $value;
$weights[$delta] = $weight;
$field_values[$weight]['value'] = (string) $value;
$pattern[$weight] = " ]*value=\"$value\" [^>]*";
}
// Press 'add more' button through Ajax, and place the expected HTML result
// as the tested content.
$commands = $this->drupalPostAJAX(NULL, $edit, $this->field_name . '_add_more');
$this->content = $commands[1]['data'];
for ($delta = 0; $delta <= $delta_range; $delta++) {
$this->assertFieldByName("$this->field_name[$langcode][$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value");
$this->assertFieldByName("$this->field_name[$langcode][$delta][_weight]", $weights[$delta], "Widget $delta has the right weight");
}
ksort($pattern);
$pattern = implode('.*', array_values($pattern));
$this->assertPattern("|$pattern|s", 'Widgets are displayed in the correct order');
$this->assertFieldByName("$this->field_name[$langcode][$delta][value]", '', "New widget is displayed");
$this->assertFieldByName("$this->field_name[$langcode][$delta][_weight]", $delta, "New widget has the right weight");
$this->assertNoField("$this->field_name[$langcode][" . ($delta + 1) . '][value]', 'No extraneous widget is displayed');
}
/**
* Tests widgets handling multiple values.
*/
function testFieldFormMultipleWidget() {
// Create a field with fixed cardinality and an instance using a multiple
// widget.
$this->field = $this->field_multiple;
$this->field_name = $this->field['field_name'];
$this->instance['field_name'] = $this->field_name;
$this->instance['widget']['type'] = 'test_field_widget_multiple';
field_create_field($this->field);
field_create_instance($this->instance);
$langcode = LANGUAGE_NONE;
// Display creation form.
$this->drupalGet('test-entity/add/test-bundle');
$this->assertFieldByName("{$this->field_name}[$langcode]", '', 'Widget is displayed.');
// Create entity with three values.
$edit = array("{$this->field_name}[$langcode]" => '1, 2, 3');
$this->drupalPost(NULL, $edit, t('Save'));
preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
$id = $match[1];
// Check that the values were saved.
$entity_init = field_test_create_stub_entity($id);
$this->assertFieldValues($entity_init, $this->field_name, $langcode, array(1, 2, 3));
// Display the form, check that the values are correctly filled in.
$this->drupalGet('test-entity/manage/' . $id . '/edit');
$this->assertFieldByName("{$this->field_name}[$langcode]", '1, 2, 3', 'Widget is displayed.');
// Submit the form with more values than the field accepts.
$edit = array("{$this->field_name}[$langcode]" => '1, 2, 3, 4, 5');
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertRaw('this field cannot hold more than 4 values', 'Form validation failed.');
// Check that the field values were not submitted.
$this->assertFieldValues($entity_init, $this->field_name, $langcode, array(1, 2, 3));
}
/**
* Tests fields with no 'edit' access.
*/
function testFieldFormAccess() {
// Create a "regular" field.
$field = $this->field_single;
$field_name = $field['field_name'];
$instance = $this->instance;
$instance['field_name'] = $field_name;
field_create_field($field);
field_create_instance($instance);
// Create a field with no edit access - see field_test_field_access().
$field_no_access = array(
'field_name' => 'field_no_edit_access',
'type' => 'test_field',
);
$field_name_no_access = $field_no_access['field_name'];
$instance_no_access = array(
'field_name' => $field_name_no_access,
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'default_value' => array(0 => array('value' => 99)),
);
field_create_field($field_no_access);
field_create_instance($instance_no_access);
$langcode = LANGUAGE_NONE;
// Test that the form structure includes full information for each delta
// apart from #access.
$entity_type = 'test_entity';
$entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
$form = array();
$form_state = form_state_defaults();
field_attach_form($entity_type, $entity, $form, $form_state);
$this->assertEqual($form[$field_name_no_access][$langcode][0]['value']['#entity_type'], $entity_type, 'The correct entity type is set in the field structure.');
$this->assertFalse($form[$field_name_no_access]['#access'], 'Field #access is FALSE for the field without edit access.');
// Display creation form.
$this->drupalGet('test-entity/add/test-bundle');
$this->assertNoFieldByName("{$field_name_no_access}[$langcode][0][value]", '', 'Widget is not displayed if field access is denied.');
// Create entity.
$edit = array("{$field_name}[$langcode][0][value]" => 1);
$this->drupalPost(NULL, $edit, t('Save'));
preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
$id = $match[1];
// Check that the default value was saved.
$entity = field_test_entity_test_load($id);
$this->assertEqual($entity->{$field_name_no_access}[$langcode][0]['value'], 99, 'Default value was saved for the field with no edit access.');
$this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 1, 'Entered value vas saved for the field with edit access.');
// Create a new revision.
$edit = array("{$field_name}[$langcode][0][value]" => 2, 'revision' => TRUE);
$this->drupalPost('test-entity/manage/' . $id . '/edit', $edit, t('Save'));
// Check that the new revision has the expected values.
$entity = field_test_entity_test_load($id);
$this->assertEqual($entity->{$field_name_no_access}[$langcode][0]['value'], 99, 'New revision has the expected value for the field with no edit access.');
$this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 2, 'New revision has the expected value for the field with edit access.');
// Check that the revision is also saved in the revisions table.
$entity = field_test_entity_test_load($id, $entity->ftvid);
$this->assertEqual($entity->{$field_name_no_access}[$langcode][0]['value'], 99, 'New revision has the expected value for the field with no edit access.');
$this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 2, 'New revision has the expected value for the field with edit access.');
}
/**
* Tests Field API form integration within a subform.
*/
function testNestedFieldForm() {
// Add two instances on the 'test_bundle'
field_create_field($this->field_single);
field_create_field($this->field_unlimited);
$this->instance['field_name'] = 'field_single';
$this->instance['label'] = 'Single field';
field_create_instance($this->instance);
$this->instance['field_name'] = 'field_unlimited';
$this->instance['label'] = 'Unlimited field';
field_create_instance($this->instance);
// Create two entities.
$entity_1 = field_test_create_stub_entity(1, 1);
$entity_1->is_new = TRUE;
$entity_1->field_single[LANGUAGE_NONE][] = array('value' => 0);
$entity_1->field_unlimited[LANGUAGE_NONE][] = array('value' => 1);
field_test_entity_save($entity_1);
$entity_2 = field_test_create_stub_entity(2, 2);
$entity_2->is_new = TRUE;
$entity_2->field_single[LANGUAGE_NONE][] = array('value' => 10);
$entity_2->field_unlimited[LANGUAGE_NONE][] = array('value' => 11);
field_test_entity_save($entity_2);
// Display the 'combined form'.
$this->drupalGet('test-entity/nested/1/2');
$this->assertFieldByName('field_single[und][0][value]', 0, 'Entity 1: field_single value appears correctly is the form.');
$this->assertFieldByName('field_unlimited[und][0][value]', 1, 'Entity 1: field_unlimited value 0 appears correctly is the form.');
$this->assertFieldByName('entity_2[field_single][und][0][value]', 10, 'Entity 2: field_single value appears correctly is the form.');
$this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 11, 'Entity 2: field_unlimited value 0 appears correctly is the form.');
// Submit the form and check that the entities are updated accordingly.
$edit = array(
'field_single[und][0][value]' => 1,
'field_unlimited[und][0][value]' => 2,
'field_unlimited[und][1][value]' => 3,
'entity_2[field_single][und][0][value]' => 11,
'entity_2[field_unlimited][und][0][value]' => 12,
'entity_2[field_unlimited][und][1][value]' => 13,
);
$this->drupalPost(NULL, $edit, t('Save'));
field_cache_clear();
$entity_1 = field_test_create_stub_entity(1);
$entity_2 = field_test_create_stub_entity(2);
$this->assertFieldValues($entity_1, 'field_single', LANGUAGE_NONE, array(1));
$this->assertFieldValues($entity_1, 'field_unlimited', LANGUAGE_NONE, array(2, 3));
$this->assertFieldValues($entity_2, 'field_single', LANGUAGE_NONE, array(11));
$this->assertFieldValues($entity_2, 'field_unlimited', LANGUAGE_NONE, array(12, 13));
// Submit invalid values and check that errors are reported on the
// correct widgets.
$edit = array(
'field_unlimited[und][1][value]' => -1,
);
$this->drupalPost('test-entity/nested/1/2', $edit, t('Save'));
$this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 1: the field validation error was reported.');
$error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-field-unlimited-und-1-value'));
$this->assertTrue($error_field, 'Entity 1: the error was flagged on the correct element.');
$edit = array(
'entity_2[field_unlimited][und][1][value]' => -1,
);
$this->drupalPost('test-entity/nested/1/2', $edit, t('Save'));
$this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 2: the field validation error was reported.');
$error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-entity-2-field-unlimited-und-1-value'));
$this->assertTrue($error_field, 'Entity 2: the error was flagged on the correct element.');
// Test that reordering works on both entities.
$edit = array(
'field_unlimited[und][0][_weight]' => 0,
'field_unlimited[und][1][_weight]' => -1,
'entity_2[field_unlimited][und][0][_weight]' => 0,
'entity_2[field_unlimited][und][1][_weight]' => -1,
);
$this->drupalPost('test-entity/nested/1/2', $edit, t('Save'));
field_cache_clear();
$this->assertFieldValues($entity_1, 'field_unlimited', LANGUAGE_NONE, array(3, 2));
$this->assertFieldValues($entity_2, 'field_unlimited', LANGUAGE_NONE, array(13, 12));
// Test the 'add more' buttons. Only Ajax submission is tested, because
// the two 'add more' buttons present in the form have the same #value,
// which confuses drupalPost().
// 'Add more' button in the first entity:
$this->drupalGet('test-entity/nested/1/2');
$this->drupalPostAJAX(NULL, array(), 'field_unlimited_add_more');
$this->assertFieldByName('field_unlimited[und][0][value]', 3, 'Entity 1: field_unlimited value 0 appears correctly is the form.');
$this->assertFieldByName('field_unlimited[und][1][value]', 2, 'Entity 1: field_unlimited value 1 appears correctly is the form.');
$this->assertFieldByName('field_unlimited[und][2][value]', '', 'Entity 1: field_unlimited value 2 appears correctly is the form.');
$this->assertFieldByName('field_unlimited[und][3][value]', '', 'Entity 1: an empty widget was added for field_unlimited value 3.');
// 'Add more' button in the first entity (changing field values):
$edit = array(
'entity_2[field_unlimited][und][0][value]' => 13,
'entity_2[field_unlimited][und][1][value]' => 14,
'entity_2[field_unlimited][und][2][value]' => 15,
);
$this->drupalPostAJAX(NULL, $edit, 'entity_2_field_unlimited_add_more');
$this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 13, 'Entity 2: field_unlimited value 0 appears correctly is the form.');
$this->assertFieldByName('entity_2[field_unlimited][und][1][value]', 14, 'Entity 2: field_unlimited value 1 appears correctly is the form.');
$this->assertFieldByName('entity_2[field_unlimited][und][2][value]', 15, 'Entity 2: field_unlimited value 2 appears correctly is the form.');
$this->assertFieldByName('entity_2[field_unlimited][und][3][value]', '', 'Entity 2: an empty widget was added for field_unlimited value 3.');
// Save the form and check values are saved correclty.
$this->drupalPost(NULL, array(), t('Save'));
field_cache_clear();
$this->assertFieldValues($entity_1, 'field_unlimited', LANGUAGE_NONE, array(3, 2));
$this->assertFieldValues($entity_2, 'field_unlimited', LANGUAGE_NONE, array(13, 14, 15));
}
}
class FieldDisplayAPITestCase extends FieldTestCase {
protected $field;
protected $instance;
protected $field_name;
protected $label;
protected $cardinality;
protected $values;
protected $entity;
protected $is_new;
public static function getInfo() {
return array(
'name' => 'Field Display API tests',
'description' => 'Test the display API.',
'group' => 'Field API',
);
}
function setUp() {
parent::setUp('field_test');
// Create a field and instance.
$this->field_name = 'test_field';
$this->label = $this->randomName();
$this->cardinality = 4;
$this->field = array(
'field_name' => $this->field_name,
'type' => 'test_field',
'cardinality' => $this->cardinality,
);
$this->instance = array(
'field_name' => $this->field_name,
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'label' => $this->label,
'display' => array(
'default' => array(
'type' => 'field_test_default',
'settings' => array(
'test_formatter_setting' => $this->randomName(),
),
),
'teaser' => array(
'type' => 'field_test_default',
'settings' => array(
'test_formatter_setting' => $this->randomName(),
),
),
),
);
field_create_field($this->field);
field_create_instance($this->instance);
// Create an entity with values.
$this->values = $this->_generateTestFieldValues($this->cardinality);
$this->entity = field_test_create_stub_entity();
$this->is_new = TRUE;
$this->entity->{$this->field_name}[LANGUAGE_NONE] = $this->values;
field_test_entity_save($this->entity);
}
/**
* Test the field_view_field() function.
*/
function testFieldViewField() {
// No display settings: check that default display settings are used.
$output = field_view_field('test_entity', $this->entity, $this->field_name);
$this->drupalSetContent(drupal_render($output));
$settings = field_info_formatter_settings('field_test_default');
$setting = $settings['test_formatter_setting'];
$this->assertText($this->label, 'Label was displayed.');
foreach ($this->values as $delta => $value) {
$this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
// Check that explicit display settings are used.
$display = array(
'label' => 'hidden',
'type' => 'field_test_multiple',
'settings' => array(
'test_formatter_setting_multiple' => $this->randomName(),
'alter' => TRUE,
),
);
$output = field_view_field('test_entity', $this->entity, $this->field_name, $display, LANGUAGE_NONE);
$this->drupalSetContent(drupal_render($output));
$setting = $display['settings']['test_formatter_setting_multiple'];
$this->assertNoText($this->label, 'Label was not displayed.');
$this->assertText('field_test_field_attach_view_alter', 'Alter fired, display passed.');
$this->assertText('field language is ' . LANGUAGE_NONE, 'Language is placed onto the context.');
$array = array();
foreach ($this->values as $delta => $value) {
$array[] = $delta . ':' . $value['value'];
}
$this->assertText($setting . '|' . implode('|', $array), 'Values were displayed with expected setting.');
// Check the prepare_view steps are invoked.
$display = array(
'label' => 'hidden',
'type' => 'field_test_with_prepare_view',
'settings' => array(
'test_formatter_setting_additional' => $this->randomName(),
),
);
$output = field_view_field('test_entity', $this->entity, $this->field_name, $display);
$view = drupal_render($output);
$this->drupalSetContent($view);
$setting = $display['settings']['test_formatter_setting_additional'];
$this->assertNoText($this->label, 'Label was not displayed.');
$this->assertNoText('field_test_field_attach_view_alter', 'Alter not fired.');
foreach ($this->values as $delta => $value) {
$this->assertText($setting . '|' . $value['value'] . '|' . ($value['value'] + 1), format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
// View mode: check that display settings specified in the instance are
// used.
$output = field_view_field('test_entity', $this->entity, $this->field_name, 'teaser');
$this->drupalSetContent(drupal_render($output));
$setting = $this->instance['display']['teaser']['settings']['test_formatter_setting'];
$this->assertText($this->label, 'Label was displayed.');
foreach ($this->values as $delta => $value) {
$this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
// Unknown view mode: check that display settings for 'default' view mode
// are used.
$output = field_view_field('test_entity', $this->entity, $this->field_name, 'unknown_view_mode');
$this->drupalSetContent(drupal_render($output));
$setting = $this->instance['display']['default']['settings']['test_formatter_setting'];
$this->assertText($this->label, 'Label was displayed.');
foreach ($this->values as $delta => $value) {
$this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
}
/**
* Test the field_view_value() function.
*/
function testFieldViewValue() {
// No display settings: check that default display settings are used.
$settings = field_info_formatter_settings('field_test_default');
$setting = $settings['test_formatter_setting'];
foreach ($this->values as $delta => $value) {
$item = $this->entity->{$this->field_name}[LANGUAGE_NONE][$delta];
$output = field_view_value('test_entity', $this->entity, $this->field_name, $item);
$this->drupalSetContent(drupal_render($output));
$this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
// Check that explicit display settings are used.
$display = array(
'type' => 'field_test_multiple',
'settings' => array(
'test_formatter_setting_multiple' => $this->randomName(),
),
);
$setting = $display['settings']['test_formatter_setting_multiple'];
$array = array();
foreach ($this->values as $delta => $value) {
$item = $this->entity->{$this->field_name}[LANGUAGE_NONE][$delta];
$output = field_view_value('test_entity', $this->entity, $this->field_name, $item, $display);
$this->drupalSetContent(drupal_render($output));
$this->assertText($setting . '|0:' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
// Check that prepare_view steps are invoked.
$display = array(
'type' => 'field_test_with_prepare_view',
'settings' => array(
'test_formatter_setting_additional' => $this->randomName(),
),
);
$setting = $display['settings']['test_formatter_setting_additional'];
$array = array();
foreach ($this->values as $delta => $value) {
$item = $this->entity->{$this->field_name}[LANGUAGE_NONE][$delta];
$output = field_view_value('test_entity', $this->entity, $this->field_name, $item, $display);
$this->drupalSetContent(drupal_render($output));
$this->assertText($setting . '|' . $value['value'] . '|' . ($value['value'] + 1), format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
// View mode: check that display settings specified in the instance are
// used.
$setting = $this->instance['display']['teaser']['settings']['test_formatter_setting'];
foreach ($this->values as $delta => $value) {
$item = $this->entity->{$this->field_name}[LANGUAGE_NONE][$delta];
$output = field_view_value('test_entity', $this->entity, $this->field_name, $item, 'teaser');
$this->drupalSetContent(drupal_render($output));
$this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
// Unknown view mode: check that display settings for 'default' view mode
// are used.
$setting = $this->instance['display']['default']['settings']['test_formatter_setting'];
foreach ($this->values as $delta => $value) {
$item = $this->entity->{$this->field_name}[LANGUAGE_NONE][$delta];
$output = field_view_value('test_entity', $this->entity, $this->field_name, $item, 'unknown_view_mode');
$this->drupalSetContent(drupal_render($output));
$this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
}
}
class FieldCrudTestCase extends FieldTestCase {
protected $field;
protected $another_field;
protected $instance_definition;
protected $another_instance_definition;
public static function getInfo() {
return array(
'name' => 'Field CRUD tests',
'description' => 'Test field create, read, update, and delete.',
'group' => 'Field API',
);
}
function setUp() {
// field_update_field() tests use number.module
parent::setUp('field_test', 'number');
}
// TODO : test creation with
// - a full fledged $field structure, check that all the values are there
// - a minimal $field structure, check all default values are set
// defer actual $field comparison to a helper function, used for the two cases above
/**
* Test the creation of a field.
*/
function testCreateField() {
$field_definition = array(
'field_name' => 'field_2',
'type' => 'test_field',
);
field_test_memorize();
$field_definition = field_create_field($field_definition);
$mem = field_test_memorize();
$this->assertIdentical($mem['field_test_field_create_field'][0][0], $field_definition, 'hook_field_create_field() called with correct arguments.');
// Read the raw record from the {field_config_instance} table.
$result = db_query('SELECT * FROM {field_config} WHERE field_name = :field_name', array(':field_name' => $field_definition['field_name']));
$record = $result->fetchAssoc();
$record['data'] = unserialize($record['data']);
// Ensure that basic properties are preserved.
$this->assertEqual($record['field_name'], $field_definition['field_name'], 'The field name is properly saved.');
$this->assertEqual($record['type'], $field_definition['type'], 'The field type is properly saved.');
// Ensure that cardinality defaults to 1.
$this->assertEqual($record['cardinality'], 1, 'Cardinality defaults to 1.');
// Ensure that default settings are present.
$field_type = field_info_field_types($field_definition['type']);
$this->assertIdentical($record['data']['settings'], $field_type['settings'], 'Default field settings have been written.');
// Ensure that default storage was set.
$this->assertEqual($record['storage_type'], variable_get('field_storage_default'), 'The field type is properly saved.');
// Guarantee that the name is unique.
try {
field_create_field($field_definition);
$this->fail(t('Cannot create two fields with the same name.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create two fields with the same name.'));
}
// Check that field type is required.
try {
$field_definition = array(
'field_name' => 'field_1',
);
field_create_field($field_definition);
$this->fail(t('Cannot create a field with no type.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create a field with no type.'));
}
// Check that field name is required.
try {
$field_definition = array(
'type' => 'test_field'
);
field_create_field($field_definition);
$this->fail(t('Cannot create an unnamed field.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create an unnamed field.'));
}
// Check that field name must start with a letter or _.
try {
$field_definition = array(
'field_name' => '2field_2',
'type' => 'test_field',
);
field_create_field($field_definition);
$this->fail(t('Cannot create a field with a name starting with a digit.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create a field with a name starting with a digit.'));
}
// Check that field name must only contain lowercase alphanumeric or _.
try {
$field_definition = array(
'field_name' => 'field#_3',
'type' => 'test_field',
);
field_create_field($field_definition);
$this->fail(t('Cannot create a field with a name containing an illegal character.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create a field with a name containing an illegal character.'));
}
// Check that field name cannot be longer than 32 characters long.
try {
$field_definition = array(
'field_name' => '_12345678901234567890123456789012',
'type' => 'test_field',
);
field_create_field($field_definition);
$this->fail(t('Cannot create a field with a name longer than 32 characters.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create a field with a name longer than 32 characters.'));
}
// Check that field name can not be an entity key.
// "ftvid" is known as an entity key from the "test_entity" type.
try {
$field_definition = array(
'type' => 'test_field',
'field_name' => 'ftvid',
);
$field = field_create_field($field_definition);
$this->fail(t('Cannot create a field bearing the name of an entity key.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create a field bearing the name of an entity key.'));
}
}
/**
* Test failure to create a field.
*/
function testCreateFieldFail() {
$field_name = 'duplicate';
$field_definition = array('field_name' => $field_name, 'type' => 'test_field', 'storage' => array('type' => 'field_test_storage_failure'));
$query = db_select('field_config')->condition('field_name', $field_name)->countQuery();
// The field does not appear in field_config.
$count = $query->execute()->fetchField();
$this->assertEqual($count, 0, 'A field_config row for the field does not exist.');
// Try to create the field.
try {
$field = field_create_field($field_definition);
$this->assertTrue(FALSE, 'Field creation (correctly) fails.');
}
catch (Exception $e) {
$this->assertTrue(TRUE, 'Field creation (correctly) fails.');
}
// The field does not appear in field_config.
$count = $query->execute()->fetchField();
$this->assertEqual($count, 0, 'A field_config row for the field does not exist.');
}
/**
* Test reading back a field definition.
*/
function testReadField() {
$field_definition = array(
'field_name' => 'field_1',
'type' => 'test_field',
);
field_create_field($field_definition);
// Read the field back.
$field = field_read_field($field_definition['field_name']);
$this->assertTrue($field_definition < $field, 'The field was properly read.');
}
/**
* Tests reading field definitions.
*/
function testReadFields() {
$field_definition = array(
'field_name' => 'field_1',
'type' => 'test_field',
);
field_create_field($field_definition);
// Check that 'single column' criteria works.
$fields = field_read_fields(array('field_name' => $field_definition['field_name']));
$this->assertTrue(count($fields) == 1 && isset($fields[$field_definition['field_name']]), 'The field was properly read.');
// Check that 'multi column' criteria works.
$fields = field_read_fields(array('field_name' => $field_definition['field_name'], 'type' => $field_definition['type']));
$this->assertTrue(count($fields) == 1 && isset($fields[$field_definition['field_name']]), 'The field was properly read.');
$fields = field_read_fields(array('field_name' => $field_definition['field_name'], 'type' => 'foo'));
$this->assertTrue(empty($fields), 'No field was found.');
// Create an instance of the field.
$instance_definition = array(
'field_name' => $field_definition['field_name'],
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
);
field_create_instance($instance_definition);
// Check that criteria spanning over the field_config_instance table work.
$fields = field_read_fields(array('entity_type' => $instance_definition['entity_type'], 'bundle' => $instance_definition['bundle']));
$this->assertTrue(count($fields) == 1 && isset($fields[$field_definition['field_name']]), 'The field was properly read.');
$fields = field_read_fields(array('entity_type' => $instance_definition['entity_type'], 'field_name' => $instance_definition['field_name']));
$this->assertTrue(count($fields) == 1 && isset($fields[$field_definition['field_name']]), 'The field was properly read.');
}
/**
* Test creation of indexes on data column.
*/
function testFieldIndexes() {
// Check that indexes specified by the field type are used by default.
$field_definition = array(
'field_name' => 'field_1',
'type' => 'test_field',
);
field_create_field($field_definition);
$field = field_read_field($field_definition['field_name']);
$expected_indexes = array('value' => array('value'));
$this->assertEqual($field['indexes'], $expected_indexes, 'Field type indexes saved by default');
// Check that indexes specified by the field definition override the field
// type indexes.
$field_definition = array(
'field_name' => 'field_2',
'type' => 'test_field',
'indexes' => array(
'value' => array(),
),
);
field_create_field($field_definition);
$field = field_read_field($field_definition['field_name']);
$expected_indexes = array('value' => array());
$this->assertEqual($field['indexes'], $expected_indexes, 'Field definition indexes override field type indexes');
// Check that indexes specified by the field definition add to the field
// type indexes.
$field_definition = array(
'field_name' => 'field_3',
'type' => 'test_field',
'indexes' => array(
'value_2' => array('value'),
),
);
field_create_field($field_definition);
$field = field_read_field($field_definition['field_name']);
$expected_indexes = array('value' => array('value'), 'value_2' => array('value'));
$this->assertEqual($field['indexes'], $expected_indexes, 'Field definition indexes are merged with field type indexes');
}
/**
* Test the deletion of a field.
*/
function testDeleteField() {
// TODO: Also test deletion of the data stored in the field ?
// Create two fields (so we can test that only one is deleted).
$this->field = array('field_name' => 'field_1', 'type' => 'test_field');
field_create_field($this->field);
$this->another_field = array('field_name' => 'field_2', 'type' => 'test_field');
field_create_field($this->another_field);
// Create instances for each.
$this->instance_definition = array(
'field_name' => $this->field['field_name'],
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
'widget' => array(
'type' => 'test_field_widget',
),
);
field_create_instance($this->instance_definition);
$this->another_instance_definition = $this->instance_definition;
$this->another_instance_definition['field_name'] = $this->another_field['field_name'];
field_create_instance($this->another_instance_definition);
// Test that the first field is not deleted, and then delete it.
$field = field_read_field($this->field['field_name'], array('include_deleted' => TRUE));
$this->assertTrue(!empty($field) && empty($field['deleted']), 'A new field is not marked for deletion.');
field_delete_field($this->field['field_name']);
// Make sure that the field is marked as deleted when it is specifically
// loaded.
$field = field_read_field($this->field['field_name'], array('include_deleted' => TRUE));
$this->assertTrue(!empty($field['deleted']), 'A deleted field is marked for deletion.');
// Make sure that this field's instance is marked as deleted when it is
// specifically loaded.
$instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle'], array('include_deleted' => TRUE));
$this->assertTrue(!empty($instance['deleted']), 'An instance for a deleted field is marked for deletion.');
// Try to load the field normally and make sure it does not show up.
$field = field_read_field($this->field['field_name']);
$this->assertTrue(empty($field), 'A deleted field is not loaded by default.');
// Try to load the instance normally and make sure it does not show up.
$instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
$this->assertTrue(empty($instance), 'An instance for a deleted field is not loaded by default.');
// Make sure the other field (and its field instance) are not deleted.
$another_field = field_read_field($this->another_field['field_name']);
$this->assertTrue(!empty($another_field) && empty($another_field['deleted']), 'A non-deleted field is not marked for deletion.');
$another_instance = field_read_instance('test_entity', $this->another_instance_definition['field_name'], $this->another_instance_definition['bundle']);
$this->assertTrue(!empty($another_instance) && empty($another_instance['deleted']), 'An instance of a non-deleted field is not marked for deletion.');
// Try to create a new field the same name as a deleted field and
// write data into it.
field_create_field($this->field);
field_create_instance($this->instance_definition);
$field = field_read_field($this->field['field_name']);
$this->assertTrue(!empty($field) && empty($field['deleted']), 'A new field with a previously used name is created.');
$instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
$this->assertTrue(!empty($instance) && empty($instance['deleted']), 'A new instance for a previously used field name is created.');
// Save an entity with data for the field
$entity = field_test_create_stub_entity(0, 0, $instance['bundle']);
$langcode = LANGUAGE_NONE;
$values[0]['value'] = mt_rand(1, 127);
$entity->{$field['field_name']}[$langcode] = $values;
$entity_type = 'test_entity';
field_attach_insert('test_entity', $entity);
// Verify the field is present on load
$entity = field_test_create_stub_entity(0, 0, $this->instance_definition['bundle']);
field_attach_load($entity_type, array(0 => $entity));
$this->assertIdentical(count($entity->{$field['field_name']}[$langcode]), count($values), "Data in previously deleted field saves and loads correctly");
foreach ($values as $delta => $value) {
$this->assertEqual($entity->{$field['field_name']}[$langcode][$delta]['value'], $values[$delta]['value'], "Data in previously deleted field saves and loads correctly");
}
}
function testUpdateNonExistentField() {
$test_field = array('field_name' => 'does_not_exist', 'type' => 'number_decimal');
try {
field_update_field($test_field);
$this->fail(t('Cannot update a field that does not exist.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot update a field that does not exist.'));
}
}
function testUpdateFieldType() {
$field = array('field_name' => 'field_type', 'type' => 'number_decimal');
$field = field_create_field($field);
$test_field = array('field_name' => 'field_type', 'type' => 'number_integer');
try {
field_update_field($test_field);
$this->fail(t('Cannot update a field to a different type.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot update a field to a different type.'));
}
}
/**
* Test updating a field.
*/
function testUpdateField() {
// Create a field with a defined cardinality, so that we can ensure it's
// respected. Since cardinality enforcement is consistent across database
// systems, it makes a good test case.
$cardinality = 4;
$field_definition = array(
'field_name' => 'field_update',
'type' => 'test_field',
'cardinality' => $cardinality,
);
$field_definition = field_create_field($field_definition);
$instance = array(
'field_name' => 'field_update',
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
);
$instance = field_create_instance($instance);
do {
// We need a unique ID for our entity. $cardinality will do.
$id = $cardinality;
$entity = field_test_create_stub_entity($id, $id, $instance['bundle']);
// Fill in the entity with more values than $cardinality.
for ($i = 0; $i < 20; $i++) {
$entity->field_update[LANGUAGE_NONE][$i]['value'] = $i;
}
// Save the entity.
field_attach_insert('test_entity', $entity);
// Load back and assert there are $cardinality number of values.
$entity = field_test_create_stub_entity($id, $id, $instance['bundle']);
field_attach_load('test_entity', array($id => $entity));
$this->assertEqual(count($entity->field_update[LANGUAGE_NONE]), $field_definition['cardinality'], 'Cardinality is kept');
// Now check the values themselves.
for ($delta = 0; $delta < $cardinality; $delta++) {
$this->assertEqual($entity->field_update[LANGUAGE_NONE][$delta]['value'], $delta, 'Value is kept');
}
// Increase $cardinality and set the field cardinality to the new value.
$field_definition['cardinality'] = ++$cardinality;
field_update_field($field_definition);
} while ($cardinality < 6);
}
/**
* Test field type modules forbidding an update.
*/
function testUpdateFieldForbid() {
$field = array('field_name' => 'forbidden', 'type' => 'test_field', 'settings' => array('changeable' => 0, 'unchangeable' => 0));
$field = field_create_field($field);
$field['settings']['changeable']++;
try {
field_update_field($field);
$this->pass(t("A changeable setting can be updated."));
}
catch (FieldException $e) {
$this->fail(t("An unchangeable setting cannot be updated."));
}
$field['settings']['unchangeable']++;
try {
field_update_field($field);
$this->fail(t("An unchangeable setting can be updated."));
}
catch (FieldException $e) {
$this->pass(t("An unchangeable setting cannot be updated."));
}
}
/**
* Test that fields are properly marked active or inactive.
*/
function testActive() {
$field_definition = array(
'field_name' => 'field_1',
'type' => 'test_field',
// For this test, we need a storage backend provided by a different
// module than field_test.module.
'storage' => array(
'type' => 'field_sql_storage',
),
);
field_create_field($field_definition);
// Test disabling and enabling:
// - the field type module,
// - the storage module,
// - both.
$this->_testActiveHelper($field_definition, array('field_test'));
$this->_testActiveHelper($field_definition, array('field_sql_storage'));
$this->_testActiveHelper($field_definition, array('field_test', 'field_sql_storage'));
}
/**
* Helper function for testActive().
*
* Test dependency between a field and a set of modules.
*
* @param $field_definition
* A field definition.
* @param $modules
* An aray of module names. The field will be tested to be inactive as long
* as any of those modules is disabled.
*/
function _testActiveHelper($field_definition, $modules) {
$field_name = $field_definition['field_name'];
// Read the field.
$field = field_read_field($field_name);
$this->assertTrue($field_definition <= $field, 'The field was properly read.');
module_disable($modules, FALSE);
$fields = field_read_fields(array('field_name' => $field_name), array('include_inactive' => TRUE));
$this->assertTrue(isset($fields[$field_name]) && $field_definition < $field, 'The field is properly read when explicitly fetching inactive fields.');
// Re-enable modules one by one, and check that the field is still inactive
// while some modules remain disabled.
while ($modules) {
$field = field_read_field($field_name);
$this->assertTrue(empty($field), format_string('%modules disabled. The field is marked inactive.', array('%modules' => implode(', ', $modules))));
$module = array_shift($modules);
module_enable(array($module), FALSE);
}
// Check that the field is active again after all modules have been
// enabled.
$field = field_read_field($field_name);
$this->assertTrue($field_definition <= $field, 'The field was was marked active.');
}
}
class FieldInstanceCrudTestCase extends FieldTestCase {
protected $field;
protected $instance_definition;
protected $another_instance_definition;
public static function getInfo() {
return array(
'name' => 'Field instance CRUD tests',
'description' => 'Create field entities by attaching fields to entities.',
'group' => 'Field API',
);
}
function setUp() {
parent::setUp('field_test');
$this->field = array(
'field_name' => drupal_strtolower($this->randomName()),
'type' => 'test_field',
);
field_create_field($this->field);
$this->instance_definition = array(
'field_name' => $this->field['field_name'],
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
);
}
// TODO : test creation with
// - a full fledged $instance structure, check that all the values are there
// - a minimal $instance structure, check all default values are set
// defer actual $instance comparison to a helper function, used for the two cases above,
// and for testUpdateFieldInstance
/**
* Test the creation of a field instance.
*/
function testCreateFieldInstance() {
field_create_instance($this->instance_definition);
// Read the raw record from the {field_config_instance} table.
$result = db_query('SELECT * FROM {field_config_instance} WHERE field_name = :field_name AND bundle = :bundle', array(':field_name' => $this->instance_definition['field_name'], ':bundle' => $this->instance_definition['bundle']));
$record = $result->fetchAssoc();
$record['data'] = unserialize($record['data']);
$field_type = field_info_field_types($this->field['type']);
$widget_type = field_info_widget_types($field_type['default_widget']);
$formatter_type = field_info_formatter_types($field_type['default_formatter']);
// Check that default values are set.
$this->assertIdentical($record['data']['required'], FALSE, 'Required defaults to false.');
$this->assertIdentical($record['data']['label'], $this->instance_definition['field_name'], 'Label defaults to field name.');
$this->assertIdentical($record['data']['description'], '', 'Description defaults to empty string.');
$this->assertIdentical($record['data']['widget']['type'], $field_type['default_widget'], 'Default widget has been written.');
$this->assertTrue(isset($record['data']['display']['default']), 'Display for "full" view_mode has been written.');
$this->assertIdentical($record['data']['display']['default']['type'], $field_type['default_formatter'], 'Default formatter for "full" view_mode has been written.');
// Check that default settings are set.
$this->assertIdentical($record['data']['settings'], $field_type['instance_settings'] , 'Default instance settings have been written.');
$this->assertIdentical($record['data']['widget']['settings'], $widget_type['settings'] , 'Default widget settings have been written.');
$this->assertIdentical($record['data']['display']['default']['settings'], $formatter_type['settings'], 'Default formatter settings for "full" view_mode have been written.');
// Guarantee that the field/bundle combination is unique.
try {
field_create_instance($this->instance_definition);
$this->fail(t('Cannot create two instances with the same field / bundle combination.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create two instances with the same field / bundle combination.'));
}
// Check that the specified field exists.
try {
$this->instance_definition['field_name'] = $this->randomName();
field_create_instance($this->instance_definition);
$this->fail(t('Cannot create an instance of a non-existing field.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create an instance of a non-existing field.'));
}
// Create a field restricted to a specific entity type.
$field_restricted = array(
'field_name' => drupal_strtolower($this->randomName()),
'type' => 'test_field',
'entity_types' => array('test_cacheable_entity'),
);
field_create_field($field_restricted);
// Check that an instance can be added to an entity type allowed
// by the field.
try {
$instance = $this->instance_definition;
$instance['field_name'] = $field_restricted['field_name'];
$instance['entity_type'] = 'test_cacheable_entity';
field_create_instance($instance);
$this->pass(t('Can create an instance on an entity type allowed by the field.'));
}
catch (FieldException $e) {
$this->fail(t('Can create an instance on an entity type allowed by the field.'));
}
// Check that an instance cannot be added to an entity type
// forbidden by the field.
try {
$instance = $this->instance_definition;
$instance['field_name'] = $field_restricted['field_name'];
field_create_instance($instance);
$this->fail(t('Cannot create an instance on an entity type forbidden by the field.'));
}
catch (FieldException $e) {
$this->pass(t('Cannot create an instance on an entity type forbidden by the field.'));
}
// TODO: test other failures.
}
/**
* Test reading back an instance definition.
*/
function testReadFieldInstance() {
field_create_instance($this->instance_definition);
// Read the instance back.
$instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
$this->assertTrue($this->instance_definition < $instance, 'The field was properly read.');
}
/**
* Test the update of a field instance.
*/
function testUpdateFieldInstance() {
field_create_instance($this->instance_definition);
$field_type = field_info_field_types($this->field['type']);
// Check that basic changes are saved.
$instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
$instance['required'] = !$instance['required'];
$instance['label'] = $this->randomName();
$instance['description'] = $this->randomName();
$instance['settings']['test_instance_setting'] = $this->randomName();
$instance['widget']['settings']['test_widget_setting'] =$this->randomName();
$instance['widget']['weight']++;
$instance['display']['default']['settings']['test_formatter_setting'] = $this->randomName();
$instance['display']['default']['weight']++;
field_update_instance($instance);
$instance_new = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
$this->assertEqual($instance['required'], $instance_new['required'], '"required" change is saved');
$this->assertEqual($instance['label'], $instance_new['label'], '"label" change is saved');
$this->assertEqual($instance['description'], $instance_new['description'], '"description" change is saved');
$this->assertEqual($instance['widget']['settings']['test_widget_setting'], $instance_new['widget']['settings']['test_widget_setting'], 'Widget setting change is saved');
$this->assertEqual($instance['widget']['weight'], $instance_new['widget']['weight'], 'Widget weight change is saved');
$this->assertEqual($instance['display']['default']['settings']['test_formatter_setting'], $instance_new['display']['default']['settings']['test_formatter_setting'], 'Formatter setting change is saved');
$this->assertEqual($instance['display']['default']['weight'], $instance_new['display']['default']['weight'], 'Widget weight change is saved');
// Check that changing widget and formatter types updates the default settings.
$instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
$instance['widget']['type'] = 'test_field_widget_multiple';
$instance['display']['default']['type'] = 'field_test_multiple';
field_update_instance($instance);
$instance_new = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
$this->assertEqual($instance['widget']['type'], $instance_new['widget']['type'] , 'Widget type change is saved.');
$settings = field_info_widget_settings($instance_new['widget']['type']);
$this->assertIdentical($settings, array_intersect_key($instance_new['widget']['settings'], $settings) , 'Widget type change updates default settings.');
$this->assertEqual($instance['display']['default']['type'], $instance_new['display']['default']['type'] , 'Formatter type change is saved.');
$info = field_info_formatter_types($instance_new['display']['default']['type']);
$settings = $info['settings'];
$this->assertIdentical($settings, array_intersect_key($instance_new['display']['default']['settings'], $settings) , 'Changing formatter type updates default settings.');
// Check that adding a new view mode is saved and gets default settings.
$instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
$instance['display']['teaser'] = array();
field_update_instance($instance);
$instance_new = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
$this->assertTrue(isset($instance_new['display']['teaser']), 'Display for the new view_mode has been written.');
$this->assertIdentical($instance_new['display']['teaser']['type'], $field_type['default_formatter'], 'Default formatter for the new view_mode has been written.');
$info = field_info_formatter_types($instance_new['display']['teaser']['type']);
$settings = $info['settings'];
$this->assertIdentical($settings, $instance_new['display']['teaser']['settings'] , 'Default formatter settings for the new view_mode have been written.');
// TODO: test failures.
}
/**
* Test the deletion of a field instance.
*/
function testDeleteFieldInstance() {
// TODO: Test deletion of the data stored in the field also.
// Need to check that data for a 'deleted' field / instance doesn't get loaded
// Need to check data marked deleted is cleaned on cron (not implemented yet...)
// Create two instances for the same field so we can test that only one
// is deleted.
field_create_instance($this->instance_definition);
$this->another_instance_definition = $this->instance_definition;
$this->another_instance_definition['bundle'] .= '_another_bundle';
$instance = field_create_instance($this->another_instance_definition);
// Test that the first instance is not deleted, and then delete it.
$instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle'], array('include_deleted' => TRUE));
$this->assertTrue(!empty($instance) && empty($instance['deleted']), 'A new field instance is not marked for deletion.');
field_delete_instance($instance);
// Make sure the instance is marked as deleted when the instance is
// specifically loaded.
$instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle'], array('include_deleted' => TRUE));
$this->assertTrue(!empty($instance['deleted']), 'A deleted field instance is marked for deletion.');
// Try to load the instance normally and make sure it does not show up.
$instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
$this->assertTrue(empty($instance), 'A deleted field instance is not loaded by default.');
// Make sure the other field instance is not deleted.
$another_instance = field_read_instance('test_entity', $this->another_instance_definition['field_name'], $this->another_instance_definition['bundle']);
$this->assertTrue(!empty($another_instance) && empty($another_instance['deleted']), 'A non-deleted field instance is not marked for deletion.');
// Make sure the field is deleted when its last instance is deleted.
field_delete_instance($another_instance);
$field = field_read_field($another_instance['field_name'], array('include_deleted' => TRUE));
$this->assertTrue(!empty($field['deleted']), 'A deleted field is marked for deletion after all its instances have been marked for deletion.');
}
}
/**
* Unit test class for the multilanguage fields logic.
*
* The following tests will check the multilanguage logic of _field_invoke() and
* that only the correct values are returned by field_available_languages().
*/
class FieldTranslationsTestCase extends FieldTestCase {
protected $field;
protected $instance;
protected $field_name;
protected $entity_type;
public static function getInfo() {
return array(
'name' => 'Field translations tests',
'description' => 'Test multilanguage fields logic.',
'group' => 'Field API',
);
}
function setUp() {
parent::setUp('locale', 'field_test');
$this->field_name = drupal_strtolower($this->randomName() . '_field_name');
$this->entity_type = 'test_entity';
$field = array(
'field_name' => $this->field_name,
'type' => 'test_field',
'cardinality' => 4,
'translatable' => TRUE,
);
field_create_field($field);
$this->field = field_read_field($this->field_name);
$instance = array(
'field_name' => $this->field_name,
'entity_type' => $this->entity_type,
'bundle' => 'test_bundle',
);
field_create_instance($instance);
$this->instance = field_read_instance('test_entity', $this->field_name, 'test_bundle');
require_once DRUPAL_ROOT . '/includes/locale.inc';
for ($i = 0; $i < 3; ++$i) {
locale_add_language('l' . $i, $this->randomString(), $this->randomString());
}
}
/**
* Ensures that only valid values are returned by field_available_languages().
*/
function testFieldAvailableLanguages() {
// Test 'translatable' fieldable info.
field_test_entity_info_translatable('test_entity', FALSE);
$field = $this->field;
$field['field_name'] .= '_untranslatable';
// Enable field translations for the entity.
field_test_entity_info_translatable('test_entity', TRUE);
// Test hook_field_languages() invocation on a translatable field.
variable_set('field_test_field_available_languages_alter', TRUE);
$enabled_languages = field_content_languages();
$available_languages = field_available_languages($this->entity_type, $this->field);
foreach ($available_languages as $delta => $langcode) {
if ($langcode != 'xx' && $langcode != 'en') {
$this->assertTrue(in_array($langcode, $enabled_languages), format_string('%language is an enabled language.', array('%language' => $langcode)));
}
}
$this->assertTrue(in_array('xx', $available_languages), format_string('%language was made available.', array('%language' => 'xx')));
$this->assertFalse(in_array('en', $available_languages), format_string('%language was made unavailable.', array('%language' => 'en')));
// Test field_available_languages() behavior for untranslatable fields.
$this->field['translatable'] = FALSE;
field_update_field($this->field);
$available_languages = field_available_languages($this->entity_type, $this->field);
$this->assertTrue(count($available_languages) == 1 && $available_languages[0] === LANGUAGE_NONE, 'For untranslatable fields only LANGUAGE_NONE is available.');
}
/**
* Test the multilanguage logic of _field_invoke().
*/
function testFieldInvoke() {
// Enable field translations for the entity.
field_test_entity_info_translatable('test_entity', TRUE);
$entity_type = 'test_entity';
$entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
// Populate some extra languages to check if _field_invoke() correctly uses
// the result of field_available_languages().
$values = array();
$extra_languages = mt_rand(1, 4);
$languages = $available_languages = field_available_languages($this->entity_type, $this->field);
for ($i = 0; $i < $extra_languages; ++$i) {
$languages[] = $this->randomName(2);
}
// For each given language provide some random values.
foreach ($languages as $langcode) {
for ($delta = 0; $delta < $this->field['cardinality']; $delta++) {
$values[$langcode][$delta]['value'] = mt_rand(1, 127);
}
}
$entity->{$this->field_name} = $values;
$results = _field_invoke('test_op', $entity_type, $entity);
foreach ($results as $langcode => $result) {
$hash = hash('sha256', serialize(array($entity_type, $entity, $this->field_name, $langcode, $values[$langcode])));
// Check whether the parameters passed to _field_invoke() were correctly
// forwarded to the callback function.
$this->assertEqual($hash, $result, format_string('The result for %language is correctly stored.', array('%language' => $langcode)));
}
$this->assertEqual(count($results), count($available_languages), 'No unavailable language has been processed.');
}
/**
* Test the multilanguage logic of _field_invoke_multiple().
*/
function testFieldInvokeMultiple() {
// Enable field translations for the entity.
field_test_entity_info_translatable('test_entity', TRUE);
$values = array();
$options = array();
$entities = array();
$entity_type = 'test_entity';
$entity_count = 5;
$available_languages = field_available_languages($this->entity_type, $this->field);
for ($id = 1; $id <= $entity_count; ++$id) {
$entity = field_test_create_stub_entity($id, $id, $this->instance['bundle']);
$languages = $available_languages;
// Populate some extra languages to check whether _field_invoke()
// correctly uses the result of field_available_languages().
$extra_languages = mt_rand(1, 4);
for ($i = 0; $i < $extra_languages; ++$i) {
$languages[] = $this->randomName(2);
}
// For each given language provide some random values.
$language_count = count($languages);
for ($i = 0; $i < $language_count; ++$i) {
$langcode = $languages[$i];
// Avoid to populate at least one field translation to check that
// per-entity language suggestions work even when available field values
// are different for each language.
if ($i !== $id) {
for ($delta = 0; $delta < $this->field['cardinality']; $delta++) {
$values[$id][$langcode][$delta]['value'] = mt_rand(1, 127);
}
}
// Ensure that a language for which there is no field translation is
// used as display language to prepare per-entity language suggestions.
elseif (!isset($display_language)) {
$display_language = $langcode;
}
}
$entity->{$this->field_name} = $values[$id];
$entities[$id] = $entity;
// Store per-entity language suggestions.
$options['language'][$id] = field_language($entity_type, $entity, NULL, $display_language);
}
$grouped_results = _field_invoke_multiple('test_op_multiple', $entity_type, $entities);
foreach ($grouped_results as $id => $results) {
foreach ($results as $langcode => $result) {
if (isset($values[$id][$langcode])) {
$hash = hash('sha256', serialize(array($entity_type, $entities[$id], $this->field_name, $langcode, $values[$id][$langcode])));
// Check whether the parameters passed to _field_invoke_multiple()
// were correctly forwarded to the callback function.
$this->assertEqual($hash, $result, format_string('The result for entity %id/%language is correctly stored.', array('%id' => $id, '%language' => $langcode)));
}
}
$this->assertEqual(count($results), count($available_languages), format_string('No unavailable language has been processed for entity %id.', array('%id' => $id)));
}
$null = NULL;
$grouped_results = _field_invoke_multiple('test_op_multiple', $entity_type, $entities, $null, $null, $options);
foreach ($grouped_results as $id => $results) {
foreach ($results as $langcode => $result) {
$this->assertTrue(isset($options['language'][$id]), format_string('The result language %language for entity %id was correctly suggested (display language: %display_language).', array('%id' => $id, '%language' => $langcode, '%display_language' => $display_language)));
}
}
}
/**
* Test translatable fields storage/retrieval.
*/
function testTranslatableFieldSaveLoad() {
// Enable field translations for nodes.
field_test_entity_info_translatable('node', TRUE);
$entity_info = entity_get_info('node');
$this->assertTrue(count($entity_info['translation']), 'Nodes are translatable.');
// Prepare the field translations.
field_test_entity_info_translatable('test_entity', TRUE);
$eid = $evid = 1;
$entity_type = 'test_entity';
$entity = field_test_create_stub_entity($eid, $evid, $this->instance['bundle']);
$field_translations = array();
$available_languages = field_available_languages($entity_type, $this->field);
$this->assertTrue(count($available_languages) > 1, 'Field is translatable.');
foreach ($available_languages as $langcode) {
$field_translations[$langcode] = $this->_generateTestFieldValues($this->field['cardinality']);
}
// Save and reload the field translations.
$entity->{$this->field_name} = $field_translations;
field_attach_insert($entity_type, $entity);
unset($entity->{$this->field_name});
field_attach_load($entity_type, array($eid => $entity));
// Check if the correct values were saved/loaded.
foreach ($field_translations as $langcode => $items) {
$result = TRUE;
foreach ($items as $delta => $item) {
$result = $result && $item['value'] == $entity->{$this->field_name}[$langcode][$delta]['value'];
}
$this->assertTrue($result, format_string('%language translation correctly handled.', array('%language' => $langcode)));
}
}
/**
* Tests display language logic for translatable fields.
*/
function testFieldDisplayLanguage() {
$field_name = drupal_strtolower($this->randomName() . '_field_name');
$entity_type = 'test_entity';
// We need an additional field here to properly test display language
// suggestions.
$field = array(
'field_name' => $field_name,
'type' => 'test_field',
'cardinality' => 2,
'translatable' => TRUE,
);
field_create_field($field);
$instance = array(
'field_name' => $field['field_name'],
'entity_type' => $entity_type,
'bundle' => 'test_bundle',
);
field_create_instance($instance);
$entity = field_test_create_stub_entity(1, 1, $this->instance['bundle']);
$instances = field_info_instances($entity_type, $this->instance['bundle']);
$enabled_languages = field_content_languages();
$languages = array();
// Generate field translations for languages different from the first
// enabled.
foreach ($instances as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
do {
// Index 0 is reserved for the requested language, this way we ensure
// that no field is actually populated with it.
$langcode = $enabled_languages[mt_rand(1, count($enabled_languages) - 1)];
}
while (isset($languages[$langcode]));
$languages[$langcode] = TRUE;
$entity->{$field_name}[$langcode] = $this->_generateTestFieldValues($field['cardinality']);
}
// Test multiple-fields display languages for untranslatable entities.
field_test_entity_info_translatable($entity_type, FALSE);
drupal_static_reset('field_language');
$requested_language = $enabled_languages[0];
$display_language = field_language($entity_type, $entity, NULL, $requested_language);
foreach ($instances as $instance) {
$field_name = $instance['field_name'];
$this->assertTrue($display_language[$field_name] == LANGUAGE_NONE, format_string('The display language for field %field_name is %language.', array('%field_name' => $field_name, '%language' => LANGUAGE_NONE)));
}
// Test multiple-fields display languages for translatable entities.
field_test_entity_info_translatable($entity_type, TRUE);
drupal_static_reset('field_language');
$display_language = field_language($entity_type, $entity, NULL, $requested_language);
foreach ($instances as $instance) {
$field_name = $instance['field_name'];
$langcode = $display_language[$field_name];
// As the requested language was not assinged to any field, if the
// returned language is defined for the current field, core fallback rules
// were successfully applied.
$this->assertTrue(isset($entity->{$field_name}[$langcode]) && $langcode != $requested_language, format_string('The display language for the field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode)));
}
// Test single-field display language.
drupal_static_reset('field_language');
$langcode = field_language($entity_type, $entity, $this->field_name, $requested_language);
$this->assertTrue(isset($entity->{$this->field_name}[$langcode]) && $langcode != $requested_language, format_string('The display language for the (single) field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode)));
// Test field_language() basic behavior without language fallback.
variable_set('field_test_language_fallback', FALSE);
$entity->{$this->field_name}[$requested_language] = mt_rand(1, 127);
drupal_static_reset('field_language');
$display_language = field_language($entity_type, $entity, $this->field_name, $requested_language);
$this->assertEqual($display_language, $requested_language, 'Display language behave correctly when language fallback is disabled');
}
/**
* Tests field translations when creating a new revision.
*/
function testFieldFormTranslationRevisions() {
$web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content'));
$this->drupalLogin($web_user);
// Prepare the field translations.
field_test_entity_info_translatable($this->entity_type, TRUE);
$eid = 1;
$entity = field_test_create_stub_entity($eid, $eid, $this->instance['bundle']);
$available_languages = array_flip(field_available_languages($this->entity_type, $this->field));
unset($available_languages[LANGUAGE_NONE]);
$field_name = $this->field['field_name'];
// Store the field translations.
$entity->is_new = TRUE;
foreach ($available_languages as $langcode => $value) {
$entity->{$field_name}[$langcode][0]['value'] = $value + 1;
}
field_test_entity_save($entity);
// Create a new revision.
$langcode = field_valid_language(NULL);
$edit = array("{$field_name}[$langcode][0][value]" => $entity->{$field_name}[$langcode][0]['value'], 'revision' => TRUE);
$this->drupalPost('test-entity/manage/' . $eid . '/edit', $edit, t('Save'));
// Check translation revisions.
$this->checkTranslationRevisions($eid, $eid, $available_languages);
$this->checkTranslationRevisions($eid, $eid + 1, $available_languages);
}
/**
* Check if the field translation attached to the entity revision identified
* by the passed arguments were correctly stored.
*/
private function checkTranslationRevisions($eid, $evid, $available_languages) {
$field_name = $this->field['field_name'];
$entity = field_test_entity_test_load($eid, $evid);
foreach ($available_languages as $langcode => $value) {
$passed = isset($entity->{$field_name}[$langcode]) && $entity->{$field_name}[$langcode][0]['value'] == $value + 1;
$this->assertTrue($passed, format_string('The @language translation for revision @revision was correctly stored', array('@language' => $langcode, '@revision' => $entity->ftvid)));
}
}
}
/**
* Unit test class for field bulk delete and batch purge functionality.
*/
class FieldBulkDeleteTestCase extends FieldTestCase {
protected $field;
protected $fields;
protected $instances;
protected $entities;
protected $entities_by_bundles;
protected $bundles;
protected $entity_type;
public static function getInfo() {
return array(
'name' => 'Field bulk delete tests',
'description' => 'Bulk delete fields and instances, and clean up afterwards.',
'group' => 'Field API',
);
}
/**
* Convenience function for Field API tests.
*
* Given an array of potentially fully-populated entities and an
* optional field name, generate an array of stub entities of the
* same fieldable type which contains the data for the field name
* (if given).
*
* @param $entity_type
* The entity type of $entities.
* @param $entities
* An array of entities of type $entity_type.
* @param $field_name
* Optional; a field name whose data should be copied from
* $entities into the returned stub entities.
* @return
* An array of stub entities corresponding to $entities.
*/
function _generateStubEntities($entity_type, $entities, $field_name = NULL) {
$stubs = array();
foreach ($entities as $id => $entity) {
$stub = entity_create_stub_entity($entity_type, entity_extract_ids($entity_type, $entity));
if (isset($field_name)) {
$stub->{$field_name} = $entity->{$field_name};
}
$stubs[$id] = $stub;
}
return $stubs;
}
/**
* Tests that the expected hooks have been invoked on the expected entities.
*
* @param $expected_hooks
* An array keyed by hook name, with one entry per expected invocation.
* Each entry is the value of the "$entity" parameter the hook is expected
* to have been passed.
* @param $actual_hooks
* The array of actual hook invocations recorded by field_test_memorize().
*/
function checkHooksInvocations($expected_hooks, $actual_hooks) {
foreach ($expected_hooks as $hook => $invocations) {
$actual_invocations = $actual_hooks[$hook];
// Check that the number of invocations is correct.
$this->assertEqual(count($actual_invocations), count($invocations), "$hook() was called the expected number of times.");
// Check that the hook was called for each expected argument.
foreach ($invocations as $argument) {
$found = FALSE;
foreach ($actual_invocations as $actual_arguments) {
if ($actual_arguments[1] == $argument) {
$found = TRUE;
break;
}
}
$this->assertTrue($found, "$hook() was called on expected argument");
}
}
}
function setUp() {
parent::setUp('field_test');
$this->fields = array();
$this->instances = array();
$this->entities = array();
$this->entities_by_bundles = array();
// Create two bundles.
$this->bundles = array('bb_1' => 'bb_1', 'bb_2' => 'bb_2');
foreach ($this->bundles as $name => $desc) {
field_test_create_bundle($name, $desc);
}
// Create two fields.
$field = array('field_name' => 'bf_1', 'type' => 'test_field', 'cardinality' => 1);
$this->fields[] = field_create_field($field);
$field = array('field_name' => 'bf_2', 'type' => 'test_field', 'cardinality' => 4);
$this->fields[] = field_create_field($field);
// For each bundle, create an instance of each field, and 10
// entities with values for each field.
$id = 0;
$this->entity_type = 'test_entity';
foreach ($this->bundles as $bundle) {
foreach ($this->fields as $field) {
$instance = array(
'field_name' => $field['field_name'],
'entity_type' => $this->entity_type,
'bundle' => $bundle,
'widget' => array(
'type' => 'test_field_widget',
)
);
$this->instances[] = field_create_instance($instance);
}
for ($i = 0; $i < 10; $i++) {
$entity = field_test_create_stub_entity($id, $id, $bundle);
foreach ($this->fields as $field) {
$entity->{$field['field_name']}[LANGUAGE_NONE] = $this->_generateTestFieldValues($field['cardinality']);
}
$this->entities[$id] = $entity;
// Also keep track of the entities per bundle.
$this->entities_by_bundles[$bundle][$id] = $entity;
field_attach_insert($this->entity_type, $entity);
$id++;
}
}
}
/**
* Verify that deleting an instance leaves the field data items in
* the database and that the appropriate Field API functions can
* operate on the deleted data and instance.
*
* This tests how EntityFieldQuery interacts with
* field_delete_instance() and could be moved to FieldCrudTestCase,
* but depends on this class's setUp().
*/
function testDeleteFieldInstance() {
$bundle = reset($this->bundles);
$field = reset($this->fields);
// There are 10 entities of this bundle.
$query = new EntityFieldQuery();
$found = $query
->fieldCondition($field)
->entityCondition('bundle', $bundle)
->execute();
$this->assertEqual(count($found['test_entity']), 10, 'Correct number of entities found before deleting');
// Delete the instance.
$instance = field_info_instance($this->entity_type, $field['field_name'], $bundle);
field_delete_instance($instance);
// The instance still exists, deleted.
$instances = field_read_instances(array('field_id' => $field['id'], 'deleted' => 1), array('include_deleted' => 1, 'include_inactive' => 1));
$this->assertEqual(count($instances), 1, 'There is one deleted instance');
$this->assertEqual($instances[0]['bundle'], $bundle, 'The deleted instance is for the correct bundle');
// There are 0 entities of this bundle with non-deleted data.
$query = new EntityFieldQuery();
$found = $query
->fieldCondition($field)
->entityCondition('bundle', $bundle)
->execute();
$this->assertTrue(!isset($found['test_entity']), 'No entities found after deleting');
// There are 10 entities of this bundle when deleted fields are allowed, and
// their values are correct.
$query = new EntityFieldQuery();
$found = $query
->fieldCondition($field)
->entityCondition('bundle', $bundle)
->deleted(TRUE)
->execute();
field_attach_load($this->entity_type, $found[$this->entity_type], FIELD_LOAD_CURRENT, array('field_id' => $field['id'], 'deleted' => 1));
$this->assertEqual(count($found['test_entity']), 10, 'Correct number of entities found after deleting');
foreach ($found['test_entity'] as $id => $entity) {
$this->assertEqual($this->entities[$id]->{$field['field_name']}, $entity->{$field['field_name']}, "Entity $id with deleted data loaded correctly");
}
}
/**
* Verify that field data items and instances are purged when an
* instance is deleted.
*/
function testPurgeInstance() {
// Start recording hook invocations.
field_test_memorize();
$bundle = reset($this->bundles);
$field = reset($this->fields);
// Delete the instance.
$instance = field_info_instance($this->entity_type, $field['field_name'], $bundle);
field_delete_instance($instance);
// No field hooks were called.
$mem = field_test_memorize();
$this->assertEqual(count($mem), 0, 'No field hooks were called');
$batch_size = 2;
for ($count = 8; $count >= 0; $count -= $batch_size) {
// Purge two entities.
field_purge_batch($batch_size);
// There are $count deleted entities left.
$query = new EntityFieldQuery();
$found = $query
->fieldCondition($field)
->entityCondition('bundle', $bundle)
->deleted(TRUE)
->execute();
$this->assertEqual($count ? count($found['test_entity']) : count($found), $count, 'Correct number of entities found after purging 2');
}
// Check hooks invocations.
// - hook_field_load() (multiple hook) should have been called on all
// entities by pairs of two.
// - hook_field_delete() should have been called once for each entity in the
// bundle.
$actual_hooks = field_test_memorize();
$hooks = array();
$stubs = $this->_generateStubEntities($this->entity_type, $this->entities_by_bundles[$bundle], $field['field_name']);
foreach (array_chunk($stubs, $batch_size, TRUE) as $chunk) {
$hooks['field_test_field_load'][] = $chunk;
}
foreach ($stubs as $stub) {
$hooks['field_test_field_delete'][] = $stub;
}
$this->checkHooksInvocations($hooks, $actual_hooks);
// The instance still exists, deleted.
$instances = field_read_instances(array('field_id' => $field['id'], 'deleted' => 1), array('include_deleted' => 1, 'include_inactive' => 1));
$this->assertEqual(count($instances), 1, 'There is one deleted instance');
// Purge the instance.
field_purge_batch($batch_size);
// The instance is gone.
$instances = field_read_instances(array('field_id' => $field['id'], 'deleted' => 1), array('include_deleted' => 1, 'include_inactive' => 1));
$this->assertEqual(count($instances), 0, 'The instance is gone');
// The field still exists, not deleted, because it has a second instance.
$fields = field_read_fields(array('id' => $field['id']), array('include_deleted' => 1, 'include_inactive' => 1));
$this->assertTrue(isset($fields[$field['id']]), 'The field exists and is not deleted');
}
/**
* Verify that fields are preserved and purged correctly as multiple
* instances are deleted and purged.
*/
function testPurgeField() {
// Start recording hook invocations.
field_test_memorize();
$field = reset($this->fields);
// Delete the first instance.
$bundle = reset($this->bundles);
$instance = field_info_instance($this->entity_type, $field['field_name'], $bundle);
field_delete_instance($instance);
// Assert that hook_field_delete() was not called yet.
$mem = field_test_memorize();
$this->assertEqual(count($mem), 0, 'No field hooks were called.');
// Purge the data.
field_purge_batch(10);
// Check hooks invocations.
// - hook_field_load() (multiple hook) should have been called once, for all
// entities in the bundle.
// - hook_field_delete() should have been called once for each entity in the
// bundle.
$actual_hooks = field_test_memorize();
$hooks = array();
$stubs = $this->_generateStubEntities($this->entity_type, $this->entities_by_bundles[$bundle], $field['field_name']);
$hooks['field_test_field_load'][] = $stubs;
foreach ($stubs as $stub) {
$hooks['field_test_field_delete'][] = $stub;
}
$this->checkHooksInvocations($hooks, $actual_hooks);
// Purge again to purge the instance.
field_purge_batch(0);
// The field still exists, not deleted.
$fields = field_read_fields(array('id' => $field['id']), array('include_deleted' => 1));
$this->assertTrue(isset($fields[$field['id']]) && !$fields[$field['id']]['deleted'], 'The field exists and is not deleted');
// Delete the second instance.
$bundle = next($this->bundles);
$instance = field_info_instance($this->entity_type, $field['field_name'], $bundle);
field_delete_instance($instance);
// Assert that hook_field_delete() was not called yet.
$mem = field_test_memorize();
$this->assertEqual(count($mem), 0, 'No field hooks were called.');
// Purge the data.
field_purge_batch(10);
// Check hooks invocations (same as above, for the 2nd bundle).
$actual_hooks = field_test_memorize();
$hooks = array();
$stubs = $this->_generateStubEntities($this->entity_type, $this->entities_by_bundles[$bundle], $field['field_name']);
$hooks['field_test_field_load'][] = $stubs;
foreach ($stubs as $stub) {
$hooks['field_test_field_delete'][] = $stub;
}
$this->checkHooksInvocations($hooks, $actual_hooks);
// The field still exists, deleted.
$fields = field_read_fields(array('id' => $field['id']), array('include_deleted' => 1));
$this->assertTrue(isset($fields[$field['id']]) && $fields[$field['id']]['deleted'], 'The field exists and is deleted');
// Purge again to purge the instance and the field.
field_purge_batch(0);
// The field is gone.
$fields = field_read_fields(array('id' => $field['id']), array('include_deleted' => 1, 'include_inactive' => 1));
$this->assertEqual(count($fields), 0, 'The field is purged.');
}
}
/**
* Tests entity properties.
*/
class EntityPropertiesTestCase extends FieldTestCase {
public static function getInfo() {
return array(
'name' => 'Entity properties',
'description' => 'Tests entity properties.',
'group' => 'Entity API',
);
}
function setUp() {
parent::setUp('field_test');
}
/**
* Tests label key and label callback of an entity.
*/
function testEntityLabel() {
$entity_types = array(
'test_entity_no_label',
'test_entity_label',
'test_entity_label_callback',
);
$entity = field_test_create_stub_entity();
foreach ($entity_types as $entity_type) {
$label = entity_label($entity_type, $entity);
switch ($entity_type) {
case 'test_entity_no_label':
$this->assertFalse($label, 'Entity with no label property or callback returned FALSE.');
break;
case 'test_entity_label':
$this->assertEqual($label, $entity->ftlabel, 'Entity with label key returned correct label.');
break;
case 'test_entity_label_callback':
$this->assertEqual($label, 'label callback ' . $entity->ftlabel, 'Entity with label callback returned correct label.');
break;
}
}
}
/**
* Tests entity_extract_ids() with an empty entity info.
*/
function testEntityKeys(){
$entity_type = 'test_entity2';
$entity = field_test_create_stub_entity();
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
$this->assertNull($id, 'Entity id for test_entity2 returned NULL.');
$this->assertNull($vid, 'Entity vid for test_entity2 returned NULL.');
}
}
array('label' => 'Test Bundle')));
$test_entity_modes = array(
'full' => array(
'label' => t('Full object'),
'custom settings' => TRUE,
),
'teaser' => array(
'label' => t('Teaser'),
'custom settings' => TRUE,
),
);
return array(
'test_entity' => array(
'label' => t('Test Entity'),
'fieldable' => TRUE,
'field cache' => FALSE,
'base table' => 'test_entity',
'revision table' => 'test_entity_revision',
'entity keys' => array(
'id' => 'ftid',
'revision' => 'ftvid',
'bundle' => 'fttype',
),
'bundles' => $bundles,
'view modes' => $test_entity_modes,
),
// This entity type doesn't get form handling for now...
'test_cacheable_entity' => array(
'label' => t('Test Entity, cacheable'),
'fieldable' => TRUE,
'field cache' => TRUE,
'entity keys' => array(
'id' => 'ftid',
'revision' => 'ftvid',
'bundle' => 'fttype',
),
'bundles' => $bundles,
'view modes' => $test_entity_modes,
),
'test_entity_bundle_key' => array(
'label' => t('Test Entity with a bundle key.'),
'base table' => 'test_entity_bundle_key',
'fieldable' => TRUE,
'field cache' => FALSE,
'entity keys' => array(
'id' => 'ftid',
'bundle' => 'fttype',
),
'bundles' => array('bundle1' => array('label' => 'Bundle1'), 'bundle2' => array('label' => 'Bundle2')) + $bundles,
'view modes' => $test_entity_modes,
),
// In this case, the bundle key is not stored in the database.
'test_entity_bundle' => array(
'label' => t('Test Entity with a specified bundle.'),
'base table' => 'test_entity_bundle',
'fieldable' => TRUE,
'controller class' => 'TestEntityBundleController',
'field cache' => FALSE,
'entity keys' => array(
'id' => 'ftid',
'bundle' => 'fttype',
),
'bundles' => array('test_entity_2' => array('label' => 'Test entity 2')) + $bundles,
'view modes' => $test_entity_modes,
),
// @see EntityPropertiesTestCase::testEntityLabel()
'test_entity_no_label' => array(
'label' => t('Test entity without label'),
'fieldable' => TRUE,
'field cache' => FALSE,
'base table' => 'test_entity',
'entity keys' => array(
'id' => 'ftid',
'revision' => 'ftvid',
'bundle' => 'fttype',
),
'bundles' => $bundles,
'view modes' => $test_entity_modes,
),
'test_entity_label' => array(
'label' => t('Test entity label'),
'fieldable' => TRUE,
'field cache' => FALSE,
'base table' => 'test_entity',
'entity keys' => array(
'id' => 'ftid',
'revision' => 'ftvid',
'bundle' => 'fttype',
'label' => 'ftlabel',
),
'bundles' => $bundles,
'view modes' => $test_entity_modes,
),
'test_entity_label_callback' => array(
'label' => t('Test entity label callback'),
'fieldable' => TRUE,
'field cache' => FALSE,
'base table' => 'test_entity',
'label callback' => 'field_test_entity_label_callback',
'entity keys' => array(
'id' => 'ftid',
'revision' => 'ftvid',
'bundle' => 'fttype',
),
'bundles' => $bundles,
'view modes' => $test_entity_modes,
),
);
}
/**
* Implements hook_entity_info_alter().
*/
function field_test_entity_info_alter(&$entity_info) {
// Enable/disable field_test as a translation handler.
foreach (field_test_entity_info_translatable() as $entity_type => $translatable) {
$entity_info[$entity_type]['translation']['field_test'] = $translatable;
}
// Disable locale as a translation handler.
foreach ($entity_info as $entity_type => $info) {
$entity_info[$entity_type]['translation']['locale'] = FALSE;
}
}
/**
* Helper function to enable entity translations.
*/
function field_test_entity_info_translatable($entity_type = NULL, $translatable = NULL) {
drupal_static_reset('field_has_translation_handler');
$stored_value = &drupal_static(__FUNCTION__, array());
if (isset($entity_type)) {
$stored_value[$entity_type] = $translatable;
entity_info_cache_clear();
}
return $stored_value;
}
/**
* Creates a new bundle for test_entity entities.
*
* @param $bundle
* The machine-readable name of the bundle.
* @param $text
* The human-readable name of the bundle. If none is provided, the machine
* name will be used.
*/
function field_test_create_bundle($bundle, $text = NULL) {
$bundles = variable_get('field_test_bundles', array('test_bundle' => array('label' => 'Test Bundle')));
$bundles += array($bundle => array('label' => $text ? $text : $bundle));
variable_set('field_test_bundles', $bundles);
$info = field_test_entity_info();
foreach ($info as $type => $type_info) {
field_attach_create_bundle($type, $bundle);
}
}
/**
* Renames a bundle for test_entity entities.
*
* @param $bundle_old
* The machine-readable name of the bundle to rename.
* @param $bundle_new
* The new machine-readable name of the bundle.
*/
function field_test_rename_bundle($bundle_old, $bundle_new) {
$bundles = variable_get('field_test_bundles', array('test_bundle' => array('label' => 'Test Bundle')));
$bundles[$bundle_new] = $bundles[$bundle_old];
unset($bundles[$bundle_old]);
variable_set('field_test_bundles', $bundles);
$info = field_test_entity_info();
foreach ($info as $type => $type_info) {
field_attach_rename_bundle($type, $bundle_old, $bundle_new);
}
}
/**
* Deletes a bundle for test_entity objects.
*
* @param $bundle
* The machine-readable name of the bundle to delete.
*/
function field_test_delete_bundle($bundle) {
$bundles = variable_get('field_test_bundles', array('test_bundle' => array('label' => 'Test Bundle')));
unset($bundles[$bundle]);
variable_set('field_test_bundles', $bundles);
$info = field_test_entity_info();
foreach ($info as $type => $type_info) {
field_attach_delete_bundle($type, $bundle);
}
}
/**
* Creates a basic test_entity entity.
*/
function field_test_create_stub_entity($id = 1, $vid = 1, $bundle = 'test_bundle', $label = '') {
$entity = new stdClass();
// Only set id and vid properties if they don't come as NULL (creation form).
if (isset($id)) {
$entity->ftid = $id;
}
if (isset($vid)) {
$entity->ftvid = $vid;
}
$entity->fttype = $bundle;
$label = !empty($label) ? $label : $bundle . ' label';
$entity->ftlabel = $label;
return $entity;
}
/**
* Loads a test_entity.
*
* @param $ftid
* The id of the entity to load.
* @param $ftvid
* (Optional) The revision id of the entity to load. If not specified, the
* current revision will be used.
* @return
* The loaded entity.
*/
function field_test_entity_test_load($ftid, $ftvid = NULL) {
// Load basic strucure.
$query = db_select('test_entity', 'fte', array())
->condition('fte.ftid', $ftid);
if ($ftvid) {
$query->join('test_entity_revision', 'fter', 'fte.ftid = fter.ftid');
$query->addField('fte', 'ftid');
$query->addField('fte', 'fttype');
$query->addField('fter', 'ftvid');
$query->condition('fter.ftvid', $ftvid);
}
else {
$query->fields('fte');
}
$entities = $query->execute()->fetchAllAssoc('ftid');
// Attach fields.
if ($ftvid) {
field_attach_load_revision('test_entity', $entities);
}
else {
field_attach_load('test_entity', $entities);
}
return $entities[$ftid];
}
/**
* Saves a test_entity.
*
* A new entity is created if $entity->ftid and $entity->is_new are both empty.
* A new revision is created if $entity->revision is not empty.
*
* @param $entity
* The entity to save.
*/
function field_test_entity_save(&$entity) {
field_attach_presave('test_entity', $entity);
if (!isset($entity->is_new)) {
$entity->is_new = empty($entity->ftid);
}
if (!$entity->is_new && !empty($entity->revision)) {
$entity->old_ftvid = $entity->ftvid;
unset($entity->ftvid);
}
$update_entity = TRUE;
if ($entity->is_new) {
drupal_write_record('test_entity', $entity);
drupal_write_record('test_entity_revision', $entity);
$op = 'insert';
}
else {
drupal_write_record('test_entity', $entity, 'ftid');
if (!empty($entity->revision)) {
drupal_write_record('test_entity_revision', $entity);
}
else {
drupal_write_record('test_entity_revision', $entity, 'ftvid');
$update_entity = FALSE;
}
$op = 'update';
}
if ($update_entity) {
db_update('test_entity')
->fields(array('ftvid' => $entity->ftvid))
->condition('ftid', $entity->ftid)
->execute();
}
// Save fields.
$function = "field_attach_$op";
$function('test_entity', $entity);
}
/**
* Menu callback: displays the 'Add new test_entity' form.
*/
function field_test_entity_add($fttype) {
$fttype = str_replace('-', '_', $fttype);
$entity = (object)array('fttype' => $fttype);
drupal_set_title(t('Create test_entity @bundle', array('@bundle' => $fttype)), PASS_THROUGH);
return drupal_get_form('field_test_entity_form', $entity, TRUE);
}
/**
* Menu callback: displays the 'Edit exiisting test_entity' form.
*/
function field_test_entity_edit($entity) {
drupal_set_title(t('test_entity @ftid revision @ftvid', array('@ftid' => $entity->ftid, '@ftvid' => $entity->ftvid)), PASS_THROUGH);
return drupal_get_form('field_test_entity_form', $entity);
}
/**
* Test_entity form.
*/
function field_test_entity_form($form, &$form_state, $entity, $add = FALSE) {
// During initial form build, add the entity to the form state for use during
// form building and processing. During a rebuild, use what is in the form
// state.
if (!isset($form_state['test_entity'])) {
$form_state['test_entity'] = $entity;
}
else {
$entity = $form_state['test_entity'];
}
foreach (array('ftid', 'ftvid', 'fttype') as $key) {
$form[$key] = array(
'#type' => 'value',
'#value' => isset($entity->$key) ? $entity->$key : NULL,
);
}
// Add field widgets.
field_attach_form('test_entity', $entity, $form, $form_state);
if (!$add) {
$form['revision'] = array(
'#access' => user_access('administer field_test content'),
'#type' => 'checkbox',
'#title' => t('Create new revision'),
'#default_value' => FALSE,
'#weight' => 100,
);
}
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save'),
'#weight' => 101,
);
return $form;
}
/**
* Validate handler for field_test_entity_form().
*/
function field_test_entity_form_validate($form, &$form_state) {
entity_form_field_validate('test_entity', $form, $form_state);
}
/**
* Submit handler for field_test_entity_form().
*/
function field_test_entity_form_submit($form, &$form_state) {
$entity = field_test_entity_form_submit_build_test_entity($form, $form_state);
$insert = empty($entity->ftid);
field_test_entity_save($entity);
$message = $insert ? t('test_entity @id has been created.', array('@id' => $entity->ftid)) : t('test_entity @id has been updated.', array('@id' => $entity->ftid));
drupal_set_message($message);
if ($entity->ftid) {
$form_state['redirect'] = 'test-entity/manage/' . $entity->ftid . '/edit';
}
else {
// Error on save.
drupal_set_message(t('The entity could not be saved.'), 'error');
$form_state['rebuild'] = TRUE;
}
}
/**
* Updates the form state's entity by processing this submission's values.
*/
function field_test_entity_form_submit_build_test_entity($form, &$form_state) {
$entity = $form_state['test_entity'];
entity_form_submit_build_entity('test_entity', $entity, $form, $form_state);
return $entity;
}
/**
* Form combining two separate entities.
*/
function field_test_entity_nested_form($form, &$form_state, $entity_1, $entity_2) {
// First entity.
foreach (array('ftid', 'ftvid', 'fttype') as $key) {
$form[$key] = array(
'#type' => 'value',
'#value' => $entity_1->$key,
);
}
field_attach_form('test_entity', $entity_1, $form, $form_state);
// Second entity.
$form['entity_2'] = array(
'#type' => 'fieldset',
'#title' => t('Second entity'),
'#tree' => TRUE,
'#parents' => array('entity_2'),
'#weight' => 50,
);
foreach (array('ftid', 'ftvid', 'fttype') as $key) {
$form['entity_2'][$key] = array(
'#type' => 'value',
'#value' => $entity_2->$key,
);
}
field_attach_form('test_entity', $entity_2, $form['entity_2'], $form_state);
$form['save'] = array(
'#type' => 'submit',
'#value' => t('Save'),
'#weight' => 100,
);
return $form;
}
/**
* Validate handler for field_test_entity_nested_form().
*/
function field_test_entity_nested_form_validate($form, &$form_state) {
$entity_1 = (object) $form_state['values'];
field_attach_form_validate('test_entity', $entity_1, $form, $form_state);
$entity_2 = (object) $form_state['values']['entity_2'];
field_attach_form_validate('test_entity', $entity_2, $form['entity_2'], $form_state);
}
/**
* Submit handler for field_test_entity_nested_form().
*/
function field_test_entity_nested_form_submit($form, &$form_state) {
$entity_1 = (object) $form_state['values'];
field_attach_submit('test_entity', $entity_1, $form, $form_state);
field_test_entity_save($entity_1);
$entity_2 = (object) $form_state['values']['entity_2'];
field_attach_submit('test_entity', $entity_2, $form['entity_2'], $form_state);
field_test_entity_save($entity_2);
drupal_set_message(t('test_entities @id_1 and @id_2 have been updated.', array('@id_1' => $entity_1->ftid, '@id_2' => $entity_2->ftid)));
}
/**
* Controller class for the test_entity_bundle entity type.
*
* This extends the DrupalDefaultEntityController class, adding required
* special handling for bundles (since they are not stored in the database).
*/
class TestEntityBundleController extends DrupalDefaultEntityController {
protected function attachLoad(&$entities, $revision_id = FALSE) {
// Add bundle information.
foreach ($entities as $key => $entity) {
$entity->fttype = 'test_entity_bundle';
$entities[$key] = $entity;
}
parent::attachLoad($entities, $revision_id);
}
}
array(
'label' => t('Test field'),
'description' => t('Dummy field type used for tests.'),
'settings' => array(
'test_field_setting' => 'dummy test string',
'changeable' => 'a changeable field setting',
'unchangeable' => 'an unchangeable field setting',
),
'instance_settings' => array(
'test_instance_setting' => 'dummy test string',
'test_hook_field_load' => FALSE,
),
'default_widget' => 'test_field_widget',
'default_formatter' => 'field_test_default',
),
'shape' => array(
'label' => t('Shape'),
'description' => t('Another dummy field type.'),
'settings' => array(
'foreign_key_name' => 'shape',
),
'instance_settings' => array(),
'default_widget' => 'test_field_widget',
'default_formatter' => 'field_test_default',
),
'hidden_test_field' => array(
'no_ui' => TRUE,
'label' => t('Hidden from UI test field'),
'description' => t('Dummy hidden field type used for tests.'),
'settings' => array(),
'instance_settings' => array(),
'default_widget' => 'test_field_widget',
'default_formatter' => 'field_test_default',
),
);
}
/**
* Implements hook_field_update_forbid().
*/
function field_test_field_update_forbid($field, $prior_field, $has_data) {
if ($field['type'] == 'test_field' && $field['settings']['unchangeable'] != $prior_field['settings']['unchangeable']) {
throw new FieldException("field_test 'unchangeable' setting cannot be changed'");
}
}
/**
* Implements hook_field_load().
*/
function field_test_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
$args = func_get_args();
field_test_memorize(__FUNCTION__, $args);
foreach ($items as $id => $item) {
// To keep the test non-intrusive, only act for instances with the
// test_hook_field_load setting explicitly set to TRUE.
if ($instances[$id]['settings']['test_hook_field_load']) {
foreach ($item as $delta => $value) {
// Don't add anything on empty values.
if ($value) {
$items[$id][$delta]['additional_key'] = 'additional_value';
}
}
}
}
}
/**
* Implements hook_field_insert().
*/
function field_test_field_insert($entity_type, $entity, $field, $instance, $items) {
$args = func_get_args();
field_test_memorize(__FUNCTION__, $args);
}
/**
* Implements hook_field_update().
*/
function field_test_field_update($entity_type, $entity, $field, $instance, $items) {
$args = func_get_args();
field_test_memorize(__FUNCTION__, $args);
}
/**
* Implements hook_field_delete().
*/
function field_test_field_delete($entity_type, $entity, $field, $instance, $items) {
$args = func_get_args();
field_test_memorize(__FUNCTION__, $args);
}
/**
* Implements hook_field_validate().
*
* Possible error codes:
* - 'field_test_invalid': The value is invalid.
*/
function field_test_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
$args = func_get_args();
field_test_memorize(__FUNCTION__, $args);
foreach ($items as $delta => $item) {
if ($item['value'] == -1) {
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => 'field_test_invalid',
'message' => t('%name does not accept the value -1.', array('%name' => $instance['label'])),
);
}
}
}
/**
* Implements hook_field_is_empty().
*/
function field_test_field_is_empty($item, $field) {
return empty($item['value']);
}
/**
* Implements hook_field_settings_form().
*/
function field_test_field_settings_form($field, $instance, $has_data) {
$settings = $field['settings'];
$form['test_field_setting'] = array(
'#type' => 'textfield',
'#title' => t('Field test field setting'),
'#default_value' => $settings['test_field_setting'],
'#required' => FALSE,
'#description' => t('A dummy form element to simulate field setting.'),
);
return $form;
}
/**
* Implements hook_field_instance_settings_form().
*/
function field_test_field_instance_settings_form($field, $instance) {
$settings = $instance['settings'];
$form['test_instance_setting'] = array(
'#type' => 'textfield',
'#title' => t('Field test field instance setting'),
'#default_value' => $settings['test_instance_setting'],
'#required' => FALSE,
'#description' => t('A dummy form element to simulate field instance setting.'),
);
return $form;
}
/**
* Implements hook_field_widget_info().
*/
function field_test_field_widget_info() {
return array(
'test_field_widget' => array(
'label' => t('Test field'),
'field types' => array('test_field', 'hidden_test_field'),
'settings' => array('test_widget_setting' => 'dummy test string'),
),
'test_field_widget_multiple' => array(
'label' => t('Test field 1'),
'field types' => array('test_field'),
'settings' => array('test_widget_setting_multiple' => 'dummy test string'),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_CUSTOM,
),
),
);
}
/**
* Implements hook_field_widget_form().
*/
function field_test_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
switch ($instance['widget']['type']) {
case 'test_field_widget':
$element += array(
'#type' => 'textfield',
'#default_value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : '',
);
return array('value' => $element);
case 'test_field_widget_multiple':
$values = array();
foreach ($items as $delta => $value) {
$values[] = $value['value'];
}
$element += array(
'#type' => 'textfield',
'#default_value' => implode(', ', $values),
'#element_validate' => array('field_test_widget_multiple_validate'),
);
return $element;
}
}
/**
* Form element validation handler for 'test_field_widget_multiple' widget.
*/
function field_test_widget_multiple_validate($element, &$form_state) {
$values = array_map('trim', explode(',', $element['#value']));
$items = array();
foreach ($values as $value) {
$items[] = array('value' => $value);
}
form_set_value($element, $items, $form_state);
}
/**
* Implements hook_field_widget_error().
*/
function field_test_field_widget_error($element, $error, $form, &$form_state) {
// @todo No easy way to differenciate widget types, we should receive it as a
// parameter.
if (isset($element['value'])) {
// Widget is test_field_widget.
$error_element = $element['value'];
}
else {
// Widget is test_field_widget_multiple.
$error_element = $element;
}
form_error($error_element, $error['message']);
}
/**
* Implements hook_field_widget_settings_form().
*/
function field_test_field_widget_settings_form($field, $instance) {
$widget = $instance['widget'];
$settings = $widget['settings'];
$form['test_widget_setting'] = array(
'#type' => 'textfield',
'#title' => t('Field test field widget setting'),
'#default_value' => $settings['test_widget_setting'],
'#required' => FALSE,
'#description' => t('A dummy form element to simulate field widget setting.'),
);
return $form;
}
/**
* Implements hook_field_formatter_info().
*/
function field_test_field_formatter_info() {
return array(
'field_test_default' => array(
'label' => t('Default'),
'description' => t('Default formatter'),
'field types' => array('test_field'),
'settings' => array(
'test_formatter_setting' => 'dummy test string',
),
),
'field_test_multiple' => array(
'label' => t('Multiple'),
'description' => t('Multiple formatter'),
'field types' => array('test_field'),
'settings' => array(
'test_formatter_setting_multiple' => 'dummy test string',
),
),
'field_test_with_prepare_view' => array(
'label' => t('Tests hook_field_formatter_prepare_view()'),
'field types' => array('test_field'),
'settings' => array(
'test_formatter_setting_additional' => 'dummy test string',
),
),
);
}
/**
* Implements hook_field_formatter_settings_form().
*/
function field_test_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$element = array();
// The name of the setting depends on the formatter type.
$map = array(
'field_test_default' => 'test_formatter_setting',
'field_test_multiple' => 'test_formatter_setting_multiple',
'field_test_with_prepare_view' => 'test_formatter_setting_additional',
);
if (isset($map[$display['type']])) {
$name = $map[$display['type']];
$element[$name] = array(
'#title' => t('Setting'),
'#type' => 'textfield',
'#size' => 20,
'#default_value' => $settings[$name],
'#required' => TRUE,
);
}
return $element;
}
/**
* Implements hook_field_formatter_settings_summary().
*/
function field_test_field_formatter_settings_summary($field, $instance, $view_mode) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$summary = '';
// The name of the setting depends on the formatter type.
$map = array(
'field_test_default' => 'test_formatter_setting',
'field_test_multiple' => 'test_formatter_setting_multiple',
'field_test_with_prepare_view' => 'test_formatter_setting_additional',
);
if (isset($map[$display['type']])) {
$name = $map[$display['type']];
$summary = t('@setting: @value', array('@setting' => $name, '@value' => $settings[$name]));
}
return $summary;
}
/**
* Implements hook_field_formatter_prepare_view().
*/
function field_test_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
foreach ($items as $id => $item) {
// To keep the test non-intrusive, only act on the
// 'field_test_with_prepare_view' formatter.
if ($displays[$id]['type'] == 'field_test_with_prepare_view') {
foreach ($item as $delta => $value) {
// Don't add anything on empty values.
if ($value) {
$items[$id][$delta]['additional_formatter_value'] = $value['value'] + 1;
}
}
}
}
}
/**
* Implements hook_field_formatter_view().
*/
function field_test_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$element = array();
$settings = $display['settings'];
switch ($display['type']) {
case 'field_test_default':
foreach ($items as $delta => $item) {
$element[$delta] = array('#markup' => $settings['test_formatter_setting'] . '|' . $item['value']);
}
break;
case 'field_test_with_prepare_view':
foreach ($items as $delta => $item) {
$element[$delta] = array('#markup' => $settings['test_formatter_setting_additional'] . '|' . $item['value'] . '|' . $item['additional_formatter_value']);
}
break;
case 'field_test_multiple':
if (!empty($items)) {
$array = array();
foreach ($items as $delta => $item) {
$array[] = $delta . ':' . $item['value'];
}
$element[0] = array('#markup' => $settings['test_formatter_setting_multiple'] . '|' . implode('|', $array));
}
break;
}
return $element;
}
/**
* Sample 'default value' callback.
*/
function field_test_default_value($entity_type, $entity, $field, $instance) {
return array(array('value' => 99));
}
/**
* Implements hook_field_access().
*/
function field_test_field_access($op, $field, $entity_type, $entity, $account) {
if ($field['field_name'] == "field_no_{$op}_access") {
return FALSE;
}
return TRUE;
}
name = "Field API Test"
description = "Support module for the Field API tests."
core = 7.x
package = Testing
files[] = field_test.entity.inc
version = VERSION
hidden = TRUE
; Information added by Drupal.org packaging script on 2023-03-15
version = "7.95"
project = "drupal"
datestamp = "1678902530"
fields(array('weight' => 1))
->condition('name', 'field_test')
->execute();
}
/**
* Implements hook_schema().
*/
function field_test_schema() {
$schema['test_entity'] = array(
'description' => 'The base table for test_entities.',
'fields' => array(
'ftid' => array(
'description' => 'The primary identifier for a test_entity.',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
),
'ftvid' => array(
'description' => 'The current {test_entity_revision}.ftvid version identifier.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'fttype' => array(
'description' => 'The type of this test_entity.',
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
),
'ftlabel' => array(
'description' => 'The label of this test_entity.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
),
'unique keys' => array(
'ftvid' => array('ftvid'),
),
'primary key' => array('ftid'),
);
$schema['test_entity_bundle_key'] = array(
'description' => 'The base table for test entities with a bundle key.',
'fields' => array(
'ftid' => array(
'description' => 'The primary identifier for a test_entity_bundle_key.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'fttype' => array(
'description' => 'The type of this test_entity.',
'type' => 'varchar',
'length' => 32,
'not null' => FALSE,
'default' => '',
),
),
);
$schema['test_entity_bundle'] = array(
'description' => 'The base table for test entities with a bundle.',
'fields' => array(
'ftid' => array(
'description' => 'The primary identifier for a test_entity_bundle.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
),
);
$schema['test_entity_revision'] = array(
'description' => 'Stores information about each saved version of a {test_entity}.',
'fields' => array(
'ftid' => array(
'description' => 'The {test_entity} this version belongs to.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'ftvid' => array(
'description' => 'The primary identifier for this version.',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
),
),
'indexes' => array(
'nid' => array('ftid'),
),
'primary key' => array('ftvid'),
);
return $schema;
}
/**
* Implements hook_field_schema().
*/
function field_test_field_schema($field) {
if ($field['type'] == 'test_field') {
return array(
'columns' => array(
'value' => array(
'type' => 'int',
'size' => 'medium',
'not null' => FALSE,
),
),
'indexes' => array(
'value' => array('value'),
),
);
}
else {
$foreign_keys = array();
// The 'foreign keys' key is not always used in tests.
if (!empty($field['settings']['foreign_key_name'])) {
$foreign_keys['foreign keys'] = array(
// This is a dummy foreign key definition, references a table that
// doesn't exist, but that's not a problem.
$field['settings']['foreign_key_name'] => array(
'table' => $field['settings']['foreign_key_name'],
'columns' => array($field['settings']['foreign_key_name'] => 'id'),
),
);
}
return array(
'columns' => array(
'shape' => array(
'type' => 'varchar',
'length' => 32,
'not null' => FALSE,
),
'color' => array(
'type' => 'varchar',
'length' => 32,
'not null' => FALSE,
),
),
) + $foreign_keys;
}
}
array(
'title' => t('Access field_test content'),
'description' => t('View published field_test content.'),
),
'administer field_test content' => array(
'title' => t('Administer field_test content'),
'description' => t('Manage field_test content'),
),
);
return $perms;
}
/**
* Implements hook_menu().
*/
function field_test_menu() {
$items = array();
$bundles = field_info_bundles('test_entity');
foreach ($bundles as $bundle_name => $bundle_info) {
$bundle_url_str = str_replace('_', '-', $bundle_name);
$items['test-entity/add/' . $bundle_url_str] = array(
'title' => t('Add %bundle test_entity', array('%bundle' => $bundle_info['label'])),
'page callback' => 'field_test_entity_add',
'page arguments' => array(2),
'access arguments' => array('administer field_test content'),
'type' => MENU_NORMAL_ITEM,
);
}
$items['test-entity/manage/%field_test_entity_test/edit'] = array(
'title' => 'Edit test entity',
'page callback' => 'field_test_entity_edit',
'page arguments' => array(2),
'access arguments' => array('administer field_test content'),
'type' => MENU_NORMAL_ITEM,
);
$items['test-entity/nested/%field_test_entity_test/%field_test_entity_test'] = array(
'title' => 'Nested entity form',
'page callback' => 'drupal_get_form',
'page arguments' => array('field_test_entity_nested_form', 2, 3),
'access arguments' => array('administer field_test content'),
'type' => MENU_NORMAL_ITEM,
);
return $items;
}
/**
* Generic op to test _field_invoke behavior.
*
* This simulates a field operation callback to be invoked by _field_invoke().
*/
function field_test_field_test_op($entity_type, $entity, $field, $instance, $langcode, &$items) {
return array($langcode => hash('sha256', serialize(array($entity_type, $entity, $field['field_name'], $langcode, $items))));
}
/**
* Generic op to test _field_invoke_multiple behavior.
*
* This simulates a multiple field operation callback to be invoked by
* _field_invoke_multiple().
*/
function field_test_field_test_op_multiple($entity_type, $entities, $field, $instances, $langcode, &$items) {
$result = array();
foreach ($entities as $id => $entity) {
// Entities, instances and items are assumed to be consistently grouped by
// language. To verify this we try to access all the passed data structures
// by entity id. If they are grouped correctly, one entity, one instance and
// one array of items should be available for each entity id.
$field_name = $instances[$id]['field_name'];
$result[$id] = array($langcode => hash('sha256', serialize(array($entity_type, $entity, $field_name, $langcode, $items[$id]))));
}
return $result;
}
/**
* Implements hook_field_available_languages_alter().
*/
function field_test_field_available_languages_alter(&$languages, $context) {
if (variable_get('field_test_field_available_languages_alter', FALSE)) {
// Add an unavailable language.
$languages[] = 'xx';
// Remove an available language.
$index = array_search('en', $languages);
unset($languages[$index]);
}
}
/**
* Implements hook_field_language_alter().
*/
function field_test_field_language_alter(&$display_language, $context) {
if (variable_get('field_test_language_fallback', TRUE)) {
locale_field_language_fallback($display_language, $context['entity'], $context['language']);
}
}
/**
* Store and retrieve keyed data for later verification by unit tests.
*
* This function is a simple in-memory key-value store with the
* distinction that it stores all values for a given key instead of
* just the most recently set value. field_test module hooks call
* this function to record their arguments, keyed by hook name. The
* unit tests later call this function to verify that the correct
* hooks were called and were passed the correct arguments.
*
* This function ignores all calls until the first time it is called
* with $key of NULL. Each time it is called with $key of NULL, it
* erases all previously stored data from its internal cache, but also
* returns the previously stored data to the caller. A typical usage
* scenario is:
*
* @code
* // calls to field_test_memorize() here are ignored
*
* // turn on memorization
* field_test_memorize();
*
* // call some Field API functions that invoke field_test hooks
* $field = field_create_field(...);
*
* // retrieve and reset the memorized hook call data
* $mem = field_test_memorize();
*
* // make sure hook_field_create_field() is invoked correctly
* assertEqual(count($mem['field_test_field_create_field']), 1);
* assertEqual($mem['field_test_field_create_field'][0], array($field));
* @endcode
*
* @param $key
* The key under which to store to $value, or NULL as described above.
* @param $value
* A value to store for $key.
* @return
* An array mapping each $key to an array of each $value passed in
* for that key.
*/
function field_test_memorize($key = NULL, $value = NULL) {
$memorize = &drupal_static(__FUNCTION__, NULL);
if (!isset($key)) {
$return = $memorize;
$memorize = array();
return $return;
}
if (is_array($memorize)) {
$memorize[$key][] = $value;
}
}
/**
* Memorize calls to hook_field_create_field().
*/
function field_test_field_create_field($field) {
$args = func_get_args();
field_test_memorize(__FUNCTION__, $args);
}
/**
* Implements hook_entity_query_alter().
*/
function field_test_entity_query_alter(&$query) {
if (variable_get('field_test_entity_query_alter_execute_callback', FALSE)) {
$query->executeCallback = 'field_test_dummy_field_storage_query';
}
}
/**
* Pseudo-implements hook_field_storage_query().
*/
function field_test_dummy_field_storage_query(EntityFieldQuery $query) {
// Return dummy values that will be checked by the test.
return array(
'user' => array(
1 => entity_create_stub_entity('user', array(1, NULL, NULL)),
),
);
}
/**
* Implements callback_entity_info_label().
*
* @return
* The label of the entity prefixed with "label callback".
*/
function field_test_entity_label_callback($entity) {
return 'label callback ' . $entity->ftlabel;
}
/**
* Implements hook_field_attach_view_alter().
*/
function field_test_field_attach_view_alter(&$output, $context) {
if (!empty($context['display']['settings']['alter'])) {
$output['test_field'][] = array('#markup' => 'field_test_field_attach_view_alter');
}
if (isset($output['test_field'])) {
$output['test_field'][] = array('#markup' => 'field language is ' . $context['language']);
}
}
/**
* Implements hook_field_widget_properties_alter().
*/
function field_test_field_widget_properties_alter(&$widget, $context) {
// Make the alter_test_text field 42 characters for nodes and comments.
if (in_array($context['entity_type'], array('node', 'comment')) && ($context['field']['field_name'] == 'alter_test_text')) {
$widget['settings']['size'] = 42;
}
}
/**
* Implements hook_field_widget_properties_ENTITY_TYPE_alter().
*/
function field_test_field_widget_properties_user_alter(&$widget, $context) {
// Always use buttons for the alter_test_options field on user forms.
if ($context['field']['field_name'] == 'alter_test_options') {
$widget['type'] = 'options_buttons';
}
}
/**
* Implements hook_field_widget_form_alter().
*/
function field_test_field_widget_form_alter(&$element, &$form_state, $context) {
switch ($context['field']['field_name']) {
case 'alter_test_text':
drupal_set_message('Field size: ' . $context['instance']['widget']['settings']['size']);
break;
case 'alter_test_options':
drupal_set_message('Widget type: ' . $context['instance']['widget']['type']);
break;
}
}
/**
* Implements hook_query_TAG_alter() for tag 'efq_table_prefixing_test'.
*
* @see EntityFieldQueryTestCase::testTablePrefixing()
*/
function field_test_query_efq_table_prefixing_test_alter(&$query) {
// Add an additional join onto the entity base table. This will cause an
// exception if the EFQ does not properly prefix the base table.
$query->join('test_entity','te2','%alias.ftid = test_entity.ftid');
}
/**
* Implements hook_query_TAG_alter() for tag 'store_global_test_query'.
*/
function field_test_query_store_global_test_query_alter($query) {
// Save the query in a global variable so that it can be examined by tests.
// This can be used by any test which needs to check a query, but see
// FieldSqlStorageTestCase::testFieldSqlStorageMultipleConditionsSameColumn()
// for an example.
$GLOBALS['test_query'] = $query;
}
array(
'label' => t('Test storage'),
'description' => t('Dummy test storage backend. Stores field values in the variable table.'),
),
'field_test_storage_failure' => array(
'label' => t('Test storage failure'),
'description' => t('Dummy test storage backend. Always fails to create fields.'),
),
);
}
/**
* Implements hook_field_storage_details().
*/
function field_test_field_storage_details($field) {
$details = array();
// Add field columns.
$columns = array();
foreach ((array) $field['columns'] as $column_name => $attributes) {
$columns[$column_name] = $column_name;
}
return array(
'drupal_variables' => array(
'field_test_storage_data[FIELD_LOAD_CURRENT]' => $columns,
'field_test_storage_data[FIELD_LOAD_REVISION]' => $columns,
),
);
}
/**
* Implements hook_field_storage_details_alter().
*
* @see FieldAttachStorageTestCase::testFieldStorageDetailsAlter()
*/
function field_test_field_storage_details_alter(&$details, $field) {
// For testing, storage details are changed only because of the field name.
if ($field['field_name'] == 'field_test_change_my_details') {
$columns = array();
foreach ((array) $field['columns'] as $column_name => $attributes) {
$columns[$column_name] = $column_name;
}
$details['drupal_variables'] = array(
FIELD_LOAD_CURRENT => array(
'moon' => $columns,
),
FIELD_LOAD_REVISION => array(
'mars' => $columns,
),
);
}
}
/**
* Helper function: stores or retrieves data from the 'storage backend'.
*/
function _field_test_storage_data($data = NULL) {
if (!isset($data)) {
return variable_get('field_test_storage_data', array());
}
else {
variable_set('field_test_storage_data', $data);
}
}
/**
* Implements hook_field_storage_load().
*/
function field_test_field_storage_load($entity_type, $entities, $age, $fields, $options) {
$data = _field_test_storage_data();
$load_current = $age == FIELD_LOAD_CURRENT;
foreach ($fields as $field_id => $ids) {
$field = field_info_field_by_id($field_id);
$field_name = $field['field_name'];
$field_data = $data[$field['id']];
$sub_table = $load_current ? 'current' : 'revisions';
$delta_count = array();
foreach ($field_data[$sub_table] as $row) {
if ($row->type == $entity_type && (!$row->deleted || $options['deleted'])) {
if (($load_current && in_array($row->entity_id, $ids)) || (!$load_current && in_array($row->revision_id, $ids))) {
if (in_array($row->language, field_available_languages($entity_type, $field))) {
if (!isset($delta_count[$row->entity_id][$row->language])) {
$delta_count[$row->entity_id][$row->language] = 0;
}
if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$row->language] < $field['cardinality']) {
$item = array();
foreach ($field['columns'] as $column => $attributes) {
$item[$column] = $row->{$column};
}
$entities[$row->entity_id]->{$field_name}[$row->language][] = $item;
$delta_count[$row->entity_id][$row->language]++;
}
}
}
}
}
}
}
/**
* Implements hook_field_storage_write().
*/
function field_test_field_storage_write($entity_type, $entity, $op, $fields) {
$data = _field_test_storage_data();
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
foreach ($fields as $field_id) {
$field = field_info_field_by_id($field_id);
$field_name = $field['field_name'];
$field_data = &$data[$field_id];
$all_languages = field_available_languages($entity_type, $field);
$field_languages = array_intersect($all_languages, array_keys((array) $entity->$field_name));
// Delete and insert, rather than update, in case a value was added.
if ($op == FIELD_STORAGE_UPDATE) {
// Delete languages present in the incoming $entity->$field_name.
// Delete all languages if $entity->$field_name is empty.
$languages = !empty($entity->$field_name) ? $field_languages : $all_languages;
if ($languages) {
foreach ($field_data['current'] as $key => $row) {
if ($row->type == $entity_type && $row->entity_id == $id && in_array($row->language, $languages)) {
unset($field_data['current'][$key]);
}
}
if (isset($vid)) {
foreach ($field_data['revisions'] as $key => $row) {
if ($row->type == $entity_type && $row->revision_id == $vid) {
unset($field_data['revisions'][$key]);
}
}
}
}
}
foreach ($field_languages as $langcode) {
$items = (array) $entity->{$field_name}[$langcode];
$delta_count = 0;
foreach ($items as $delta => $item) {
$row = (object) array(
'field_id' => $field_id,
'type' => $entity_type,
'entity_id' => $id,
'revision_id' => $vid,
'bundle' => $bundle,
'delta' => $delta,
'deleted' => FALSE,
'language' => $langcode,
);
foreach ($field['columns'] as $column => $attributes) {
$row->{$column} = isset($item[$column]) ? $item[$column] : NULL;
}
$field_data['current'][] = $row;
if (isset($vid)) {
$field_data['revisions'][] = $row;
}
if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) {
break;
}
}
}
}
_field_test_storage_data($data);
}
/**
* Implements hook_field_storage_delete().
*/
function field_test_field_storage_delete($entity_type, $entity, $fields) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
// Note: reusing field_test_storage_purge(), like field_sql_storage.module
// does, is highly inefficient in our case...
foreach (field_info_instances($bundle) as $instance) {
if (isset($fields[$instance['field_id']])) {
$field = field_info_field_by_id($instance['field_id']);
field_test_field_storage_purge($entity_type, $entity, $field, $instance);
}
}
}
/**
* Implements hook_field_storage_purge().
*/
function field_test_field_storage_purge($entity_type, $entity, $field, $instance) {
$data = _field_test_storage_data();
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
$field_data = &$data[$field['id']];
foreach (array('current', 'revisions') as $sub_table) {
foreach ($field_data[$sub_table] as $key => $row) {
if ($row->type == $entity_type && $row->entity_id == $id) {
unset($field_data[$sub_table][$key]);
}
}
}
_field_test_storage_data($data);
}
/**
* Implements hook_field_storage_delete_revision().
*/
function field_test_field_storage_delete_revision($entity_type, $entity, $fields) {
$data = _field_test_storage_data();
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
foreach ($fields as $field_id) {
$field_data = &$data[$field_id];
foreach (array('current', 'revisions') as $sub_table) {
foreach ($field_data[$sub_table] as $key => $row) {
if ($row->type == $entity_type && $row->entity_id == $id && $row->revision_id == $vid) {
unset($field_data[$sub_table][$key]);
}
}
}
}
_field_test_storage_data($data);
}
/**
* Sort helper for field_test_field_storage_query().
*
* Sorts by entity type and entity id.
*/
function _field_test_field_storage_query_sort_helper($a, $b) {
if ($a->type == $b->type) {
if ($a->entity_id == $b->entity_id) {
return 0;
}
else {
return $a->entity_id < $b->entity_id ? -1 : 1;
}
}
else {
return $a->type < $b->type ? -1 : 1;
}
}
/**
* Implements hook_field_storage_create_field().
*/
function field_test_field_storage_create_field($field) {
if ($field['storage']['type'] == 'field_test_storage_failure') {
throw new Exception('field_test_storage_failure engine always fails to create fields');
}
$data = _field_test_storage_data();
$data[$field['id']] = array(
'current' => array(),
'revisions' => array(),
);
_field_test_storage_data($data);
}
/**
* Implements hook_field_storage_delete_field().
*/
function field_test_field_storage_delete_field($field) {
$data = _field_test_storage_data();
$field_data = &$data[$field['id']];
foreach (array('current', 'revisions') as $sub_table) {
foreach ($field_data[$sub_table] as &$row) {
$row->deleted = TRUE;
}
}
_field_test_storage_data($data);
}
/**
* Implements hook_field_storage_delete_instance().
*/
function field_test_field_storage_delete_instance($instance) {
$data = _field_test_storage_data();
$field = field_info_field($instance['field_name']);
$field_data = &$data[$field['id']];
foreach (array('current', 'revisions') as $sub_table) {
foreach ($field_data[$sub_table] as &$row) {
if ($row->bundle == $instance['bundle']) {
$row->deleted = TRUE;
}
}
}
_field_test_storage_data($data);
}
/**
* Implements hook_field_attach_create_bundle().
*/
function field_test_field_attach_create_bundle($bundle) {
// We don't need to do anything here.
}
/**
* Implements hook_field_attach_rename_bundle().
*/
function field_test_field_attach_rename_bundle($bundle_old, $bundle_new) {
$data = _field_test_storage_data();
// We need to account for deleted or inactive fields and instances.
$instances = field_read_instances(array('bundle' => $bundle_new), array('include_deleted' => TRUE, 'include_inactive' => TRUE));
foreach ($instances as $field_name => $instance) {
$field = field_info_field_by_id($instance['field_id']);
if ($field['storage']['type'] == 'field_test_storage') {
$field_data = &$data[$field['id']];
foreach (array('current', 'revisions') as $sub_table) {
foreach ($field_data[$sub_table] as &$row) {
if ($row->bundle == $bundle_old) {
$row->bundle = $bundle_new;
}
}
}
}
}
_field_test_storage_data($data);
}
/**
* Implements hook_field_attach_delete_bundle().
*/
function field_test_field_attach_delete_bundle($entity_type, $bundle, $instances) {
$data = _field_test_storage_data();
foreach ($instances as $instance) {
$field = field_info_field_by_id($instance['field_id']);
if ($field['storage']['type'] == 'field_test_storage') {
$field_data = &$data[$field['id']];
foreach (array('current', 'revisions') as $sub_table) {
foreach ($field_data[$sub_table] as &$row) {
if ($row->bundle == $bundle) {
$row->deleted = TRUE;
}
}
}
}
}
_field_test_storage_data($data);
}
form .field-multiple-table th.field-label {
padding-right: 0;
}
form .field-multiple-table td.field-multiple-drag {
padding-left: 0;
}
form .field-multiple-table td.field-multiple-drag a.tabledrag-handle{
padding-left: .5em;
}
.field-label-inline .field-label,
.field-label-inline .field-items {
float: right;
}
/* Field display */
.field .field-label {
font-weight: bold;
}
.field-label-inline .field-label,
.field-label-inline .field-items {
float:left; /*LTR*/
}
/* Form display */
form .field-multiple-table {
margin: 0;
}
form .field-multiple-table th.field-label {
padding-left: 0; /*LTR*/
}
form .field-multiple-table td.field-multiple-drag {
width: 30px;
padding-right: 0; /*LTR*/
}
form .field-multiple-table td.field-multiple-drag a.tabledrag-handle {
padding-right: .5em; /*LTR*/
}
form .field-add-more-submit {
margin: .5em 0 0;
}
/**
* @file
* Right-to-left specific stylesheet for the Field UI module.
*/
/* 'Manage fields' overview */
table.field-ui-overview tr.add-new .label-input {
float: right;
}
$type_bundles) {
foreach ($type_bundles as $bundle => $bundle_instances) {
foreach ($bundle_instances as $field_name => $instance) {
$field = field_info_field($field_name);
// Initialize the row if we encounter the field for the first time.
if (!isset($rows[$field_name])) {
$rows[$field_name]['class'] = $field['locked'] ? array('menu-disabled') : array('');
$rows[$field_name]['data'][0] = $field['locked'] ? t('@field_name (Locked)', array('@field_name' => $field_name)) : $field_name;
$module_name = $field_types[$field['type']]['module'];
$rows[$field_name]['data'][1] = $field_types[$field['type']]['label'] . ' ' . t('(module: !module)', array('!module' => $modules[$module_name]->info['name']));
}
// Add the current instance.
$admin_path = _field_ui_bundle_admin_path($entity_type, $bundle);
$rows[$field_name]['data'][2][] = $admin_path ? l($bundles[$entity_type][$bundle]['label'], $admin_path . '/fields') : $bundles[$entity_type][$bundle]['label'];
}
}
}
foreach ($rows as $field_name => $cell) {
$rows[$field_name]['data'][2] = implode(', ', $cell['data'][2]);
}
if (empty($rows)) {
$output = t('No fields have been defined yet.');
}
else {
// Sort rows by field name.
ksort($rows);
$output = theme('table', array('header' => $header, 'rows' => $rows));
}
return $output;
}
/**
* Displays a message listing the inactive fields of a given bundle.
*/
function field_ui_inactive_message($entity_type, $bundle) {
$inactive_instances = field_ui_inactive_instances($entity_type, $bundle);
if (!empty($inactive_instances)) {
$field_types = field_info_field_types();
$widget_types = field_info_widget_types();
foreach ($inactive_instances as $field_name => $instance) {
$list[] = t('%field (@field_name) field requires the %widget_type widget provided by %widget_module module', array(
'%field' => $instance['label'],
'@field_name' => $instance['field_name'],
'%widget_type' => isset($widget_types[$instance['widget']['type']]) ? $widget_types[$instance['widget']['type']]['label'] : $instance['widget']['type'],
'%widget_module' => $instance['widget']['module'],
));
}
drupal_set_message(t('Inactive fields are not shown unless their providing modules are enabled. The following fields are not enabled: !list', array('!list' => theme('item_list', array('items' => $list)))), 'error');
}
}
/**
* Determines the rendering order of an array representing a tree.
*
* Callback for array_reduce() within field_ui_table_pre_render().
*/
function _field_ui_reduce_order($array, $a) {
$array = !isset($array) ? array() : $array;
if ($a['name']) {
$array[] = $a['name'];
}
if (!empty($a['children'])) {
uasort($a['children'], 'drupal_sort_weight');
$array = array_merge($array, array_reduce($a['children'], '_field_ui_reduce_order'));
}
return $array;
}
/**
* Returns the region to which a row in the 'Manage fields' screen belongs.
*
* This function is used as a #region_callback in
* field_ui_field_overview_form(). It is called during
* field_ui_table_pre_render().
*/
function field_ui_field_overview_row_region($row) {
switch ($row['#row_type']) {
case 'field':
case 'extra_field':
return 'main';
case 'add_new_field':
// If no input in 'label', assume the row has not been dragged out of the
// 'add new' section.
return (!empty($row['label']['#value']) ? 'main' : 'add_new');
}
}
/**
* Returns the region to which a row in the 'Manage display' screen belongs.
*
* This function is used as a #region_callback in
* field_ui_field_overview_form(), and is called during
* field_ui_table_pre_render().
*/
function field_ui_display_overview_row_region($row) {
switch ($row['#row_type']) {
case 'field':
case 'extra_field':
return ($row['format']['type']['#value'] == 'hidden' ? 'hidden' : 'visible');
}
}
/**
* Pre-render callback for field_ui_table elements.
*/
function field_ui_table_pre_render($elements) {
$js_settings = array();
// For each region, build the tree structure from the weight and parenting
// data contained in the flat form structure, to determine row order and
// indentation.
$regions = $elements['#regions'];
$tree = array('' => array('name' => '', 'children' => array()));
$trees = array_fill_keys(array_keys($regions), $tree);
$parents = array();
$list = drupal_map_assoc(element_children($elements));
// Iterate on rows until we can build a known tree path for all of them.
while ($list) {
foreach ($list as $name) {
$row = &$elements[$name];
$parent = $row['parent_wrapper']['parent']['#value'];
// Proceed if parent is known.
if (empty($parent) || isset($parents[$parent])) {
// Grab parent, and remove the row from the next iteration.
$parents[$name] = $parent ? array_merge($parents[$parent], array($parent)) : array();
unset($list[$name]);
// Determine the region for the row.
$function = $row['#region_callback'];
$region_name = $function($row);
// Add the element in the tree.
$target = &$trees[$region_name][''];
foreach ($parents[$name] as $key) {
$target = &$target['children'][$key];
}
$target['children'][$name] = array('name' => $name, 'weight' => $row['weight']['#value']);
// Add tabledrag indentation to the first row cell.
if ($depth = count($parents[$name])) {
$children = element_children($row);
$cell = current($children);
$row[$cell]['#prefix'] = theme('indentation', array('size' => $depth)) . (isset($row[$cell]['#prefix']) ? $row[$cell]['#prefix'] : '');
}
// Add row id and associate JS settings.
$id = drupal_html_class($name);
$row['#attributes']['id'] = $id;
if (isset($row['#js_settings'])) {
$row['#js_settings'] += array(
'rowHandler' => $row['#row_type'],
'name' => $name,
'region' => $region_name,
);
$js_settings[$id] = $row['#js_settings'];
}
}
}
}
// Determine rendering order from the tree structure.
foreach ($regions as $region_name => $region) {
$elements['#regions'][$region_name]['rows_order'] = array_reduce($trees[$region_name], '_field_ui_reduce_order');
}
$elements['#attached']['js'][] = array(
'type' => 'setting',
'data' => array('fieldUIRowsData' => $js_settings),
);
return $elements;
}
/**
* Returns HTML for Field UI overview tables.
*
* @param $variables
* An associative array containing:
* - elements: An associative array containing a Form API structure to be
* rendered as a table.
*
* @ingroup themeable
*/
function theme_field_ui_table($variables) {
$elements = $variables['elements'];
$table = array();
$js_settings = array();
// Add table headers and attributes.
foreach (array('header', 'attributes') as $key) {
if (isset($elements["#$key"])) {
$table[$key] = $elements["#$key"];
}
}
// Determine the colspan to use for region rows, by checking the number of
// columns in the headers.
$columns_count = 0;
foreach ($table['header'] as $header) {
$columns_count += (is_array($header) && isset($header['colspan']) ? $header['colspan'] : 1);
}
// Render rows, region by region.
foreach ($elements['#regions'] as $region_name => $region) {
$region_name_class = drupal_html_class($region_name);
// Add region rows.
if (isset($region['title'])) {
$table['rows'][] = array(
'class' => array('region-title', 'region-' . $region_name_class . '-title'),
'no_striping' => TRUE,
'data' => array(
array('data' => $region['title'], 'colspan' => $columns_count),
),
);
}
if (isset($region['message'])) {
$class = (empty($region['rows_order']) ? 'region-empty' : 'region-populated');
$table['rows'][] = array(
'class' => array('region-message', 'region-' . $region_name_class . '-message', $class),
'no_striping' => TRUE,
'data' => array(
array('data' => $region['message'], 'colspan' => $columns_count),
),
);
}
// Add form rows, in the order determined at pre-render time.
foreach ($region['rows_order'] as $name) {
$element = $elements[$name];
$row = array('data' => array());
if (isset($element['#attributes'])) {
$row += $element['#attributes'];
}
// Render children as table cells.
foreach (element_children($element) as $cell_key) {
$child = &$element[$cell_key];
// Do not render a cell for children of #type 'value'.
if (!(isset($child['#type']) && $child['#type'] == 'value')) {
$cell = array('data' => drupal_render($child));
if (isset($child['#cell_attributes'])) {
$cell += $child['#cell_attributes'];
}
$row['data'][] = $cell;
}
}
$table['rows'][] = $row;
}
}
return theme('table', $table);
}
/**
* Form constructor for the 'Manage fields' form of a bundle.
*
* Allows fields and pseudo-fields to be re-ordered.
*
* @see field_ui_field_overview_form_validate()
* @see field_ui_field_overview_form_submit()
* @ingroup forms
*/
function field_ui_field_overview_form($form, &$form_state, $entity_type, $bundle) {
$bundle = field_extract_bundle($entity_type, $bundle);
field_ui_inactive_message($entity_type, $bundle);
$admin_path = _field_ui_bundle_admin_path($entity_type, $bundle);
// When displaying the form, make sure the list of fields is up-to-date.
if (empty($form_state['post'])) {
field_info_cache_clear();
}
// Gather bundle information.
$instances = field_info_instances($entity_type, $bundle);
$field_types = field_info_field_types();
$widget_types = field_info_widget_types();
$extra_fields = field_info_extra_fields($entity_type, $bundle, 'form');
$form += array(
'#entity_type' => $entity_type,
'#bundle' => $bundle,
'#fields' => array_keys($instances),
'#extra' => array_keys($extra_fields),
);
$table = array(
'#type' => 'field_ui_table',
'#tree' => TRUE,
'#header' => array(
t('Label'),
t('Weight'),
t('Parent'),
t('Machine name'),
t('Field type'),
t('Widget'),
array('data' => t('Operations'), 'colspan' => 2),
),
'#parent_options' => array(),
'#regions' => array(
'main' => array('message' => t('No fields are present yet.')),
'add_new' => array('title' => ' '),
),
'#attributes' => array(
'class' => array('field-ui-overview'),
'id' => 'field-overview',
),
);
// Fields.
foreach ($instances as $name => $instance) {
$field = field_info_field($instance['field_name']);
$admin_field_path = $admin_path . '/fields/' . $instance['field_name'];
$table[$name] = array(
'#attributes' => array('class' => array('draggable', 'tabledrag-leaf')),
'#row_type' => 'field',
'#region_callback' => 'field_ui_field_overview_row_region',
'label' => array(
'#markup' => check_plain($instance['label']),
),
'weight' => array(
'#type' => 'textfield',
'#title' => t('Weight for @title', array('@title' => $instance['label'])),
'#title_display' => 'invisible',
'#default_value' => $instance['widget']['weight'],
'#size' => 3,
'#attributes' => array('class' => array('field-weight')),
),
'parent_wrapper' => array(
'parent' => array(
'#type' => 'select',
'#title' => t('Parent for @title', array('@title' => $instance['label'])),
'#title_display' => 'invisible',
'#options' => $table['#parent_options'],
'#empty_value' => '',
'#attributes' => array('class' => array('field-parent')),
'#parents' => array('fields', $name, 'parent'),
),
'hidden_name' => array(
'#type' => 'hidden',
'#default_value' => $name,
'#attributes' => array('class' => array('field-name')),
),
),
'field_name' => array(
'#markup' => $instance['field_name'],
),
'type' => array(
'#type' => 'link',
'#title' => t($field_types[$field['type']]['label']),
'#href' => $admin_field_path . '/field-settings',
'#options' => array('attributes' => array('title' => t('Edit field settings.'))),
),
'widget_type' => array(
'#type' => 'link',
'#title' => t($widget_types[$instance['widget']['type']]['label']),
'#href' => $admin_field_path . '/widget-type',
'#options' => array('attributes' => array('title' => t('Change widget type.'))),
),
'edit' => array(
'#type' => 'link',
'#title' => t('edit'),
'#href' => $admin_field_path,
'#options' => array('attributes' => array('title' => t('Edit instance settings.'))),
),
'delete' => array(
'#type' => 'link',
'#title' => t('delete'),
'#href' => $admin_field_path . '/delete',
'#options' => array('attributes' => array('title' => t('Delete instance.'))),
),
);
if (!empty($instance['locked'])) {
$table[$name]['edit'] = array('#value' => t('Locked'));
$table[$name]['delete'] = array();
$table[$name]['#attributes']['class'][] = 'menu-disabled';
}
}
// Non-field elements.
foreach ($extra_fields as $name => $extra_field) {
$table[$name] = array(
'#attributes' => array('class' => array('draggable', 'tabledrag-leaf')),
'#row_type' => 'extra_field',
'#region_callback' => 'field_ui_field_overview_row_region',
'label' => array(
'#markup' => check_plain($extra_field['label']),
),
'weight' => array(
'#type' => 'textfield',
'#default_value' => $extra_field['weight'],
'#size' => 3,
'#attributes' => array('class' => array('field-weight')),
'#title_display' => 'invisible',
'#title' => t('Weight for @title', array('@title' => $extra_field['label'])),
),
'parent_wrapper' => array(
'parent' => array(
'#type' => 'select',
'#title' => t('Parent for @title', array('@title' => $extra_field['label'])),
'#title_display' => 'invisible',
'#options' => $table['#parent_options'],
'#empty_value' => '',
'#attributes' => array('class' => array('field-parent')),
'#parents' => array('fields', $name, 'parent'),
),
'hidden_name' => array(
'#type' => 'hidden',
'#default_value' => $name,
'#attributes' => array('class' => array('field-name')),
),
),
'field_name' => array(
'#markup' => $name,
),
'type' => array(
'#markup' => isset($extra_field['description']) ? $extra_field['description'] : '',
'#cell_attributes' => array('colspan' => 2),
),
'edit' => array(
'#markup' => isset($extra_field['edit']) ? $extra_field['edit'] : '',
),
'delete' => array(
'#markup' => isset($extra_field['delete']) ? $extra_field['delete'] : '',
),
);
}
// Additional row: add new field.
$max_weight = field_info_max_weight($entity_type, $bundle, 'form');
$field_type_options = field_ui_field_type_options();
$widget_type_options = field_ui_widget_type_options(NULL, TRUE);
if ($field_type_options && $widget_type_options) {
$name = '_add_new_field';
$table[$name] = array(
'#attributes' => array('class' => array('draggable', 'tabledrag-leaf', 'add-new')),
'#row_type' => 'add_new_field',
'#region_callback' => 'field_ui_field_overview_row_region',
'label' => array(
'#type' => 'textfield',
'#title' => t('New field label'),
'#title_display' => 'invisible',
'#size' => 15,
'#description' => t('Label'),
'#prefix' => '',
),
'weight' => array(
'#type' => 'textfield',
'#default_value' => $max_weight + 1,
'#size' => 3,
'#title_display' => 'invisible',
'#title' => t('Weight for new field'),
'#attributes' => array('class' => array('field-weight')),
'#prefix' => '
',
),
'parent_wrapper' => array(
'parent' => array(
'#type' => 'select',
'#title' => t('Parent for new field'),
'#title_display' => 'invisible',
'#options' => $table['#parent_options'],
'#empty_value' => '',
'#attributes' => array('class' => array('field-parent')),
'#prefix' => '
',
'#parents' => array('fields', $name, 'parent'),
),
'hidden_name' => array(
'#type' => 'hidden',
'#default_value' => $name,
'#attributes' => array('class' => array('field-name')),
),
),
'field_name' => array(
'#type' => 'machine_name',
'#title' => t('New field name'),
'#title_display' => 'invisible',
// This field should stay LTR even for RTL languages.
'#field_prefix' => 'field_',
'#field_suffix' => ' ',
'#size' => 15,
'#description' => t('A unique machine-readable name containing letters, numbers, and underscores.'),
// 32 characters minus the 'field_' prefix.
'#maxlength' => 26,
'#prefix' => '
',
'#machine_name' => array(
'source' => array('fields', $name, 'label'),
'exists' => '_field_ui_field_name_exists',
'standalone' => TRUE,
'label' => '',
),
'#required' => FALSE,
),
'type' => array(
'#type' => 'select',
'#title' => t('Type of new field'),
'#title_display' => 'invisible',
'#options' => $field_type_options,
'#empty_option' => t('- Select a field type -'),
'#description' => t('Type of data to store.'),
'#attributes' => array('class' => array('field-type-select')),
'#prefix' => '
',
),
'widget_type' => array(
'#type' => 'select',
'#title' => t('Widget for new field'),
'#title_display' => 'invisible',
'#options' => $widget_type_options,
'#empty_option' => t('- Select a widget -'),
'#description' => t('Form element to edit the data.'),
'#attributes' => array('class' => array('widget-type-select')),
'#cell_attributes' => array('colspan' => 3),
'#prefix' => '
',
),
// Place the 'translatable' property as an explicit value so that contrib
// modules can form_alter() the value for newly created fields.
'translatable' => array(
'#type' => 'value',
'#value' => FALSE,
),
);
}
// Additional row: add existing field.
$existing_fields = field_ui_existing_field_options($entity_type, $bundle);
if ($existing_fields && $widget_type_options) {
// Build list of options.
$existing_field_options = array();
foreach ($existing_fields as $field_name => $info) {
$text = t('@type: @field (@label)', array(
'@type' => $info['type_label'],
'@label' => $info['label'],
'@field' => $info['field'],
));
$existing_field_options[$field_name] = truncate_utf8($text, 80, FALSE, TRUE);
}
asort($existing_field_options);
$name = '_add_existing_field';
$table[$name] = array(
'#attributes' => array('class' => array('draggable', 'tabledrag-leaf', 'add-new')),
'#row_type' => 'add_new_field',
'#region_callback' => 'field_ui_field_overview_row_region',
'label' => array(
'#type' => 'textfield',
'#title' => t('Existing field label'),
'#title_display' => 'invisible',
'#size' => 15,
'#description' => t('Label'),
'#attributes' => array('class' => array('label-textfield')),
'#prefix' => '',
),
'weight' => array(
'#type' => 'textfield',
'#default_value' => $max_weight + 2,
'#size' => 3,
'#title_display' => 'invisible',
'#title' => t('Weight for added field'),
'#attributes' => array('class' => array('field-weight')),
'#prefix' => '
',
),
'parent_wrapper' => array(
'parent' => array(
'#type' => 'select',
'#title' => t('Parent for existing field'),
'#title_display' => 'invisible',
'#options' => $table['#parent_options'],
'#empty_value' => '',
'#attributes' => array('class' => array('field-parent')),
'#prefix' => '
',
'#parents' => array('fields', $name, 'parent'),
),
'hidden_name' => array(
'#type' => 'hidden',
'#default_value' => $name,
'#attributes' => array('class' => array('field-name')),
),
),
'field_name' => array(
'#type' => 'select',
'#title' => t('Existing field to share'),
'#title_display' => 'invisible',
'#options' => $existing_field_options,
'#empty_option' => t('- Select an existing field -'),
'#description' => t('Field to share'),
'#attributes' => array('class' => array('field-select')),
'#cell_attributes' => array('colspan' => 2),
'#prefix' => '
',
),
'widget_type' => array(
'#type' => 'select',
'#title' => t('Widget for existing field'),
'#title_display' => 'invisible',
'#options' => $widget_type_options,
'#empty_option' => t('- Select a widget -'),
'#description' => t('Form element to edit the data.'),
'#attributes' => array('class' => array('widget-type-select')),
'#cell_attributes' => array('colspan' => 3),
'#prefix' => '
',
),
);
}
$form['fields'] = $table;
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'));
$form['#attached']['css'][] = drupal_get_path('module', 'field_ui') . '/field_ui.css';
$form['#attached']['js'][] = drupal_get_path('module', 'field_ui') . '/field_ui.js';
// Add settings for the update selects behavior.
$js_fields = array();
foreach ($existing_fields as $field_name => $info) {
$js_fields[$field_name] = array('label' => $info['label'], 'type' => $info['type'], 'widget' => $info['widget_type']);
}
$form['#attached']['js'][] = array(
'type' => 'setting',
'data' => array('fields' => $js_fields, 'fieldWidgetTypes' => field_ui_widget_type_options()),
);
// Add tabledrag behavior.
$form['#attached']['drupal_add_tabledrag'][] = array('field-overview', 'order', 'sibling', 'field-weight');
$form['#attached']['drupal_add_tabledrag'][] = array('field-overview', 'match', 'parent', 'field-parent', 'field-parent', 'field-name');
return $form;
}
/**
* Form validation handler for field_ui_field_overview_form().
*
* @see field_ui_field_overview_form_submit()
*/
function field_ui_field_overview_form_validate($form, &$form_state) {
_field_ui_field_overview_form_validate_add_new($form, $form_state);
_field_ui_field_overview_form_validate_add_existing($form, $form_state);
}
/**
* Validates the 'add new field' row of field_ui_field_overview_form().
*
* @see field_ui_field_overview_form_validate()
*/
function _field_ui_field_overview_form_validate_add_new($form, &$form_state) {
$field = $form_state['values']['fields']['_add_new_field'];
// Validate if any information was provided in the 'add new field' row.
if (array_filter(array($field['label'], $field['field_name'], $field['type'], $field['widget_type']))) {
// Missing label.
if (!$field['label']) {
form_set_error('fields][_add_new_field][label', t('Add new field: you need to provide a label.'));
}
// Missing field name.
if (!$field['field_name']) {
form_set_error('fields][_add_new_field][field_name', t('Add new field: you need to provide a field name.'));
}
// Field name validation.
else {
$field_name = $field['field_name'];
// Add the 'field_' prefix.
$field_name = 'field_' . $field_name;
form_set_value($form['fields']['_add_new_field']['field_name'], $field_name, $form_state);
}
// Missing field type.
if (!$field['type']) {
form_set_error('fields][_add_new_field][type', t('Add new field: you need to select a field type.'));
}
// Missing widget type.
if (!$field['widget_type']) {
form_set_error('fields][_add_new_field][widget_type', t('Add new field: you need to select a widget.'));
}
// Wrong widget type.
elseif ($field['type']) {
$widget_types = field_ui_widget_type_options($field['type']);
if (!isset($widget_types[$field['widget_type']])) {
form_set_error('fields][_add_new_field][widget_type', t('Add new field: invalid widget.'));
}
}
}
}
/**
* Render API callback: Checks if a field machine name is taken.
*
* @param $value
* The machine name, not prefixed with 'field_'.
*
* @return
* Whether or not the field machine name is taken.
*/
function _field_ui_field_name_exists($value) {
// Prefix with 'field_'.
$field_name = 'field_' . $value;
// We need to check inactive fields as well, so we can't use
// field_info_fields().
return (bool) field_read_fields(array('field_name' => $field_name), array('include_inactive' => TRUE));
}
/**
* Validates the 'add existing field' row of field_ui_field_overview_form().
*
* @see field_ui_field_overview_form_validate()
*/
function _field_ui_field_overview_form_validate_add_existing($form, &$form_state) {
// The form element might be absent if no existing fields can be added to
// this bundle.
if (isset($form_state['values']['fields']['_add_existing_field'])) {
$field = $form_state['values']['fields']['_add_existing_field'];
// Validate if any information was provided in the 'add existing field' row.
if (array_filter(array($field['label'], $field['field_name'], $field['widget_type']))) {
// Missing label.
if (!$field['label']) {
form_set_error('fields][_add_existing_field][label', t('Add existing field: you need to provide a label.'));
}
// Missing existing field name.
if (!$field['field_name']) {
form_set_error('fields][_add_existing_field][field_name', t('Add existing field: you need to select a field.'));
}
// Missing widget type.
if (!$field['widget_type']) {
form_set_error('fields][_add_existing_field][widget_type', t('Add existing field: you need to select a widget.'));
}
// Wrong widget type.
elseif ($field['field_name'] && ($existing_field = field_info_field($field['field_name']))) {
$widget_types = field_ui_widget_type_options($existing_field['type']);
if (!isset($widget_types[$field['widget_type']])) {
form_set_error('fields][_add_existing_field][widget_type', t('Add existing field: invalid widget.'));
}
}
}
}
}
/**
* Form submission handler for field_ui_field_overview_form().
*
* @see field_ui_field_overview_form_validate()
*/
function field_ui_field_overview_form_submit($form, &$form_state) {
$form_values = $form_state['values']['fields'];
$entity_type = $form['#entity_type'];
$bundle = $form['#bundle'];
$admin_path = _field_ui_bundle_admin_path($entity_type, $bundle);
$bundle_settings = field_bundle_settings($entity_type, $bundle);
// Update field weights.
foreach ($form_values as $key => $values) {
if (in_array($key, $form['#fields'])) {
$instance = field_read_instance($entity_type, $key, $bundle);
$instance['widget']['weight'] = $values['weight'];
field_update_instance($instance);
}
elseif (in_array($key, $form['#extra'])) {
$bundle_settings['extra_fields']['form'][$key]['weight'] = $values['weight'];
}
}
field_bundle_settings($entity_type, $bundle, $bundle_settings);
$destinations = array();
// Check if the target entity uses a non numeric ID.
$entity_info = entity_get_info($entity_type);
if (!empty($entity_info['entity_id_type']) && $entity_info['entity_id_type'] === 'string') {
$entity_id_type = 'string';
} else {
$entity_id_type = NULL;
}
// Create new field.
$field = array();
if (!empty($form_values['_add_new_field']['field_name'])) {
$values = $form_values['_add_new_field'];
$field = array(
'field_name' => $values['field_name'],
'type' => $values['type'],
'translatable' => $values['translatable'],
'entity_id_type' => $entity_id_type,
);
$instance = array(
'field_name' => $field['field_name'],
'entity_type' => $entity_type,
'bundle' => $bundle,
'label' => $values['label'],
'widget' => array(
'type' => $values['widget_type'],
'weight' => $values['weight'],
),
);
// Create the field and instance.
try {
field_create_field($field);
field_create_instance($instance);
$destinations[] = $admin_path . '/fields/' . $field['field_name'] . '/field-settings';
$destinations[] = $admin_path . '/fields/' . $field['field_name'];
// Store new field information for any additional submit handlers.
$form_state['fields_added']['_add_new_field'] = $field['field_name'];
}
catch (Exception $e) {
drupal_set_message(t('There was a problem creating field %label: !message', array('%label' => $instance['label'], '!message' => $e->getMessage())), 'error');
}
}
// Add existing field.
if (!empty($form_values['_add_existing_field']['field_name'])) {
$values = $form_values['_add_existing_field'];
$field = field_info_field($values['field_name']);
if (!empty($field['locked'])) {
drupal_set_message(t('The field %label cannot be added because it is locked.', array('%label' => $values['label'])), 'error');
}
else {
$instance = array(
'field_name' => $field['field_name'],
'entity_type' => $entity_type,
'bundle' => $bundle,
'label' => $values['label'],
'widget' => array(
'type' => $values['widget_type'],
'weight' => $values['weight'],
),
);
try {
field_create_instance($instance);
$destinations[] = $admin_path . '/fields/' . $instance['field_name'] . '/edit';
// Store new field information for any additional submit handlers.
$form_state['fields_added']['_add_existing_field'] = $instance['field_name'];
}
catch (Exception $e) {
drupal_set_message(t('There was a problem creating field instance %label: @message.', array('%label' => $instance['label'], '@message' => $e->getMessage())), 'error');
}
}
}
if ($destinations) {
$destination = drupal_get_destination();
$destinations[] = $destination['destination'];
unset($_GET['destination']);
$form_state['redirect'] = field_ui_get_destinations($destinations);
}
else {
drupal_set_message(t('Your settings have been saved.'));
}
}
/**
* Form constructor for the field display settings for a given view mode.
*
* @see field_ui_display_overview_multistep_submit()
* @see field_ui_display_overview_form_submit()
* @ingroup forms
*/
function field_ui_display_overview_form($form, &$form_state, $entity_type, $bundle, $view_mode) {
$bundle = field_extract_bundle($entity_type, $bundle);
field_ui_inactive_message($entity_type, $bundle);
$admin_path = _field_ui_bundle_admin_path($entity_type, $bundle);
// Gather type information.
$instances = field_info_instances($entity_type, $bundle);
$field_types = field_info_field_types();
$extra_fields = field_info_extra_fields($entity_type, $bundle, 'display');
$form_state += array(
'formatter_settings_edit' => NULL,
);
$form += array(
'#entity_type' => $entity_type,
'#bundle' => $bundle,
'#view_mode' => $view_mode,
'#fields' => array_keys($instances),
'#extra' => array_keys($extra_fields),
);
if (empty($instances) && empty($extra_fields)) {
drupal_set_message(t('There are no fields yet added. You can add new fields on the Manage fields page.', array('@link' => url($admin_path . '/fields'))), 'warning');
return $form;
}
$table = array(
'#type' => 'field_ui_table',
'#tree' => TRUE,
'#header' => array(
t('Field'),
t('Weight'),
t('Parent'),
t('Label'),
array('data' => t('Format'), 'colspan' => 3),
),
'#regions' => array(
'visible' => array('message' => t('No field is displayed.')),
'hidden' => array('title' => t('Hidden'), 'message' => t('No field is hidden.')),
),
'#parent_options' => array(),
'#attributes' => array(
'class' => array('field-ui-overview'),
'id' => 'field-display-overview',
),
// Add Ajax wrapper.
'#prefix' => '',
'#suffix' => '
',
);
$field_label_options = array(
'above' => t('Above'),
'inline' => t('Inline'),
'hidden' => '<' . t('Hidden') . '>',
);
$extra_visibility_options = array(
'visible' => t('Visible'),
'hidden' => t('Hidden'),
);
// Field rows.
foreach ($instances as $name => $instance) {
$field = field_info_field($instance['field_name']);
$display = $instance['display'][$view_mode];
$table[$name] = array(
'#attributes' => array('class' => array('draggable', 'tabledrag-leaf')),
'#row_type' => 'field',
'#region_callback' => 'field_ui_display_overview_row_region',
'#js_settings' => array(
'rowHandler' => 'field',
'defaultFormatter' => $field_types[$field['type']]['default_formatter'],
),
'human_name' => array(
'#markup' => check_plain($instance['label']),
),
'weight' => array(
'#type' => 'textfield',
'#title' => t('Weight for @title', array('@title' => $instance['label'])),
'#title_display' => 'invisible',
'#default_value' => $display['weight'],
'#size' => 3,
'#attributes' => array('class' => array('field-weight')),
),
'parent_wrapper' => array(
'parent' => array(
'#type' => 'select',
'#title' => t('Label display for @title', array('@title' => $instance['label'])),
'#title_display' => 'invisible',
'#options' => $table['#parent_options'],
'#empty_value' => '',
'#attributes' => array('class' => array('field-parent')),
'#parents' => array('fields', $name, 'parent'),
),
'hidden_name' => array(
'#type' => 'hidden',
'#default_value' => $name,
'#attributes' => array('class' => array('field-name')),
),
),
'label' => array(
'#type' => 'select',
'#title' => t('Label display for @title', array('@title' => $instance['label'])),
'#title_display' => 'invisible',
'#options' => $field_label_options,
'#default_value' => $display['label'],
),
);
$formatter_options = field_ui_formatter_options($field['type']);
$formatter_options['hidden'] = '<' . t('Hidden') . '>';
$table[$name]['format'] = array(
'type' => array(
'#type' => 'select',
'#title' => t('Formatter for @title', array('@title' => $instance['label'])),
'#title_display' => 'invisible',
'#options' => $formatter_options,
'#default_value' => $display['type'],
'#parents' => array('fields', $name, 'type'),
'#attributes' => array('class' => array('field-formatter-type')),
),
'settings_edit_form' => array(),
);
// Formatter settings.
// Check the currently selected formatter, and merge persisted values for
// formatter settings.
if (isset($form_state['values']['fields'][$name]['type'])) {
$formatter_type = $form_state['values']['fields'][$name]['type'];
}
else {
$formatter_type = $display['type'];
}
if (isset($form_state['formatter_settings'][$name])) {
$settings = $form_state['formatter_settings'][$name];
}
else {
$settings = $display['settings'];
}
$settings += field_info_formatter_settings($formatter_type);
$instance['display'][$view_mode]['type'] = $formatter_type;
$formatter = field_info_formatter_types($formatter_type);
// For hidden fields, $formatter will be NULL, but we expect an array later.
// To maintain BC, but avoid PHP 7.4 Notices, ensure $formatter is an array
// with a 'module' element.
$formatter['module'] = isset($formatter['module']) ? $formatter['module'] : '';
$instance['display'][$view_mode]['module'] = $formatter['module'];
$instance['display'][$view_mode]['settings'] = $settings;
// Base button element for the various formatter settings actions.
$base_button = array(
'#submit' => array('field_ui_display_overview_multistep_submit'),
'#ajax' => array(
'callback' => 'field_ui_display_overview_multistep_js',
'wrapper' => 'field-display-overview-wrapper',
'effect' => 'fade',
),
'#field_name' => $name,
);
if ($form_state['formatter_settings_edit'] == $name) {
// We are currently editing this field's formatter settings. Display the
// settings form and submit buttons.
$table[$name]['format']['settings_edit_form'] = array();
$settings_form = array();
$function = $formatter['module'] . '_field_formatter_settings_form';
if (function_exists($function)) {
$settings_form = $function($field, $instance, $view_mode, $form, $form_state);
}
if ($settings_form) {
$table[$name]['format']['#cell_attributes'] = array('colspan' => 3);
$table[$name]['format']['settings_edit_form'] = array(
'#type' => 'container',
'#attributes' => array('class' => array('field-formatter-settings-edit-form')),
'#parents' => array('fields', $name, 'settings_edit_form'),
'label' => array(
'#markup' => t('Format settings:') . ' ' . $formatter['label'] . ' ',
),
'settings' => $settings_form,
'actions' => array(
'#type' => 'actions',
'save_settings' => $base_button + array(
'#type' => 'submit',
'#name' => $name . '_formatter_settings_update',
'#value' => t('Update'),
'#op' => 'update',
),
'cancel_settings' => $base_button + array(
'#type' => 'submit',
'#name' => $name . '_formatter_settings_cancel',
'#value' => t('Cancel'),
'#op' => 'cancel',
// Do not check errors for the 'Cancel' button, but make sure we
// get the value of the 'formatter type' select.
'#limit_validation_errors' => array(array('fields', $name, 'type')),
),
),
);
$table[$name]['#attributes']['class'][] = 'field-formatter-settings-editing';
}
}
else {
// Display a summary of the current formatter settings.
$summary = module_invoke($formatter['module'], 'field_formatter_settings_summary', $field, $instance, $view_mode);
$table[$name]['settings_summary'] = array();
$table[$name]['settings_edit'] = array();
if ($summary) {
$table[$name]['settings_summary'] = array(
'#markup' => '' . $summary . '
',
'#cell_attributes' => array('class' => array('field-formatter-summary-cell')),
);
$table[$name]['settings_edit'] = $base_button + array(
'#type' => 'image_button',
'#name' => $name . '_formatter_settings_edit',
'#src' => 'misc/configure.png',
'#attributes' => array('class' => array('field-formatter-settings-edit'), 'alt' => t('Edit')),
'#op' => 'edit',
// Do not check errors for the 'Edit' button, but make sure we get
// the value of the 'formatter type' select.
'#limit_validation_errors' => array(array('fields', $name, 'type')),
'#prefix' => '',
'#suffix' => '
',
);
}
}
}
// Non-field elements.
foreach ($extra_fields as $name => $extra_field) {
$display = $extra_field['display'][$view_mode];
$table[$name] = array(
'#attributes' => array('class' => array('draggable', 'tabledrag-leaf')),
'#row_type' => 'extra_field',
'#region_callback' => 'field_ui_display_overview_row_region',
'#js_settings' => array('rowHandler' => 'field'),
'human_name' => array(
'#markup' => check_plain($extra_field['label']),
),
'weight' => array(
'#type' => 'textfield',
'#title' => t('Weight for @title', array('@title' => $extra_field['label'])),
'#title_display' => 'invisible',
'#default_value' => $display['weight'],
'#size' => 3,
'#attributes' => array('class' => array('field-weight')),
),
'parent_wrapper' => array(
'parent' => array(
'#type' => 'select',
'#title' => t('Parents for @title', array('@title' => $extra_field['label'])),
'#title_display' => 'invisible',
'#options' => $table['#parent_options'],
'#empty_value' => '',
'#attributes' => array('class' => array('field-parent')),
'#parents' => array('fields', $name, 'parent'),
),
'hidden_name' => array(
'#type' => 'hidden',
'#default_value' => $name,
'#attributes' => array('class' => array('field-name')),
),
),
'empty_cell' => array(
'#markup' => ' ',
),
'format' => array(
'type' => array(
'#type' => 'select',
'#title' => t('Visibility for @title', array('@title' => $extra_field['label'])),
'#title_display' => 'invisible',
'#options' => $extra_visibility_options,
'#default_value' => $display['visible'] ? 'visible' : 'hidden',
'#parents' => array('fields', $name, 'type'),
'#attributes' => array('class' => array('field-formatter-type')),
),
),
'settings_summary' => array(),
'settings_edit' => array(),
);
}
$form['fields'] = $table;
// Custom display settings.
if ($view_mode == 'default') {
$form['modes'] = array(
'#type' => 'fieldset',
'#title' => t('Custom display settings'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
// Collect options and default values for the 'Custom display settings'
// checkboxes.
$options = array();
$default = array();
$entity_info = entity_get_info($entity_type);
$view_modes = $entity_info['view modes'];
$view_mode_settings = field_view_mode_settings($entity_type, $bundle);
foreach ($view_modes as $view_mode_name => $view_mode_info) {
$options[$view_mode_name] = $view_mode_info['label'];
if (!empty($view_mode_settings[$view_mode_name]['custom_settings'])) {
$default[] = $view_mode_name;
}
}
$form['modes']['view_modes_custom'] = array(
'#type' => 'checkboxes',
'#title' => t('Use custom display settings for the following view modes'),
'#options' => $options,
'#default_value' => $default,
);
}
// In overviews involving nested rows from contributed modules (i.e
// field_group), the 'format type' selects can trigger a series of changes in
// child rows. The #ajax behavior is therefore not attached directly to the
// selects, but triggered by the client-side script through a hidden #ajax
// 'Refresh' button. A hidden 'refresh_rows' input tracks the name of
// affected rows.
$form['refresh_rows'] = array('#type' => 'hidden');
$form['refresh'] = array(
'#type' => 'submit',
'#value' => t('Refresh'),
'#op' => 'refresh_table',
'#submit' => array('field_ui_display_overview_multistep_submit'),
'#ajax' => array(
'callback' => 'field_ui_display_overview_multistep_js',
'wrapper' => 'field-display-overview-wrapper',
'effect' => 'fade',
// The button stays hidden, so we hide the Ajax spinner too. Ad-hoc
// spinners will be added manually by the client-side script.
'progress' => 'none',
),
);
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'));
$form['#attached']['js'][] = drupal_get_path('module', 'field_ui') . '/field_ui.js';
$form['#attached']['css'][] = drupal_get_path('module', 'field_ui') . '/field_ui.css';
// Add tabledrag behavior.
$form['#attached']['drupal_add_tabledrag'][] = array('field-display-overview', 'order', 'sibling', 'field-weight');
$form['#attached']['drupal_add_tabledrag'][] = array('field-display-overview', 'match', 'parent', 'field-parent', 'field-parent', 'field-name');
return $form;
}
/**
* Form submission handler for buttons in field_ui_display_overview_form().
*/
function field_ui_display_overview_multistep_submit($form, &$form_state) {
$trigger = $form_state['triggering_element'];
$op = $trigger['#op'];
switch ($op) {
case 'edit':
// Store the field whose settings are currently being edited.
$field_name = $trigger['#field_name'];
$form_state['formatter_settings_edit'] = $field_name;
break;
case 'update':
// Store the saved settings, and set the field back to 'non edit' mode.
$field_name = $trigger['#field_name'];
$values = $form_state['values']['fields'][$field_name]['settings_edit_form']['settings'];
$form_state['formatter_settings'][$field_name] = $values;
unset($form_state['formatter_settings_edit']);
break;
case 'cancel':
// Set the field back to 'non edit' mode.
unset($form_state['formatter_settings_edit']);
break;
case 'refresh_table':
// If the currently edited field is one of the rows to be refreshed, set
// it back to 'non edit' mode.
$updated_rows = explode(' ', $form_state['values']['refresh_rows']);
if (isset($form_state['formatter_settings_edit']) && in_array($form_state['formatter_settings_edit'], $updated_rows)) {
unset($form_state['formatter_settings_edit']);
}
break;
}
$form_state['rebuild'] = TRUE;
}
/**
* Ajax handler for multistep buttons on the 'Manage display' screen.
*/
function field_ui_display_overview_multistep_js($form, &$form_state) {
$trigger = $form_state['triggering_element'];
$op = $trigger['#op'];
// Pick the elements that need to receive the ajax-new-content effect.
switch ($op) {
case 'edit':
$updated_rows = array($trigger['#field_name']);
$updated_columns = array('format');
break;
case 'update':
case 'cancel':
$updated_rows = array($trigger['#field_name']);
$updated_columns = array('format', 'settings_summary', 'settings_edit');
break;
case 'refresh_table':
$updated_rows = array_values(explode(' ', $form_state['values']['refresh_rows']));
$updated_columns = array('settings_summary', 'settings_edit');
break;
}
foreach ($updated_rows as $name) {
foreach ($updated_columns as $key) {
$element = &$form['fields'][$name][$key];
$element['#prefix'] = '' . (isset($element['#prefix']) ? $element['#prefix'] : '');
$element['#suffix'] = (isset($element['#suffix']) ? $element['#suffix'] : '') . '
';
}
}
// Return the whole table.
return $form['fields'];
}
/**
* Form submission handler for field_ui_display_overview_form().
*/
function field_ui_display_overview_form_submit($form, &$form_state) {
$form_values = $form_state['values'];
$entity_type = $form['#entity_type'];
$bundle = $form['#bundle'];
$view_mode = $form['#view_mode'];
// Save data for 'regular' fields.
foreach ($form['#fields'] as $field_name) {
// Retrieve the stored instance settings to merge with the incoming values.
$instance = field_read_instance($entity_type, $field_name, $bundle);
$values = $form_values['fields'][$field_name];
// Get formatter settings. They lie either directly in submitted form
// values (if the whole form was submitted while some formatter
// settings were being edited), or have been persisted in
// $form_state.
$settings = array();
if (isset($values['settings_edit_form']['settings'])) {
$settings = $values['settings_edit_form']['settings'];
}
elseif (isset($form_state['formatter_settings'][$field_name])) {
$settings = $form_state['formatter_settings'][$field_name];
}
elseif (isset($instance['display'][$view_mode]['settings'])) {
$settings = $instance['display'][$view_mode]['settings'];
}
// Only save settings actually used by the selected formatter.
$default_settings = field_info_formatter_settings($values['type']);
$settings = array_intersect_key($settings, $default_settings);
$instance['display'][$view_mode] = array(
'label' => $values['label'],
'type' => $values['type'],
'weight' => $values['weight'],
'settings' => $settings,
);
field_update_instance($instance);
}
// Get current bundle settings.
$bundle_settings = field_bundle_settings($entity_type, $bundle);
// Save data for 'extra' fields.
foreach ($form['#extra'] as $name) {
$bundle_settings['extra_fields']['display'][$name][$view_mode] = array(
'weight' => $form_values['fields'][$name]['weight'],
'visible' => $form_values['fields'][$name]['type'] == 'visible',
);
}
// Save view modes data.
if ($view_mode == 'default') {
$entity_info = entity_get_info($entity_type);
foreach ($form_values['view_modes_custom'] as $view_mode_name => $value) {
// Display a message for each view mode newly configured to use custom
// settings.
$view_mode_settings = field_view_mode_settings($entity_type, $bundle);
if (!empty($value) && empty($view_mode_settings[$view_mode_name]['custom_settings'])) {
$view_mode_label = $entity_info['view modes'][$view_mode_name]['label'];
$path = _field_ui_bundle_admin_path($entity_type, $bundle) . "/display/$view_mode_name";
drupal_set_message(t('The %view_mode mode now uses custom display settings. You might want to configure them .', array('%view_mode' => $view_mode_label, '@url' => url($path))));
// Initialize the newly customized view mode with the display settings
// from the default view mode.
_field_ui_add_default_view_mode_settings($entity_type, $bundle, $view_mode_name, $bundle_settings);
}
$bundle_settings['view_modes'][$view_mode_name]['custom_settings'] = !empty($value);
}
}
// Save updated bundle settings.
field_bundle_settings($entity_type, $bundle, $bundle_settings);
drupal_set_message(t('Your settings have been saved.'));
}
/**
* Populates display settings for a new view mode from the default view mode.
*
* When an administrator decides to use custom display settings for a view mode,
* that view mode needs to be initialized with the display settings for the
* 'default' view mode, which it was previously using. This helper function
* adds the new custom display settings to this bundle's instances, and saves
* them. It also modifies the passed-in $settings array, which the caller can
* then save using field_bundle_settings().
*
* @param $entity_type
* The bundle's entity type.
* @param $bundle
* The bundle whose view mode is being customized.
* @param $view_mode
* The view mode that the administrator has set to use custom settings.
* @param $settings
* An associative array of bundle settings, as expected by
* field_bundle_settings().
*
* @see field_ui_display_overview_form_submit().
* @see field_bundle_settings()
*/
function _field_ui_add_default_view_mode_settings($entity_type, $bundle, $view_mode, &$settings) {
// Update display settings for field instances.
$instances = field_read_instances(array('entity_type' => $entity_type, 'bundle' => $bundle));
foreach ($instances as $instance) {
// If this field instance has display settings defined for this view mode,
// respect those settings.
if (!isset($instance['display'][$view_mode])) {
// The instance doesn't specify anything for this view mode, so use the
// default display settings.
$instance['display'][$view_mode] = $instance['display']['default'];
field_update_instance($instance);
}
}
// Update display settings for 'extra fields'.
foreach (array_keys($settings['extra_fields']['display']) as $name) {
if (!isset($settings['extra_fields']['display'][$name][$view_mode])) {
$settings['extra_fields']['display'][$name][$view_mode] = $settings['extra_fields']['display'][$name]['default'];
}
}
}
/**
* Returns an array of field_type options.
*/
function field_ui_field_type_options() {
$options = &drupal_static(__FUNCTION__);
if (!isset($options)) {
$options = array();
$field_types = field_info_field_types();
$field_type_options = array();
foreach ($field_types as $name => $field_type) {
// Skip field types which have no widget types, or should not be add via
// uesr interface.
if (field_ui_widget_type_options($name) && empty($field_type['no_ui'])) {
$options[$name] = $field_type['label'];
}
}
asort($options);
}
return $options;
}
/**
* Returns an array of widget type options for a field type.
*
* If no field type is provided, returns a nested array of all widget types,
* keyed by field type human name.
*/
function field_ui_widget_type_options($field_type = NULL, $by_label = FALSE) {
$options = &drupal_static(__FUNCTION__);
if (!isset($options)) {
$options = array();
$field_types = field_info_field_types();
foreach (field_info_widget_types() as $name => $widget_type) {
foreach ($widget_type['field types'] as $widget_field_type) {
// Check that the field type exists.
if (isset($field_types[$widget_field_type])) {
$options[$widget_field_type][$name] = $widget_type['label'];
}
}
}
}
if (isset($field_type)) {
return !empty($options[$field_type]) ? $options[$field_type] : array();
}
if ($by_label) {
$field_types = field_info_field_types();
$options_by_label = array();
foreach ($options as $field_type => $widgets) {
$options_by_label[$field_types[$field_type]['label']] = $widgets;
}
return $options_by_label;
}
return $options;
}
/**
* Returns an array of formatter options for a field type.
*
* If no field type is provided, returns a nested array of all formatters, keyed
* by field type.
*/
function field_ui_formatter_options($field_type = NULL) {
$options = &drupal_static(__FUNCTION__);
if (!isset($options)) {
$field_types = field_info_field_types();
$options = array();
foreach (field_info_formatter_types() as $name => $formatter) {
foreach ($formatter['field types'] as $formatter_field_type) {
// Check that the field type exists.
if (isset($field_types[$formatter_field_type])) {
$options[$formatter_field_type][$name] = $formatter['label'];
}
}
}
}
if ($field_type) {
return !empty($options[$field_type]) ? $options[$field_type] : array();
}
return $options;
}
/**
* Returns an array of existing fields to be added to a bundle.
*/
function field_ui_existing_field_options($entity_type, $bundle) {
$info = array();
$field_types = field_info_field_types();
foreach (field_info_instances() as $existing_entity_type => $bundles) {
foreach ($bundles as $existing_bundle => $instances) {
// No need to look in the current bundle.
if (!($existing_bundle == $bundle && $existing_entity_type == $entity_type)) {
foreach ($instances as $instance) {
$field = field_info_field($instance['field_name']);
// Don't show
// - locked fields,
// - fields already in the current bundle,
// - fields that cannot be added to the entity type,
// - fields that should not be added via user interface.
if (empty($field['locked'])
&& !field_info_instance($entity_type, $field['field_name'], $bundle)
&& (empty($field['entity_types']) || in_array($entity_type, $field['entity_types']))
&& empty($field_types[$field['type']]['no_ui'])) {
$info[$instance['field_name']] = array(
'type' => $field['type'],
'type_label' => $field_types[$field['type']]['label'],
'field' => $field['field_name'],
'label' => $instance['label'],
'widget_type' => $instance['widget']['type'],
);
}
}
}
}
}
return $info;
}
/**
* Form constructor for the field settings edit page.
*
* @see field_ui_field_settings_form_submit()
* @ingroup forms
*/
function field_ui_field_settings_form($form, &$form_state, $instance) {
$bundle = $instance['bundle'];
$entity_type = $instance['entity_type'];
$field = field_info_field($instance['field_name']);
drupal_set_title($instance['label']);
$description = '' . t('These settings apply to the %field field everywhere it is used. These settings impact the way that data is stored in the database and cannot be changed once data has been created.', array('%field' => $instance['label'])) . '
';
// Create a form structure for the field values.
$form['field'] = array(
'#type' => 'fieldset',
'#title' => t('Field settings'),
'#description' => $description,
'#tree' => TRUE,
);
// See if data already exists for this field.
// If so, prevent changes to the field settings.
$has_data = field_has_data($field);
if ($has_data) {
$form['field']['#description'] = '' . t('There is data for this field in the database. The field settings can no longer be changed.') . '
' . $form['field']['#description'];
}
// Build the non-configurable field values.
$form['field']['field_name'] = array('#type' => 'value', '#value' => $field['field_name']);
$form['field']['type'] = array('#type' => 'value', '#value' => $field['type']);
$form['field']['module'] = array('#type' => 'value', '#value' => $field['module']);
$form['field']['active'] = array('#type' => 'value', '#value' => $field['active']);
// Add settings provided by the field module. The field module is
// responsible for not returning settings that cannot be changed if
// the field already has data.
$form['field']['settings'] = array();
$additions = module_invoke($field['module'], 'field_settings_form', $field, $instance, $has_data);
if (is_array($additions)) {
$form['field']['settings'] = $additions;
}
if (empty($form['field']['settings'])) {
$form['field']['settings'] = array(
'#markup' => t('%field has no field settings.', array('%field' => $instance['label'])),
);
}
$form['#entity_type'] = $entity_type;
$form['#bundle'] = $bundle;
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save field settings'));
return $form;
}
/**
* Form submission handler for field_ui_field_settings_form().
*/
function field_ui_field_settings_form_submit($form, &$form_state) {
$form_values = $form_state['values'];
$field_values = $form_values['field'];
// Merge incoming form values into the existing field.
$field = field_info_field($field_values['field_name']);
$entity_type = $form['#entity_type'];
$bundle = $form['#bundle'];
$instance = field_info_instance($entity_type, $field['field_name'], $bundle);
// Update the field.
$field = array_merge($field, $field_values);
try {
field_update_field($field);
drupal_set_message(t('Updated field %label field settings.', array('%label' => $instance['label'])));
$form_state['redirect'] = field_ui_next_destination($entity_type, $bundle);
}
catch (Exception $e) {
drupal_set_message(t('Attempt to update field %label failed: %message.', array('%label' => $instance['label'], '%message' => $e->getMessage())), 'error');
}
}
/**
* Form constructor for the widget selection form.
*
* Path: BUNDLE_ADMIN_PATH/fields/%field/widget-type, where BUNDLE_ADMIN_PATH is
* the path stored in the ['admin']['info'] property in the return value of
* hook_entity_info().
*
* @see field_ui_menu()
* @see field_ui_widget_type_form_submit()
* @ingroup forms
*/
function field_ui_widget_type_form($form, &$form_state, $instance) {
drupal_set_title($instance['label']);
$bundle = $instance['bundle'];
$entity_type = $instance['entity_type'];
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
$field_type = field_info_field_types($field['type']);
$widget_type = field_info_widget_types($instance['widget']['type']);
$bundles = field_info_bundles();
$bundle_label = $bundles[$entity_type][$bundle]['label'];
$form = array(
'#bundle' => $bundle,
'#entity_type' => $entity_type,
'#field_name' => $field_name,
);
$form['basic'] = array(
'#type' => 'fieldset',
'#title' => t('Change widget'),
);
$form['basic']['widget_type'] = array(
'#type' => 'select',
'#title' => t('Widget type'),
'#required' => TRUE,
'#options' => field_ui_widget_type_options($field['type']),
'#default_value' => $instance['widget']['type'],
'#description' => t('The type of form element you would like to present to the user when creating this field in the %type type.', array('%type' => $bundle_label)),
);
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Continue'));
$form['#validate'] = array();
$form['#submit'] = array('field_ui_widget_type_form_submit');
return $form;
}
/**
* Form submission handler for field_ui_widget_type_form().
*/
function field_ui_widget_type_form_submit($form, &$form_state) {
$form_values = $form_state['values'];
$bundle = $form['#bundle'];
$entity_type = $form['#entity_type'];
$field_name = $form['#field_name'];
// Retrieve the stored instance settings to merge with the incoming values.
$instance = field_read_instance($entity_type, $field_name, $bundle);
// Set the right module information.
$widget_type = field_info_widget_types($form_values['widget_type']);
$widget_module = $widget_type['module'];
$instance['widget']['type'] = $form_values['widget_type'];
$instance['widget']['module'] = $widget_module;
try {
field_update_instance($instance);
drupal_set_message(t('Changed the widget for field %label.', array('%label' => $instance['label'])));
}
catch (Exception $e) {
drupal_set_message(t('There was a problem changing the widget for field %label.', array('%label' => $instance['label'])), 'error');
}
$form_state['redirect'] = field_ui_next_destination($entity_type, $bundle);
}
/**
* Form constructor for removing a field instance from a bundle.
*
* @see field_ui_field_delete_form_submit()
* @ingroup forms
*/
function field_ui_field_delete_form($form, &$form_state, $instance) {
$bundle = $instance['bundle'];
$entity_type = $instance['entity_type'];
$field = field_info_field($instance['field_name']);
$admin_path = _field_ui_bundle_admin_path($entity_type, $bundle);
$form['entity_type'] = array('#type' => 'value', '#value' => $entity_type);
$form['bundle'] = array('#type' => 'value', '#value' => $bundle);
$form['field_name'] = array('#type' => 'value', '#value' => $field['field_name']);
$output = confirm_form($form,
t('Are you sure you want to delete the field %field?', array('%field' => $instance['label'])),
$admin_path . '/fields',
t('If you have any content left in this field, it will be lost. This action cannot be undone.'),
t('Delete'), t('Cancel'),
'confirm'
);
if ($field['locked']) {
unset($output['actions']['submit']);
$output['description']['#markup'] = t('This field is locked and cannot be deleted.');
}
return $output;
}
/**
* Form submission handler for field_ui_field_delete_form().
*
* Removes a field instance from a bundle. If the field has no more instances,
* it will be marked as deleted too.
*/
function field_ui_field_delete_form_submit($form, &$form_state) {
$form_values = $form_state['values'];
$field_name = $form_values['field_name'];
$bundle = $form_values['bundle'];
$entity_type = $form_values['entity_type'];
$field = field_info_field($field_name);
$instance = field_info_instance($entity_type, $field_name, $bundle);
$bundles = field_info_bundles();
$bundle_label = $bundles[$entity_type][$bundle]['label'];
if (!empty($bundle) && $field && !$field['locked'] && $form_values['confirm']) {
field_delete_instance($instance);
drupal_set_message(t('The field %field has been deleted from the %type content type.', array('%field' => $instance['label'], '%type' => $bundle_label)));
}
else {
drupal_set_message(t('There was a problem removing the %field from the %type content type.', array('%field' => $instance['label'], '%type' => $bundle_label)), 'error');
}
$admin_path = _field_ui_bundle_admin_path($entity_type, $bundle);
$form_state['redirect'] = field_ui_get_destinations(array($admin_path . '/fields'));
// Fields are purged on cron. However field module prevents disabling modules
// when field types they provided are used in a field until it is fully
// purged. In the case that a field has minimal or no content, a single call
// to field_purge_batch() will remove it from the system. Call this with a
// low batch limit to avoid administrators having to wait for cron runs when
// removing instances that meet this criteria.
field_purge_batch(10);
}
/**
* Form constructor for the field instance settings form.
*
* @see field_ui_field_edit_form_validate()
* @see field_ui_field_edit_form_submit()
* @ingroup forms
*/
function field_ui_field_edit_form($form, &$form_state, $instance) {
$bundle = $instance['bundle'];
$entity_type = $instance['entity_type'];
$field = field_info_field($instance['field_name']);
drupal_set_title($instance['label']);
$form['#field'] = $field;
$form['#instance'] = $instance;
if (!empty($field['locked'])) {
$form['locked'] = array(
'#markup' => t('The field %field is locked and cannot be edited.', array('%field' => $instance['label'])),
);
return $form;
}
$field_type = field_info_field_types($field['type']);
$widget_type = field_info_widget_types($instance['widget']['type']);
$bundles = field_info_bundles();
// Create a form structure for the instance values.
$form['instance'] = array(
'#tree' => TRUE,
'#type' => 'fieldset',
'#title' => t('%type settings', array('%type' => $bundles[$entity_type][$bundle]['label'])),
'#description' => t('These settings apply only to the %field field when used in the %type type.', array(
'%field' => $instance['label'],
'%type' => $bundles[$entity_type][$bundle]['label'],
)),
// Ensure field_ui_field_edit_instance_pre_render() gets called in addition
// to, not instead of, the #pre_render function(s) needed by all fieldsets.
'#pre_render' => array_merge(array('field_ui_field_edit_instance_pre_render'), element_info_property('fieldset', '#pre_render', array())),
);
// Build the non-configurable instance values.
$form['instance']['field_name'] = array(
'#type' => 'value',
'#value' => $instance['field_name'],
);
$form['instance']['entity_type'] = array(
'#type' => 'value',
'#value' => $entity_type,
);
$form['instance']['bundle'] = array(
'#type' => 'value',
'#value' => $bundle,
);
$form['instance']['widget']['weight'] = array(
'#type' => 'value',
'#value' => !empty($instance['widget']['weight']) ? $instance['widget']['weight'] : 0,
);
// Build the configurable instance values.
$form['instance']['label'] = array(
'#type' => 'textfield',
'#title' => t('Label'),
'#default_value' => !empty($instance['label']) ? $instance['label'] : $field['field_name'],
'#required' => TRUE,
'#weight' => -20,
);
$form['instance']['required'] = array(
'#type' => 'checkbox',
'#title' => t('Required field'),
'#default_value' => !empty($instance['required']),
'#weight' => -10,
);
$form['instance']['description'] = array(
'#type' => 'textarea',
'#title' => t('Help text'),
'#default_value' => !empty($instance['description']) ? $instance['description'] : '',
'#rows' => 5,
'#description' => t('Instructions to present to the user below this field on the editing form. Allowed HTML tags: @tags', array('@tags' => _field_filter_xss_display_allowed_tags())),
'#weight' => -5,
);
// Build the widget component of the instance.
$form['instance']['widget']['type'] = array(
'#type' => 'value',
'#value' => $instance['widget']['type'],
);
$form['instance']['widget']['module'] = array(
'#type' => 'value',
'#value' => $widget_type['module'],
);
$form['instance']['widget']['active'] = array(
'#type' => 'value',
'#value' => !empty($field['instance']['widget']['active']) ? 1 : 0,
);
// Add additional field instance settings from the field module.
$additions = module_invoke($field['module'], 'field_instance_settings_form', $field, $instance);
if (is_array($additions)) {
$form['instance']['settings'] = $additions;
}
// Add additional widget settings from the widget module.
$additions = module_invoke($widget_type['module'], 'field_widget_settings_form', $field, $instance);
if (is_array($additions)) {
$form['instance']['widget']['settings'] = $additions;
$form['instance']['widget']['active']['#value'] = 1;
}
// Add handling for default value if not provided by any other module.
if (field_behaviors_widget('default value', $instance) == FIELD_BEHAVIOR_DEFAULT && empty($instance['default_value_function'])) {
$form['instance']['default_value_widget'] = field_ui_default_value_widget($field, $instance, $form, $form_state);
}
$has_data = field_has_data($field);
if ($has_data) {
$description = '' . t('These settings apply to the %field field everywhere it is used. Because the field already has data, some settings can no longer be changed.', array('%field' => $instance['label'])) . '
';
}
else {
$description = '' . t('These settings apply to the %field field everywhere it is used.', array('%field' => $instance['label'])) . '
';
}
// Create a form structure for the field values.
$form['field'] = array(
'#type' => 'fieldset',
'#title' => t('%field field settings', array('%field' => $instance['label'])),
'#description' => $description,
'#tree' => TRUE,
);
// Build the configurable field values.
$description = t('Maximum number of values users can enter for this field.');
if (field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) {
$description .= ' ' . t("'Unlimited' will provide an 'Add more' button so the users can add as many values as they like.");
}
$form['field']['cardinality'] = array(
'#type' => 'select',
'#title' => t('Number of values'),
'#options' => array(FIELD_CARDINALITY_UNLIMITED => t('Unlimited')) + drupal_map_assoc(range(1, 10)),
'#default_value' => $field['cardinality'],
'#description' => $description,
);
// Add additional field type settings. The field type module is
// responsible for not returning settings that cannot be changed if
// the field already has data.
$additions = module_invoke($field['module'], 'field_settings_form', $field, $instance, $has_data);
if (is_array($additions)) {
$form['field']['settings'] = $additions;
}
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save settings'));
return $form;
}
/**
* Pre-render function for field instance settings.
*
* Combines the instance, widget, and other settings into a single fieldset so
* that elements within each group can be shown at different weights as if they
* all had the same parent.
*/
function field_ui_field_edit_instance_pre_render($element) {
// Merge the widget settings into the main form.
if (isset($element['widget']['settings'])) {
foreach (element_children($element['widget']['settings']) as $key) {
$element['widget_' . $key] = $element['widget']['settings'][$key];
}
unset($element['widget']['settings']);
}
// Merge the instance settings into the main form.
if (isset($element['settings'])) {
foreach (element_children($element['settings']) as $key) {
$element['instance_' . $key] = $element['settings'][$key];
}
unset($element['settings']);
}
return $element;
}
/**
* Builds the default value fieldset for a given field instance.
*/
function field_ui_default_value_widget($field, $instance, &$form, &$form_state) {
$field_name = $field['field_name'];
$element = array(
'#type' => 'fieldset',
'#title' => t('Default value'),
'#collapsible' => FALSE,
'#tree' => TRUE,
'#description' => t('The default value for this field, used when creating new content.'),
// Stick to an empty 'parents' on this form in order not to breaks widgets
// that do not use field_widget_[field|instance]() and still access
// $form_state['field'] directly.
'#parents' => array(),
);
// Insert the widget.
$items = $instance['default_value'];
$instance['required'] = FALSE;
$instance['description'] = '';
// @todo Allow multiple values (requires more work on 'add more' JS handler).
$element += field_default_form($instance['entity_type'], NULL, $field, $instance, LANGUAGE_NONE, $items, $element, $form_state, 0);
return $element;
}
/**
* Form validation handler for field_ui_field_edit_form().
*
* @see field_ui_field_edit_form_submit().
*/
function field_ui_field_edit_form_validate($form, &$form_state) {
// Take the incoming values as the $instance definition, so that the 'default
// value' gets validated using the instance settings being submitted.
$instance = $form_state['values']['instance'];
$field_name = $instance['field_name'];
if (isset($form['instance']['default_value_widget'])) {
$element = $form['instance']['default_value_widget'];
$field_state = field_form_get_state($element['#parents'], $field_name, LANGUAGE_NONE, $form_state);
$field = $field_state['field'];
// Extract the 'default value'.
$items = array();
field_default_extract_form_values(NULL, NULL, $field, $instance, LANGUAGE_NONE, $items, $element, $form_state);
// Validate the value and report errors.
$errors = array();
$function = $field['module'] . '_field_validate';
if (function_exists($function)) {
$function(NULL, NULL, $field, $instance, LANGUAGE_NONE, $items, $errors);
}
if (isset($errors[$field_name][LANGUAGE_NONE])) {
// Store reported errors in $form_state.
$field_state['errors'] = $errors[$field_name][LANGUAGE_NONE];
field_form_set_state($element['#parents'], $field_name, LANGUAGE_NONE, $form_state, $field_state);
// Assign reported errors to the correct form element.
field_default_form_errors(NULL, NULL, $field, $instance, LANGUAGE_NONE, $items, $element, $form_state);
}
}
}
/**
* Form submission handler for field_ui_field_edit_form().
*
* @see field_ui_field_edit_form_validate().
*/
function field_ui_field_edit_form_submit($form, &$form_state) {
$instance = $form_state['values']['instance'];
$field = $form_state['values']['field'];
// Update any field settings that have changed.
$field_source = field_info_field($instance['field_name']);
$field = array_merge($field_source, $field);
try {
field_update_field($field);
}
catch (Exception $e) {
drupal_set_message(t('Attempt to update field %label failed: %message.', array('%label' => $instance['label'], '%message' => $e->getMessage())), 'error');
return;
}
// Handle the default value.
if (isset($form['instance']['default_value_widget'])) {
$element = $form['instance']['default_value_widget'];
// Extract field values.
$items = array();
field_default_extract_form_values(NULL, NULL, $field, $instance, LANGUAGE_NONE, $items, $element, $form_state);
field_default_submit(NULL, NULL, $field, $instance, LANGUAGE_NONE, $items, $element, $form_state);
$instance['default_value'] = $items ? $items : NULL;
}
// Retrieve the stored instance settings to merge with the incoming values.
$instance_source = field_read_instance($instance['entity_type'], $instance['field_name'], $instance['bundle']);
$instance = array_merge($instance_source, $instance);
field_update_instance($instance);
drupal_set_message(t('Saved %label configuration.', array('%label' => $instance['label'])));
$form_state['redirect'] = field_ui_next_destination($instance['entity_type'], $instance['bundle']);
}
/**
* Extracts next redirect path from an array of multiple destinations.
*
* @see field_ui_next_destination()
*/
function field_ui_get_destinations($destinations) {
$path = array_shift($destinations);
$options = drupal_parse_url($path);
if ($destinations) {
$options['query']['destinations'] = $destinations;
}
return array($options['path'], $options);
}
/**
* Returns the next redirect path in a multipage sequence.
*/
function field_ui_next_destination($entity_type, $bundle) {
$destinations = !empty($_REQUEST['destinations']) ? $_REQUEST['destinations'] : array();
if (!empty($destinations)) {
unset($_REQUEST['destinations']);
}
// Remove any external URLs.
$destinations = array_diff($destinations, array_filter($destinations, 'url_is_external'));
if ($destinations) {
return field_ui_get_destinations($destinations);
}
$admin_path = _field_ui_bundle_admin_path($entity_type, $bundle);
return $admin_path . '/fields';
}
'textfield',
'#title' => t('Maximum length'),
'#default_value' => $settings['max_length'],
'#required' => FALSE,
'#element_validate' => array('element_validate_integer_positive'),
'#description' => t('The maximum length of the field in characters. Leave blank for an unlimited size.'),
);
return $form;
}
/**
* Add settings to an instance field settings form.
*
* Invoked from field_ui_field_edit_form() to allow the module defining the
* field to add settings for a field instance.
*
* @param $field
* The field structure being configured.
* @param $instance
* The instance structure being configured.
*
* @return
* The form definition for the field instance settings.
*/
function hook_field_instance_settings_form($field, $instance) {
$settings = $instance['settings'];
$form['text_processing'] = array(
'#type' => 'radios',
'#title' => t('Text processing'),
'#default_value' => $settings['text_processing'],
'#options' => array(
t('Plain text'),
t('Filtered text (user selects text format)'),
),
);
if ($field['type'] == 'text_with_summary') {
$form['display_summary'] = array(
'#type' => 'select',
'#title' => t('Display summary'),
'#options' => array(
t('No'),
t('Yes'),
),
'#description' => t('Display the summary to allow the user to input a summary value. Hide the summary to automatically fill it with a trimmed portion from the main post.'),
'#default_value' => !empty($settings['display_summary']) ? $settings['display_summary'] : 0,
);
}
return $form;
}
/**
* Add settings to a widget settings form.
*
* Invoked from field_ui_field_edit_form() to allow the module defining the
* widget to add settings for a widget instance.
*
* @param $field
* The field structure being configured.
* @param $instance
* The instance structure being configured.
*
* @return
* The form definition for the widget settings.
*/
function hook_field_widget_settings_form($field, $instance) {
$widget = $instance['widget'];
$settings = $widget['settings'];
if ($widget['type'] == 'text_textfield') {
$form['size'] = array(
'#type' => 'textfield',
'#title' => t('Size of textfield'),
'#default_value' => $settings['size'],
'#element_validate' => array('element_validate_integer_positive'),
'#required' => TRUE,
);
}
else {
$form['rows'] = array(
'#type' => 'textfield',
'#title' => t('Rows'),
'#default_value' => $settings['rows'],
'#element_validate' => array('element_validate_integer_positive'),
'#required' => TRUE,
);
}
return $form;
}
/**
* Specify the form elements for a formatter's settings.
*
* This hook is only invoked if hook_field_formatter_settings_summary()
* returns a non-empty value.
*
* @param $field
* The field structure being configured.
* @param $instance
* The instance structure being configured.
* @param $view_mode
* The view mode being configured.
* @param $form
* The (entire) configuration form array, which will usually have no use here.
* @param $form_state
* The form state of the (entire) configuration form.
*
* @return
* The form elements for the formatter settings.
*/
function hook_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$element = array();
if ($display['type'] == 'text_trimmed' || $display['type'] == 'text_summary_or_trimmed') {
$element['trim_length'] = array(
'#title' => t('Length'),
'#type' => 'textfield',
'#size' => 20,
'#default_value' => $settings['trim_length'],
'#element_validate' => array('element_validate_integer_positive'),
'#required' => TRUE,
);
}
return $element;
}
/**
* Return a short summary for the current formatter settings of an instance.
*
* If an empty result is returned, the formatter is assumed to have no
* configurable settings, and no UI will be provided to display a settings
* form.
*
* @param $field
* The field structure.
* @param $instance
* The instance structure.
* @param $view_mode
* The view mode for which a settings summary is requested.
*
* @return
* A string containing a short summary of the formatter settings.
*/
function hook_field_formatter_settings_summary($field, $instance, $view_mode) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$summary = '';
if ($display['type'] == 'text_trimmed' || $display['type'] == 'text_summary_or_trimmed') {
$summary = t('Length: @chars chars', array('@chars' => $settings['trim_length']));
}
return $summary;
}
/**
* @} End of "addtogroup field_types".
*/
/**
* @file
* Stylesheet for the Field UI module.
*/
/* 'Manage fields' and 'Manage display' overviews */
table.field-ui-overview tr.add-new .label-input {
float: left; /* LTR */
}
table.field-ui-overview tr.add-new .tabledrag-changed {
display: none;
}
table.field-ui-overview tr.add-new .description {
margin-bottom: 0;
max-width: 250px;
}
table.field-ui-overview tr.add-new .form-type-machine-name .description {
white-space: normal;
}
table.field-ui-overview tr.add-new .add-new-placeholder {
font-weight: bold;
padding-bottom: .5em;
}
table.field-ui-overview tr.region-title td {
font-weight: bold;
}
table.field-ui-overview tr.region-message td {
font-style: italic;
}
table.field-ui-overview tr.region-populated {
display: none;
}
table.field-ui-overview tr.region-add-new-title {
display: none;
}
table.field-ui-overview tr.add-new td {
vertical-align: top;
white-space: nowrap;
}
/* 'Manage display' overview */
#field-display-overview .field-formatter-summary-cell {
line-height: 1em;
}
#field-display-overview .field-formatter-summary {
float: left;
font-size: 0.9em;
}
#field-display-overview td.field-formatter-summary-cell span.warning {
display: block;
float: left;
margin-right: .5em;
}
#field-display-overview .field-formatter-settings-edit-wrapper {
float: right;
}
#field-display-overview .field-formatter-settings-edit {
float: right;
}
#field-display-overview tr.field-formatter-settings-editing td {
vertical-align: top;
}
#field-display-overview tr.field-formatter-settings-editing .field-formatter-type {
display: none;
}
#field-display-overview .field-formatter-settings-edit-form .formatter-name{
font-weight: bold;
}
#field-ui-display-overview-form #edit-refresh {
display:none;
}
name = Field UI
description = User interface for the Field API.
package = Core
version = VERSION
core = 7.x
dependencies[] = field
files[] = field_ui.test
; Information added by Drupal.org packaging script on 2023-03-15
version = "7.95"
project = "drupal"
datestamp = "1678902530"
/**
* @file
* Attaches the behaviors for the Field UI module.
*/
(function($) {
Drupal.behaviors.fieldUIFieldOverview = {
attach: function (context, settings) {
$('table#field-overview', context).once('field-overview', function () {
Drupal.fieldUIFieldOverview.attachUpdateSelects(this, settings);
});
}
};
Drupal.fieldUIFieldOverview = {
/**
* Implements dependent select dropdowns on the 'Manage fields' screen.
*/
attachUpdateSelects: function(table, settings) {
var widgetTypes = settings.fieldWidgetTypes;
var fields = settings.fields;
// Store the default text of widget selects.
$('.widget-type-select', table).each(function () {
this.initialValue = this.options[0].text;
});
// 'Field type' select updates its 'Widget' select.
$('.field-type-select', table).each(function () {
this.targetSelect = $('.widget-type-select', $(this).closest('tr'));
$(this).bind('change keyup', function () {
var selectedFieldType = this.options[this.selectedIndex].value;
var options = (selectedFieldType in widgetTypes ? widgetTypes[selectedFieldType] : []);
this.targetSelect.fieldUIPopulateOptions(options);
});
// Trigger change on initial pageload to get the right widget options
// when field type comes pre-selected (on failed validation).
$(this).trigger('change', false);
});
// 'Existing field' select updates its 'Widget' select and 'Label' textfield.
$('.field-select', table).each(function () {
this.targetSelect = $('.widget-type-select', $(this).closest('tr'));
this.targetTextfield = $('.label-textfield', $(this).closest('tr'));
this.targetTextfield
.data('field_ui_edited', false)
.bind('keyup', function (e) {
$(this).data('field_ui_edited', $(this).val() != '');
});
$(this).bind('change keyup', function (e, updateText) {
var updateText = (typeof updateText == 'undefined' ? true : updateText);
var selectedField = this.options[this.selectedIndex].value;
var selectedFieldType = (selectedField in fields ? fields[selectedField].type : null);
var selectedFieldWidget = (selectedField in fields ? fields[selectedField].widget : null);
var options = (selectedFieldType && (selectedFieldType in widgetTypes) ? widgetTypes[selectedFieldType] : []);
this.targetSelect.fieldUIPopulateOptions(options, selectedFieldWidget);
// Only overwrite the "Label" input if it has not been manually
// changed, or if it is empty.
if (updateText && !this.targetTextfield.data('field_ui_edited')) {
this.targetTextfield.val(selectedField in fields ? fields[selectedField].label : '');
}
});
// Trigger change on initial pageload to get the right widget options
// and label when field type comes pre-selected (on failed validation).
$(this).trigger('change', false);
});
}
};
/**
* Populates options in a select input.
*/
jQuery.fn.fieldUIPopulateOptions = function (options, selected) {
return this.each(function () {
var disabled = false;
if (options.length == 0) {
options = [this.initialValue];
disabled = true;
}
// If possible, keep the same widget selected when changing field type.
// This is based on textual value, since the internal value might be
// different (options_buttons vs. node_reference_buttons).
var previousSelectedText = this.options[this.selectedIndex].text;
var html = '';
jQuery.each(options, function (value, text) {
// Figure out which value should be selected. The 'selected' param
// takes precedence.
var is_selected = ((typeof selected != 'undefined' && value == selected) || (typeof selected == 'undefined' && text == previousSelectedText));
html += '' + text + ' ';
});
$(this).html(html).attr('disabled', disabled ? 'disabled' : false);
});
};
Drupal.behaviors.fieldUIDisplayOverview = {
attach: function (context, settings) {
$('table#field-display-overview', context).once('field-display-overview', function() {
Drupal.fieldUIOverview.attach(this, settings.fieldUIRowsData, Drupal.fieldUIDisplayOverview);
});
}
};
Drupal.fieldUIOverview = {
/**
* Attaches the fieldUIOverview behavior.
*/
attach: function (table, rowsData, rowHandlers) {
var tableDrag = Drupal.tableDrag[table.id];
// Add custom tabledrag callbacks.
tableDrag.onDrop = this.onDrop;
tableDrag.row.prototype.onSwap = this.onSwap;
// Create row handlers.
$('tr.draggable', table).each(function () {
// Extract server-side data for the row.
var row = this;
if (row.id in rowsData) {
var data = rowsData[row.id];
data.tableDrag = tableDrag;
// Create the row handler, make it accessible from the DOM row element.
var rowHandler = new rowHandlers[data.rowHandler](row, data);
$(row).data('fieldUIRowHandler', rowHandler);
}
});
},
/**
* Event handler to be attached to form inputs triggering a region change.
*/
onChange: function () {
var $trigger = $(this);
var row = $trigger.closest('tr').get(0);
var rowHandler = $(row).data('fieldUIRowHandler');
var refreshRows = {};
refreshRows[rowHandler.name] = $trigger.get(0);
// Handle region change.
var region = rowHandler.getRegion();
if (region != rowHandler.region) {
// Remove parenting.
$('select.field-parent', row).val('');
// Let the row handler deal with the region change.
$.extend(refreshRows, rowHandler.regionChange(region));
// Update the row region.
rowHandler.region = region;
}
// Ajax-update the rows.
Drupal.fieldUIOverview.AJAXRefreshRows(refreshRows);
},
/**
* Lets row handlers react when a row is dropped into a new region.
*/
onDrop: function () {
var dragObject = this;
var row = dragObject.rowObject.element;
var rowHandler = $(row).data('fieldUIRowHandler');
if (typeof rowHandler !== 'undefined') {
var regionRow = $(row).prevAll('tr.region-message').get(0);
var region = regionRow.className.replace(/([^ ]+[ ]+)*region-([^ ]+)-message([ ]+[^ ]+)*/, '$2');
if (region != rowHandler.region) {
// Let the row handler deal with the region change.
refreshRows = rowHandler.regionChange(region);
// Update the row region.
rowHandler.region = region;
// Ajax-update the rows.
Drupal.fieldUIOverview.AJAXRefreshRows(refreshRows);
}
}
},
/**
* Refreshes placeholder rows in empty regions while a row is being dragged.
*
* Copied from block.js.
*
* @param table
* The table DOM element.
* @param rowObject
* The tableDrag rowObject for the row being dragged.
*/
onSwap: function (draggedRow) {
var rowObject = this;
$('tr.region-message', rowObject.table).each(function () {
// If the dragged row is in this region, but above the message row, swap
// it down one space.
if ($(this).prev('tr').get(0) == rowObject.group[rowObject.group.length - 1]) {
// Prevent a recursion problem when using the keyboard to move rows up.
if ((rowObject.method != 'keyboard' || rowObject.direction == 'down')) {
rowObject.swap('after', this);
}
}
// This region has become empty.
if ($(this).next('tr').is(':not(.draggable)') || $(this).next('tr').length == 0) {
$(this).removeClass('region-populated').addClass('region-empty');
}
// This region has become populated.
else if ($(this).is('.region-empty')) {
$(this).removeClass('region-empty').addClass('region-populated');
}
});
},
/**
* Triggers Ajax refresh of selected rows.
*
* The 'format type' selects can trigger a series of changes in child rows.
* The #ajax behavior is therefore not attached directly to the selects, but
* triggered manually through a hidden #ajax 'Refresh' button.
*
* @param rows
* A hash object, whose keys are the names of the rows to refresh (they
* will receive the 'ajax-new-content' effect on the server side), and
* whose values are the DOM element in the row that should get an Ajax
* throbber.
*/
AJAXRefreshRows: function (rows) {
// Separate keys and values.
var rowNames = [];
var ajaxElements = [];
$.each(rows, function (rowName, ajaxElement) {
rowNames.push(rowName);
ajaxElements.push(ajaxElement);
});
if (rowNames.length) {
// Add a throbber next each of the ajaxElements.
var $throbber = $('');
$(ajaxElements)
.addClass('progress-disabled')
.after($throbber);
// Fire the Ajax update.
$('input[name=refresh_rows]').val(rowNames.join(' '));
$('input#edit-refresh').mousedown();
// Disabled elements do not appear in POST ajax data, so we mark the
// elements disabled only after firing the request.
$(ajaxElements).attr('disabled', true);
}
}
};
/**
* Row handlers for the 'Manage display' screen.
*/
Drupal.fieldUIDisplayOverview = {};
/**
* Constructor for a 'field' row handler.
*
* This handler is used for both fields and 'extra fields' rows.
*
* @param row
* The row DOM element.
* @param data
* Additional data to be populated in the constructed object.
*/
Drupal.fieldUIDisplayOverview.field = function (row, data) {
this.row = row;
this.name = data.name;
this.region = data.region;
this.tableDrag = data.tableDrag;
// Attach change listener to the 'formatter type' select.
this.$formatSelect = $('select.field-formatter-type', row);
this.$formatSelect.change(Drupal.fieldUIOverview.onChange);
return this;
};
Drupal.fieldUIDisplayOverview.field.prototype = {
/**
* Returns the region corresponding to the current form values of the row.
*/
getRegion: function () {
return (this.$formatSelect.val() == 'hidden') ? 'hidden' : 'visible';
},
/**
* Reacts to a row being changed regions.
*
* This function is called when the row is moved to a different region, as a
* result of either :
* - a drag-and-drop action (the row's form elements then probably need to be
* updated accordingly)
* - user input in one of the form elements watched by the
* Drupal.fieldUIOverview.onChange change listener.
*
* @param region
* The name of the new region for the row.
* @return
* A hash object indicating which rows should be Ajax-updated as a result
* of the change, in the format expected by
* Drupal.displayOverview.AJAXRefreshRows().
*/
regionChange: function (region) {
// When triggered by a row drag, the 'format' select needs to be adjusted
// to the new region.
var currentValue = this.$formatSelect.val();
switch (region) {
case 'visible':
if (currentValue == 'hidden') {
// Restore the formatter back to the default formatter. Pseudo-fields do
// not have default formatters, we just return to 'visible' for those.
var value = (typeof this.defaultFormatter !== 'undefined') ? this.defaultFormatter : this.$formatSelect.find('option').val();
}
break;
default:
var value = 'hidden';
break;
}
if (value != undefined) {
this.$formatSelect.val(value);
}
var refreshRows = {};
refreshRows[this.name] = this.$formatSelect.get(0);
return refreshRows;
}
};
})(jQuery);
' . t('About') . '';
$output .= '' . t('The Field UI module provides an administrative user interface (UI) for attaching and managing fields. Fields can be defined at the content-type level for content items and comments, at the vocabulary level for taxonomy terms, and at the site level for user accounts. Other modules may also enable fields to be defined for their data. Field types (text, image, number, etc.) are defined by modules, and collected and managed by the Field module . For more information, see the online handbook entry for Field UI module .', array('@field' => url('admin/help/field'), '@field_ui' => 'http://drupal.org/documentation/modules/field-ui')) . '
';
$output .= '' . t('Uses') . ' ';
$output .= '';
$output .= '' . t('Planning fields') . ' ';
$output .= '' . t('There are several decisions you will need to make before defining a field for content, comments, etc.:') . '';
$output .= '' . t('What the field will be called') . ' ';
$output .= '' . t('A field has a label (the name displayed in the user interface) and a machine name (the name used internally). The label can be changed after you create the field, if needed, but the machine name cannot be changed after you have created the field.') . '';
$output .= ' ' . t('What type of data the field will store') . ' ';
$output .= '' . t('Each field can store one type of data (text, number, file, etc.). When you define a field, you choose a particular field type , which corresponds to the type of data you want to store. The field type cannot be changed after you have created the field.') . ' ';
$output .= '' . t('How the data will be input and displayed') . ' ';
$output .= '' . t('Each field type has one or more available widgets associated with it; each widget provides a mechanism for data input when you are editing (text box, select list, file upload, etc.). Each field type also has one or more display options, which determine how the field is displayed to site visitors. The widget and display options can be changed after you have created the field.') . ' ';
$output .= '' . t('How many values the field will store') . ' ';
$output .= '' . t('You can store one value, a specific maximum number of values, or an unlimited number of values in each field. For example, an employee identification number field might store a single number, whereas a phone number field might store multiple phone numbers. This setting can be changed after you have created the field, but if you reduce the maximum number of values, you may lose information.') . ' ';
$output .= ' ';
$output .= ' ' . t('Reusing fields') . ' ';
$output .= '' . t('Once you have defined a field, you can reuse it. For example, if you define a custom image field for one content type, and you need to have an image field with the same parameters on another content type, you can add the same field to the second content type, in the Add existing field area of the user interface. You could also add this field to a taxonomy vocabulary, comments, user accounts, etc.') . ' ';
$output .= '' . t('Some settings of a reused field are unique to each use of the field; others are shared across all places you use the field. For example, the label of a text field is unique to each use, while the setting for the number of values is shared.') . ' ';
$output .= '' . t('There are two main reasons for reusing fields. First, reusing fields can save you time over defining new fields. Second, reusing fields also allows you to display, filter, group, and sort content together by field across content types. For example, the contributed Views module allows you to create lists and tables of content. So if you use the same field on multiple content types, you can create a View containing all of those content types together displaying that field, sorted by that field, and/or filtered by that field.') . ' ';
$output .= '' . t('Fields on content items') . ' ';
$output .= '' . t('Fields on content items are defined at the content-type level, on the Manage fields tab of the content type edit page (which you can reach from the Content types page ). When you define a field for a content type, each content item of that type will have that field added to it. Some fields, such as the Title and Body, are provided for you when you create a content type, or are provided on content types created by your installation profile.', array('@types' => url('admin/structure/types'))) . ' ';
$output .= '' . t('Fields on taxonomy terms') . ' ';
$output .= '' . t('Fields on taxonomy terms are defined at the taxonomy vocabulary level, on the Manage fields tab of the vocabulary edit page (which you can reach from the Taxonomy page ). When you define a field for a vocabulary, each term in that vocabulary will have that field added to it. For example, you could define an image field for a vocabulary to store an icon with each term.', array('@taxonomy' => url('admin/structure/taxonomy'))) . ' ';
$output .= '' . t('Fields on user accounts') . ' ';
$output .= '' . t('Fields on user accounts are defined on a site-wide basis on the Manage fields tab of the Account settings page. When you define a field for user accounts, each user account will have that field added to it. For example, you could add a long text field to allow users to include a biography.', array('@fields' => url('admin/config/people/accounts/fields'), '@accounts' => url('admin/config/people/accounts'))) . ' ';
$output .= '' . t('Fields on comments') . ' ';
$output .= '' . t('Fields on comments are defined at the content-type level, on the Comment fields tab of the content type edit page (which you can reach from the Content types page ). When you add a field for comments, each comment on a content item of that type will have that field added to it. For example, you could add a website field to the comments on forum posts, to allow forum commenters to add a link to their website.', array('@types' => url('admin/structure/types'))) . ' ';
$output .= ' ';
return $output;
case 'admin/reports/fields':
return '' . t('This list shows all fields currently in use for easy reference.') . '
';
}
}
/**
* Implements hook_field_attach_rename_bundle().
*/
function field_ui_field_attach_rename_bundle($entity_type, $bundle_old, $bundle_new) {
// The Field UI relies on entity_get_info() to build menu items for entity
// field administration pages. Clear the entity info cache and ensure that
// the menu is rebuilt.
entity_info_cache_clear();
menu_rebuild();
}
/**
* Implements hook_menu().
*/
function field_ui_menu() {
$items['admin/reports/fields'] = array(
'title' => 'Field list',
'description' => 'Overview of fields on all entity types.',
'page callback' => 'field_ui_fields_list',
'access arguments' => array('administer content types'),
'type' => MENU_NORMAL_ITEM,
'file' => 'field_ui.admin.inc',
);
// Ensure the following is not executed until field_bundles is working and
// tables are updated. Needed to avoid errors on initial installation.
if (defined('MAINTENANCE_MODE')) {
return $items;
}
// Create tabs for all possible bundles.
foreach (entity_get_info() as $entity_type => $entity_info) {
if ($entity_info['fieldable']) {
foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
if (isset($bundle_info['admin'])) {
// Extract path information from the bundle.
$path = $bundle_info['admin']['path'];
// Different bundles can appear on the same path (e.g. %node_type and
// %comment_node_type). To allow field_ui_menu_load() to extract the
// actual bundle object from the translated menu router path
// arguments, we need to identify the argument position of the bundle
// name string ('bundle argument') and pass that position to the menu
// loader. The position needs to be casted into a string; otherwise it
// would be replaced with the bundle name string.
if (isset($bundle_info['admin']['bundle argument'])) {
$bundle_arg = $bundle_info['admin']['bundle argument'];
$bundle_pos = (string) $bundle_arg;
}
else {
$bundle_arg = $bundle_name;
$bundle_pos = '0';
}
// This is the position of the %field_ui_menu placeholder in the
// items below.
$field_position = count(explode('/', $path)) + 1;
// Extract access information, providing defaults.
$access = array_intersect_key($bundle_info['admin'], drupal_map_assoc(array('access callback', 'access arguments')));
$access += array(
'access callback' => 'user_access',
'access arguments' => array('administer fields'),
);
// Add the "administer fields" permission on top of the access
// restriction because the field UI should only be accessible to
// trusted users.
if ($access['access callback'] != 'user_access' || $access['access arguments'] != array('administer fields')) {
$access = array(
'access callback' => 'field_ui_admin_access',
'access arguments' => array($access['access callback'], $access['access arguments']),
);
}
$items["$path/fields"] = array(
'title' => 'Manage fields',
'page callback' => 'drupal_get_form',
'page arguments' => array('field_ui_field_overview_form', $entity_type, $bundle_arg),
'type' => MENU_LOCAL_TASK,
'weight' => 1,
'file' => 'field_ui.admin.inc',
) + $access;
$items["$path/fields/%field_ui_menu"] = array(
'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
'title callback' => 'field_ui_menu_title',
'title arguments' => array($field_position),
'page callback' => 'drupal_get_form',
'page arguments' => array('field_ui_field_edit_form', $field_position),
'file' => 'field_ui.admin.inc',
) + $access;
$items["$path/fields/%field_ui_menu/edit"] = array(
'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
'title' => 'Edit',
'page callback' => 'drupal_get_form',
'page arguments' => array('field_ui_field_edit_form', $field_position),
'type' => MENU_DEFAULT_LOCAL_TASK,
'file' => 'field_ui.admin.inc',
) + $access;
$items["$path/fields/%field_ui_menu/field-settings"] = array(
'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
'title' => 'Field settings',
'page callback' => 'drupal_get_form',
'page arguments' => array('field_ui_field_settings_form', $field_position),
'type' => MENU_LOCAL_TASK,
'file' => 'field_ui.admin.inc',
) + $access;
$items["$path/fields/%field_ui_menu/widget-type"] = array(
'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
'title' => 'Widget type',
'page callback' => 'drupal_get_form',
'page arguments' => array('field_ui_widget_type_form', $field_position),
'type' => MENU_LOCAL_TASK,
'file' => 'field_ui.admin.inc',
) + $access;
$items["$path/fields/%field_ui_menu/delete"] = array(
'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
'title' => 'Delete',
'page callback' => 'drupal_get_form',
'page arguments' => array('field_ui_field_delete_form', $field_position),
'type' => MENU_LOCAL_TASK,
'weight' => 10,
'file' => 'field_ui.admin.inc',
) + $access;
// 'Manage display' tab.
$items["$path/display"] = array(
'title' => 'Manage display',
'page callback' => 'drupal_get_form',
'page arguments' => array('field_ui_display_overview_form', $entity_type, $bundle_arg, 'default'),
'type' => MENU_LOCAL_TASK,
'weight' => 2,
'file' => 'field_ui.admin.inc',
) + $access;
// View modes secondary tabs.
// The same base $path for the menu item (with a placeholder) can be
// used for all bundles of a given entity type; but depending on
// administrator settings, each bundle has a different set of view
// modes available for customisation. So we define menu items for all
// view modes, and use an access callback to determine which ones are
// actually visible for a given bundle.
$weight = 0;
$view_modes = array('default' => array('label' => t('Default'))) + $entity_info['view modes'];
foreach ($view_modes as $view_mode => $view_mode_info) {
$items["$path/display/$view_mode"] = array(
'title' => $view_mode_info['label'],
'page arguments' => array('field_ui_display_overview_form', $entity_type, $bundle_arg, $view_mode),
// The access callback needs to check both the current 'custom
// display' setting for the view mode, and the overall access
// rules for the bundle admin pages.
'access callback' => '_field_ui_view_mode_menu_access',
'access arguments' => array_merge(array($entity_type, $bundle_arg, $view_mode, $access['access callback']), $access['access arguments']),
'type' => ($view_mode == 'default' ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK),
'weight' => ($view_mode == 'default' ? -10 : $weight++),
'file' => 'field_ui.admin.inc',
);
}
}
}
}
}
return $items;
}
/**
* Menu loader; Load a field instance based on field and bundle name.
*
* @param $field_name
* The name of the field, as contained in the path.
* @param $entity_type
* The name of the entity.
* @param $bundle_name
* The name of the bundle, as contained in the path.
* @param $bundle_pos
* The position of $bundle_name in $map.
* @param $map
* The translated menu router path argument map.
*
* @return
* The field instance array.
*
* @ingroup field
*/
function field_ui_menu_load($field_name, $entity_type, $bundle_name, $bundle_pos, $map) {
// Extract the actual bundle name from the translated argument map.
// The menu router path to manage fields of an entity can be shared among
// multiple bundles. For example:
// - admin/structure/types/manage/%node_type/fields/%field_ui_menu
// - admin/structure/types/manage/%comment_node_type/fields/%field_ui_menu
// The menu system will automatically load the correct bundle depending on the
// actual path arguments, but this menu loader function only receives the node
// type string as $bundle_name, which is not the bundle name for comments.
// We therefore leverage the dynamically translated $map provided by the menu
// system to retrieve the actual bundle and bundle name for the current path.
if ($bundle_pos > 0) {
$bundle = $map[$bundle_pos];
$bundle_name = field_extract_bundle($entity_type, $bundle);
}
// Check whether the field exists at all.
if ($field = field_info_field($field_name)) {
// Only return the field if a field instance exists for the given entity
// type and bundle.
if ($instance = field_info_instance($entity_type, $field_name, $bundle_name)) {
return $instance;
}
}
return FALSE;
}
/**
* Menu title callback.
*/
function field_ui_menu_title($instance) {
return $instance['label'];
}
/**
* Menu access callback for the 'view mode display settings' pages.
*/
function _field_ui_view_mode_menu_access($entity_type, $bundle, $view_mode, $access_callback) {
// It's good practice to call func_get_args() at the beginning of a function
// to avoid problems with function parameters being modified later. The
// behavior of func_get_args() changed in PHP7.
// @see https://www.php.net/manual/en/migration70.incompatible.php#migration70.incompatible.other.func-parameter-modified
$all_args = func_get_args();
// First, determine visibility according to the 'use custom display'
// setting for the view mode.
$bundle = field_extract_bundle($entity_type, $bundle);
$view_mode_settings = field_view_mode_settings($entity_type, $bundle);
$visibility = ($view_mode == 'default') || !empty($view_mode_settings[$view_mode]['custom_settings']);
// Then, determine access according to the $access parameter. This duplicates
// part of _menu_check_access().
if ($visibility) {
// Grab the variable 'access arguments' part.
$args = array_slice($all_args, 4);
$callback = empty($access_callback) ? 0 : trim($access_callback);
if (is_numeric($callback)) {
return (bool) $callback;
}
else {
// As call_user_func_array() is quite slow and user_access is a very
// common callback, it is worth making a special case for it.
if ($access_callback == 'user_access') {
return (count($args) == 1) ? user_access($args[0]) : user_access($args[0], $args[1]);
}
elseif (function_exists($access_callback)) {
return call_user_func_array($access_callback, $args);
}
}
}
}
/**
* Implements hook_theme().
*/
function field_ui_theme() {
return array(
'field_ui_table' => array(
'render element' => 'elements',
),
);
}
/**
* Implements hook_element_info().
*/
function field_ui_element_info() {
return array(
'field_ui_table' => array(
'#theme' => 'field_ui_table',
'#pre_render' => array('field_ui_table_pre_render'),
'#regions' => array('' => array()),
),
);
}
/**
* Implements hook_field_attach_create_bundle().
*/
function field_ui_field_attach_create_bundle($entity_type, $bundle) {
// When a new bundle is created, the menu needs to be rebuilt to add our
// menu item tabs.
variable_set('menu_rebuild_needed', TRUE);
}
/**
* Determines the administration path for a bundle.
*/
function _field_ui_bundle_admin_path($entity_type, $bundle_name) {
$bundles = field_info_bundles($entity_type);
$bundle_info = $bundles[$bundle_name];
if (isset($bundle_info['admin'])) {
return isset($bundle_info['admin']['real path']) ? $bundle_info['admin']['real path'] : $bundle_info['admin']['path'];
}
}
/**
* Identifies inactive fields within a bundle.
*/
function field_ui_inactive_instances($entity_type, $bundle_name = NULL) {
$params = array('entity_type' => $entity_type);
if (empty($bundle_name)) {
$active = field_info_instances($entity_type);
$inactive = array();
}
else {
// Restrict to the specified bundle. For consistency with the case where
// $bundle_name is NULL, the $active and $inactive arrays are keyed by
// bundle name first.
$params['bundle'] = $bundle_name;
$active = array($bundle_name => field_info_instances($entity_type, $bundle_name));
$inactive = array($bundle_name => array());
}
// Iterate on existing definitions, and spot those that do not appear in the
// $active list collected earlier.
$all_instances = field_read_instances($params, array('include_inactive' => TRUE));
foreach ($all_instances as $instance) {
if (!isset($active[$instance['bundle']][$instance['field_name']])) {
$inactive[$instance['bundle']][$instance['field_name']] = $instance;
}
}
if (!empty($bundle_name)) {
return $inactive[$bundle_name];
}
return $inactive;
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Adds a button 'Save and add fields' to the 'Create content type' form.
*
* @see node_type_form()
* @see field_ui_form_node_type_form_submit()
*/
function field_ui_form_node_type_form_alter(&$form, $form_state) {
// We want to display the button only on add page.
if (empty($form['#node_type']->type)) {
$form['actions']['save_continue'] = array(
'#type' => 'submit',
'#value' => t('Save and add fields'),
'#weight' => 45,
);
$form['#submit'][] = 'field_ui_form_node_type_form_submit';
}
}
/**
* Form submission handler for the 'Save and add fields' button.
*
* @see field_ui_form_node_type_form_alter()
*/
function field_ui_form_node_type_form_submit($form, &$form_state) {
if ($form_state['triggering_element']['#parents'][0] === 'save_continue') {
$form_state['redirect'] = _field_ui_bundle_admin_path('node', $form_state['values']['type']) .'/fields';
}
}
/**
* Access callback to determine if a user is allowed to use the field UI.
*
* Only grant access if the user has both the "administer fields" permission and
* is granted access by the entity specific restrictions.
*/
function field_ui_admin_access($access_callback, $access_arguments) {
return user_access('administer fields') && call_user_func_array($access_callback, $access_arguments);
}
drupalCreateUser(array('access content', 'administer content types', 'administer taxonomy', 'administer fields'));
$this->drupalLogin($admin_user);
// Create content type, with underscores.
$type_name = strtolower($this->randomName(8)) . '_test';
$type = $this->drupalCreateContentType(array('name' => $type_name, 'type' => $type_name));
$this->type = $type->type;
// Store a valid URL name, with hyphens instead of underscores.
$this->hyphen_type = str_replace('_', '-', $this->type);
}
/**
* Creates a new field through the Field UI.
*
* @param $bundle_path
* Admin path of the bundle that the new field is to be attached to.
* @param $initial_edit
* $edit parameter for drupalPost() on the first step ('Manage fields'
* screen).
* @param $field_edit
* $edit parameter for drupalPost() on the second step ('Field settings'
* form).
* @param $instance_edit
* $edit parameter for drupalPost() on the third step ('Instance settings'
* form).
*/
function fieldUIAddNewField($bundle_path, $initial_edit, $field_edit = array(), $instance_edit = array()) {
// Use 'test_field' field type by default.
$initial_edit += array(
'fields[_add_new_field][type]' => 'test_field',
'fields[_add_new_field][widget_type]' => 'test_field_widget',
);
$label = $initial_edit['fields[_add_new_field][label]'];
$field_name = $initial_edit['fields[_add_new_field][field_name]'];
// First step : 'Add new field' on the 'Manage fields' page.
$this->drupalPost("$bundle_path/fields", $initial_edit, t('Save'));
$this->assertRaw(t('These settings apply to the %label field everywhere it is used.', array('%label' => $label)), 'Field settings page was displayed.');
// Second step : 'Field settings' form.
$this->drupalPost(NULL, $field_edit, t('Save field settings'));
$this->assertRaw(t('Updated field %label field settings.', array('%label' => $label)), 'Redirected to instance and widget settings page.');
// Third step : 'Instance settings' form.
$this->drupalPost(NULL, $instance_edit, t('Save settings'));
$this->assertRaw(t('Saved %label configuration.', array('%label' => $label)), 'Redirected to "Manage fields" page.');
// Check that the field appears in the overview form.
$this->assertFieldByXPath('//table[@id="field-overview"]//td[1]', $label, 'Field was created and appears in the overview page.');
}
/**
* Adds an existing field through the Field UI.
*
* @param $bundle_path
* Admin path of the bundle that the field is to be attached to.
* @param $initial_edit
* $edit parameter for drupalPost() on the first step ('Manage fields'
* screen).
* @param $instance_edit
* $edit parameter for drupalPost() on the second step ('Instance settings'
* form).
*/
function fieldUIAddExistingField($bundle_path, $initial_edit, $instance_edit = array()) {
// Use 'test_field_widget' by default.
$initial_edit += array(
'fields[_add_existing_field][widget_type]' => 'test_field_widget',
);
$label = $initial_edit['fields[_add_existing_field][label]'];
$field_name = $initial_edit['fields[_add_existing_field][field_name]'];
// First step : 'Add existing field' on the 'Manage fields' page.
$this->drupalPost("$bundle_path/fields", $initial_edit, t('Save'));
// Second step : 'Instance settings' form.
$this->drupalPost(NULL, $instance_edit, t('Save settings'));
$this->assertRaw(t('Saved %label configuration.', array('%label' => $label)), 'Redirected to "Manage fields" page.');
// Check that the field appears in the overview form.
$this->assertFieldByXPath('//table[@id="field-overview"]//td[1]', $label, 'Field was created and appears in the overview page.');
}
/**
* Deletes a field instance through the Field UI.
*
* @param $bundle_path
* Admin path of the bundle that the field instance is to be deleted from.
* @param $field_name
* The name of the field.
* @param $label
* The label of the field.
* @param $bundle_label
* The label of the bundle.
*/
function fieldUIDeleteField($bundle_path, $field_name, $label, $bundle_label) {
// Display confirmation form.
$this->drupalGet("$bundle_path/fields/$field_name/delete");
$this->assertRaw(t('Are you sure you want to delete the field %label', array('%label' => $label)), 'Delete confirmation was found.');
// Submit confirmation form.
$this->drupalPost(NULL, array(), t('Delete'));
$this->assertRaw(t('The field %label has been deleted from the %type content type.', array('%label' => $label, '%type' => $bundle_label)), 'Delete message was found.');
// Check that the field does not appear in the overview form.
$this->assertNoFieldByXPath('//table[@id="field-overview"]//span[@class="label-field"]', $label, 'Field does not appear in the overview page.');
}
}
/**
* Tests the functionality of the 'Manage fields' screen.
*/
class FieldUIManageFieldsTestCase extends FieldUITestCase {
protected $field_label;
protected $field_name_input;
protected $field_name;
public static function getInfo() {
return array(
'name' => 'Manage fields',
'description' => 'Test the Field UI "Manage fields" screen.',
'group' => 'Field UI',
);
}
function setUp() {
parent::setUp();
// Create random field name.
$this->field_label = $this->randomName(8);
$this->field_name_input = strtolower($this->randomName(8));
$this->field_name = 'field_'. $this->field_name_input;
}
/**
* Runs the field CRUD tests.
*
* In order to act on the same fields, and not create the fields over and over
* again the following tests create, update and delete the same fields.
*/
function testCRUDFields() {
$this->manageFieldsPage();
$this->createField();
$this->updateField();
$this->addExistingField();
}
/**
* Tests the manage fields page.
*/
function manageFieldsPage() {
$this->drupalGet('admin/structure/types/manage/' . $this->hyphen_type . '/fields');
// Check all table columns.
$table_headers = array(
t('Label'),
t('Machine name'),
t('Field type'),
t('Widget'),
t('Operations'),
);
foreach ($table_headers as $table_header) {
// We check that the label appear in the table headings.
$this->assertRaw($table_header . '', format_string('%table_header table header was found.', array('%table_header' => $table_header)));
}
// "Add new field" and "Add existing field" aren't a table heading so just
// test the text.
foreach (array('Add new field', 'Add existing field') as $element) {
$this->assertText($element, format_string('"@element" was found.', array('@element' => $element)));
}
}
/**
* Tests adding a new field.
*
* @todo Assert properties can bet set in the form and read back in $field and
* $instances.
*/
function createField() {
// Create a test field.
$edit = array(
'fields[_add_new_field][label]' => $this->field_label,
'fields[_add_new_field][field_name]' => $this->field_name_input,
);
$this->fieldUIAddNewField('admin/structure/types/manage/' . $this->hyphen_type, $edit);
// Assert the field appears in the "add existing field" section for
// different entity types; e.g. if a field was added in a node entity, it
// should also appear in the 'taxonomy term' entity.
$vocabulary = taxonomy_vocabulary_load(1);
$this->drupalGet('admin/structure/taxonomy/' . $vocabulary->machine_name . '/fields');
$this->assertTrue($this->xpath('//select[@name="fields[_add_existing_field][field_name]"]//option[@value="' . $this->field_name . '"]'), 'Existing field was found in account settings.');
}
/**
* Tests editing an existing field.
*/
function updateField() {
// Go to the field edit page.
$this->drupalGet('admin/structure/types/manage/' . $this->hyphen_type . '/fields/' . $this->field_name);
// Populate the field settings with new settings.
$string = 'updated dummy test string';
$edit = array(
'field[settings][test_field_setting]' => $string,
'instance[settings][test_instance_setting]' => $string,
'instance[widget][settings][test_widget_setting]' => $string,
);
$this->drupalPost(NULL, $edit, t('Save settings'));
// Assert the field settings are correct.
$this->assertFieldSettings($this->type, $this->field_name, $string);
// Assert redirection back to the "manage fields" page.
$this->assertText(t('Saved @label configuration.', array('@label' => $this->field_label)), 'Redirected to "Manage fields" page.');
}
/**
* Tests adding an existing field in another content type.
*/
function addExistingField() {
// Check "Add existing field" appears.
$this->drupalGet('admin/structure/types/manage/page/fields');
$this->assertRaw(t('Add existing field'), '"Add existing field" was found.');
// Check that the list of options respects entity type restrictions on
// fields. The 'comment' field is restricted to the 'comment' entity type
// and should not appear in the list.
$this->assertFalse($this->xpath('//select[@id="edit-add-existing-field-field-name"]//option[@value="comment"]'), 'The list of options respects entity type restrictions.');
// Add a new field based on an existing field.
$edit = array(
'fields[_add_existing_field][label]' => $this->field_label . '_2',
'fields[_add_existing_field][field_name]' => $this->field_name,
);
$this->fieldUIAddExistingField("admin/structure/types/manage/page", $edit);
}
/**
* Asserts field settings are as expected.
*
* @param $bundle
* The bundle name for the instance.
* @param $field_name
* The field name for the instance.
* @param $string
* The settings text.
* @param $entity_type
* The entity type for the instance.
*/
function assertFieldSettings($bundle, $field_name, $string = 'dummy test string', $entity_type = 'node') {
// Reset the fields info.
field_info_cache_clear();
// Assert field settings.
$field = field_info_field($field_name);
$this->assertTrue($field['settings']['test_field_setting'] == $string, 'Field settings were found.');
// Assert instance and widget settings.
$instance = field_info_instance($entity_type, $field_name, $bundle);
$this->assertTrue($instance['settings']['test_instance_setting'] == $string, 'Field instance settings were found.');
$this->assertTrue($instance['widget']['settings']['test_widget_setting'] == $string, 'Field widget settings were found.');
}
/**
* Tests that default value is correctly validated and saved.
*/
function testDefaultValue() {
// Create a test field and instance.
$field_name = 'test';
$field = array(
'field_name' => $field_name,
'type' => 'test_field'
);
field_create_field($field);
$instance = array(
'field_name' => $field_name,
'entity_type' => 'node',
'bundle' => $this->type,
);
field_create_instance($instance);
$langcode = LANGUAGE_NONE;
$admin_path = 'admin/structure/types/manage/' . $this->hyphen_type . '/fields/' . $field_name;
$element_id = "edit-$field_name-$langcode-0-value";
$element_name = "{$field_name}[$langcode][0][value]";
$this->drupalGet($admin_path);
$this->assertFieldById($element_id, '', 'The default value widget was empty.');
// Check that invalid default values are rejected.
$edit = array($element_name => '-1');
$this->drupalPost($admin_path, $edit, t('Save settings'));
$this->assertText("$field_name does not accept the value -1", 'Form vaildation failed.');
// Check that the default value is saved.
$edit = array($element_name => '1');
$this->drupalPost($admin_path, $edit, t('Save settings'));
$this->assertText("Saved $field_name configuration", 'The form was successfully submitted.');
$instance = field_info_instance('node', $field_name, $this->type);
$this->assertEqual($instance['default_value'], array(array('value' => 1)), 'The default value was correctly saved.');
// Check that the default value shows up in the form
$this->drupalGet($admin_path);
$this->assertFieldById($element_id, '1', 'The default value widget was displayed with the correct value.');
// Check that the default value can be emptied.
$edit = array($element_name => '');
$this->drupalPost(NULL, $edit, t('Save settings'));
$this->assertText("Saved $field_name configuration", 'The form was successfully submitted.');
field_info_cache_clear();
$instance = field_info_instance('node', $field_name, $this->type);
$this->assertEqual($instance['default_value'], NULL, 'The default value was correctly saved.');
}
/**
* Tests that deletion removes fields and instances as expected.
*/
function testDeleteField() {
// Create a new field.
$bundle_path1 = 'admin/structure/types/manage/' . $this->hyphen_type;
$edit1 = array(
'fields[_add_new_field][label]' => $this->field_label,
'fields[_add_new_field][field_name]' => $this->field_name_input,
);
$this->fieldUIAddNewField($bundle_path1, $edit1);
// Create an additional node type.
$type_name2 = strtolower($this->randomName(8)) . '_test';
$type2 = $this->drupalCreateContentType(array('name' => $type_name2, 'type' => $type_name2));
$type_name2 = $type2->type;
$hyphen_type2 = str_replace('_', '-', $type_name2);
// Add an instance to the second node type.
$bundle_path2 = 'admin/structure/types/manage/' . $hyphen_type2;
$edit2 = array(
'fields[_add_existing_field][label]' => $this->field_label,
'fields[_add_existing_field][field_name]' => $this->field_name,
);
$this->fieldUIAddExistingField($bundle_path2, $edit2);
// Delete the first instance.
$this->fieldUIDeleteField($bundle_path1, $this->field_name, $this->field_label, $this->type);
// Reset the fields info.
field_info_cache_clear();
// Check that the field instance was deleted.
$this->assertNull(field_info_instance('node', $this->field_name, $this->type), 'Field instance was deleted.');
// Check that the field was not deleted
$this->assertNotNull(field_info_field($this->field_name), 'Field was not deleted.');
// Delete the second instance.
$this->fieldUIDeleteField($bundle_path2, $this->field_name, $this->field_label, $type_name2);
// Reset the fields info.
field_info_cache_clear();
// Check that the field instance was deleted.
$this->assertNull(field_info_instance('node', $this->field_name, $type_name2), 'Field instance was deleted.');
// Check that the field was deleted too.
$this->assertNull(field_info_field($this->field_name), 'Field was deleted.');
}
/**
* Tests that Field UI respects the 'no_ui' option in hook_field_info().
*/
function testHiddenFields() {
$bundle_path = 'admin/structure/types/manage/' . $this->hyphen_type . '/fields/';
// Check that the field type is not available in the 'add new field' row.
$this->drupalGet($bundle_path);
$this->assertFalse($this->xpath('//select[@id="edit-add-new-field-type"]//option[@value="hidden_test_field"]'), "The 'add new field' select respects field types 'no_ui' property.");
// Create a field and an instance programmatically.
$field_name = 'hidden_test_field';
field_create_field(array('field_name' => $field_name, 'type' => $field_name));
$instance = array(
'field_name' => $field_name,
'bundle' => $this->type,
'entity_type' => 'node',
'label' => t('Hidden field'),
'widget' => array('type' => 'test_field_widget'),
);
field_create_instance($instance);
$this->assertTrue(field_read_instance('node', $field_name, $this->type), format_string('An instance of the field %field was created programmatically.', array('%field' => $field_name)));
// Check that the newly added instance appears on the 'Manage Fields'
// screen.
$this->drupalGet($bundle_path);
$this->assertFieldByXPath('//table[@id="field-overview"]//td[1]', $instance['label'], 'Field was created and appears in the overview page.');
// Check that the instance does not appear in the 'add existing field' row
// on other bundles.
$bundle_path = 'admin/structure/types/manage/article/fields/';
$this->drupalGet($bundle_path);
$this->assertFalse($this->xpath('//select[@id="edit-add-existing-field-field-name"]//option[@value=:field_name]', array(':field_name' => $field_name)), "The 'add existing field' select respects field types 'no_ui' property.");
}
/**
* Tests renaming a bundle.
*/
function testRenameBundle() {
$type2 = strtolower($this->randomName(8)) . '_' .'test';
$hyphen_type2 = str_replace('_', '-', $type2);
$options = array(
'type' => $type2,
);
$this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type, $options, t('Save content type'));
$this->drupalGet('admin/structure/types/manage/' . $hyphen_type2 . '/fields');
}
/**
* Tests that a duplicate field name is caught by validation.
*/
function testDuplicateFieldName() {
// field_tags already exists, so we're expecting an error when trying to
// create a new field with the same name.
$edit = array(
'fields[_add_new_field][field_name]' => 'tags',
'fields[_add_new_field][label]' => $this->randomName(),
'fields[_add_new_field][type]' => 'taxonomy_term_reference',
'fields[_add_new_field][widget_type]' => 'options_select',
);
$url = 'admin/structure/types/manage/' . $this->hyphen_type . '/fields';
$this->drupalPost($url, $edit, t('Save'));
$this->assertText(t('The machine-readable name is already in use. It must be unique.'));
$this->assertUrl($url, array(), 'Stayed on the same page.');
}
/**
* Tests that external URLs in the 'destinations' query parameter are blocked.
*/
function testExternalDestinations() {
$path = 'admin/structure/types/manage/article/fields/field_tags/field-settings';
$options = array(
'query' => array('destinations' => array('http://example.com')),
);
$this->drupalPost($path, NULL, t('Save field settings'), $options);
$this->assertUrl('admin/structure/types/manage/article/fields', array(), 'Stayed on the same site.');
}
}
/**
* Tests the functionality of the 'Manage display' screens.
*/
class FieldUIManageDisplayTestCase extends FieldUITestCase {
public static function getInfo() {
return array(
'name' => 'Manage display',
'description' => 'Test the Field UI "Manage display" screens.',
'group' => 'Field UI',
);
}
function setUp() {
parent::setUp(array('search'));
}
/**
* Tests formatter settings.
*/
function testFormatterUI() {
$manage_fields = 'admin/structure/types/manage/' . $this->hyphen_type;
$manage_display = $manage_fields . '/display';
// Create a field, and a node with some data for the field.
$edit = array(
'fields[_add_new_field][label]' => 'Test field',
'fields[_add_new_field][field_name]' => 'test',
);
$this->fieldUIAddNewField($manage_fields, $edit);
// Clear the test-side cache and get the saved field instance.
field_info_cache_clear();
$instance = field_info_instance('node', 'field_test', $this->type);
$format = $instance['display']['default']['type'];
$default_settings = field_info_formatter_settings($format);
$setting_name = key($default_settings);
$setting_value = $instance['display']['default']['settings'][$setting_name];
// Display the "Manage display" screen and check that the expected formatter is
// selected.
$this->drupalGet($manage_display);
$this->assertFieldByName('fields[field_test][type]', $format, 'The expected formatter is selected.');
$this->assertText("$setting_name: $setting_value", 'The expected summary is displayed.');
// Change the formatter and check that the summary is updated.
$edit = array('fields[field_test][type]' => 'field_test_multiple', 'refresh_rows' => 'field_test');
$this->drupalPostAJAX(NULL, $edit, array('op' => t('Refresh')));
$format = 'field_test_multiple';
$default_settings = field_info_formatter_settings($format);
$setting_name = key($default_settings);
$setting_value = $default_settings[$setting_name];
$this->assertFieldByName('fields[field_test][type]', $format, 'The expected formatter is selected.');
$this->assertText("$setting_name: $setting_value", 'The expected summary is displayed.');
// Submit the form and check that the instance is updated.
$this->drupalPost(NULL, array(), t('Save'));
field_info_cache_clear();
$instance = field_info_instance('node', 'field_test', $this->type);
$current_format = $instance['display']['default']['type'];
$current_setting_value = $instance['display']['default']['settings'][$setting_name];
$this->assertEqual($current_format, $format, 'The formatter was updated.');
$this->assertEqual($current_setting_value, $setting_value, 'The setting was updated.');
}
/**
* Tests switching view modes to use custom or 'default' settings'.
*/
function testViewModeCustom() {
// Create a field, and a node with some data for the field.
$edit = array(
'fields[_add_new_field][label]' => 'Test field',
'fields[_add_new_field][field_name]' => 'test',
);
$this->fieldUIAddNewField('admin/structure/types/manage/' . $this->hyphen_type, $edit);
// For this test, use a formatter setting value that is an integer unlikely
// to appear in a rendered node other than as part of the field being tested
// (for example, unlikely to be part of the "Submitted by ... on ..." line).
$value = 12345;
$settings = array(
'type' => $this->type,
'field_test' => array(LANGUAGE_NONE => array(array('value' => $value))),
);
$node = $this->drupalCreateNode($settings);
// Gather expected output values with the various formatters.
$formatters = field_info_formatter_types();
$output = array(
'field_test_default' => $formatters['field_test_default']['settings']['test_formatter_setting'] . '|' . $value,
'field_test_with_prepare_view' => $formatters['field_test_with_prepare_view']['settings']['test_formatter_setting_additional'] . '|' . $value. '|' . ($value + 1),
);
// Check that the field is displayed with the default formatter in 'rss'
// mode (uses 'default'), and hidden in 'teaser' mode (uses custom settings).
$this->assertNodeViewText($node, 'rss', $output['field_test_default'], "The field is displayed as expected in view modes that use 'default' settings.");
$this->assertNodeViewNoText($node, 'teaser', $value, "The field is hidden in view modes that use custom settings.");
// Change fomatter for 'default' mode, check that the field is displayed
// accordingly in 'rss' mode.
$edit = array(
'fields[field_test][type]' => 'field_test_with_prepare_view',
);
$this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type . '/display', $edit, t('Save'));
$this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected in view modes that use 'default' settings.");
// Specialize the 'rss' mode, check that the field is displayed the same.
$edit = array(
"view_modes_custom[rss]" => TRUE,
);
$this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type . '/display', $edit, t('Save'));
$this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected in newly specialized 'rss' mode.");
// Set the field to 'hidden' in the view mode, check that the field is
// hidden.
$edit = array(
'fields[field_test][type]' => 'hidden',
);
$this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type . '/display/rss', $edit, t('Save'));
$this->assertNodeViewNoText($node, 'rss', $value, "The field is hidden in 'rss' mode.");
// Set the view mode back to 'default', check that the field is displayed
// accordingly.
$edit = array(
"view_modes_custom[rss]" => FALSE,
);
$this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type . '/display', $edit, t('Save'));
$this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected when 'rss' mode is set back to 'default' settings.");
// Specialize the view mode again.
$edit = array(
"view_modes_custom[rss]" => TRUE,
);
$this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type . '/display', $edit, t('Save'));
// Check that the previous settings for the view mode have been kept.
$this->assertNodeViewNoText($node, 'rss', $value, "The previous settings are kept when 'rss' mode is specialized again.");
}
/**
* Asserts that a string is found in the rendered node in a view mode.
*
* @param $node
* The node.
* @param $view_mode
* The view mode in which the node should be displayed.
* @param $text
* Plain text to look for.
* @param $message
* Message to display.
*
* @return
* TRUE on pass, FALSE on fail.
*/
function assertNodeViewText($node, $view_mode, $text, $message) {
return $this->assertNodeViewTextHelper($node, $view_mode, $text, $message, FALSE);
}
/**
* Asserts that a string is not found in the rendered node in a view mode.
*
* @param $node
* The node.
* @param $view_mode
* The view mode in which the node should be displayed.
* @param $text
* Plain text to look for.
* @param $message
* Message to display.
* @return
* TRUE on pass, FALSE on fail.
*/
function assertNodeViewNoText($node, $view_mode, $text, $message) {
return $this->assertNodeViewTextHelper($node, $view_mode, $text, $message, TRUE);
}
/**
* Asserts that a string is (not) found in the rendered nodein a view mode.
*
* This helper function is used by assertNodeViewText() and
* assertNodeViewNoText().
*
* @param $node
* The node.
* @param $view_mode
* The view mode in which the node should be displayed.
* @param $text
* Plain text to look for.
* @param $message
* Message to display.
* @param $not_exists
* TRUE if this text should not exist, FALSE if it should.
*
* @return
* TRUE on pass, FALSE on fail.
*/
function assertNodeViewTextHelper($node, $view_mode, $text, $message, $not_exists) {
// Make sure caches on the tester side are refreshed after changes
// submitted on the tested side.
field_info_cache_clear();
// Save current content so that we can restore it when we're done.
$old_content = $this->drupalGetContent();
// Render a cloned node, so that we do not alter the original.
$clone = clone $node;
$element = node_view($clone, $view_mode);
$output = drupal_render($element);
$this->verbose(t('Rendered node - view mode: @view_mode', array('@view_mode' => $view_mode)) . ' '. $output);
// Assign content so that DrupalWebTestCase functions can be used.
$this->drupalSetContent($output);
$method = ($not_exists ? 'assertNoText' : 'assertText');
$return = $this->{$method}((string) $text, $message);
// Restore previous content.
$this->drupalSetContent($old_content);
return $return;
}
}
/**
* Tests custom widget hooks and callbacks on the field administration pages.
*/
class FieldUIAlterTestCase extends DrupalWebTestCase {
public static function getInfo() {
return array(
'name' => 'Widget customization',
'description' => 'Test custom field widget hooks and callbacks on field administration pages.',
'group' => 'Field UI',
);
}
function setUp() {
parent::setUp(array('field_test'));
// Create test user.
$admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer users', 'administer fields'));
$this->drupalLogin($admin_user);
}
/**
* Tests hook_field_widget_properties_alter() on the default field widget.
*
* @see field_test_field_widget_properties_alter()
* @see field_test_field_widget_properties_user_alter()
* @see field_test_field_widget_form_alter()
*/
function testDefaultWidgetPropertiesAlter() {
// Create the alter_test_text field and an instance on article nodes.
field_create_field(array(
'field_name' => 'alter_test_text',
'type' => 'text',
));
field_create_instance(array(
'field_name' => 'alter_test_text',
'entity_type' => 'node',
'bundle' => 'article',
'widget' => array(
'type' => 'text_textfield',
'size' => 60,
),
));
// Test that field_test_field_widget_properties_alter() sets the size to
// 42 and that field_test_field_widget_form_alter() reports the correct
// size when the form is displayed.
$this->drupalGet('admin/structure/types/manage/article/fields/alter_test_text');
$this->assertText('Field size: 42', 'Altered field size is found in hook_field_widget_form_alter().');
// Create the alter_test_options field.
field_create_field(array(
'field_name' => 'alter_test_options',
'type' => 'list_text'
));
// Create instances on users and page nodes.
field_create_instance(array(
'field_name' => 'alter_test_options',
'entity_type' => 'user',
'bundle' => 'user',
'widget' => array(
'type' => 'options_select',
)
));
field_create_instance(array(
'field_name' => 'alter_test_options',
'entity_type' => 'node',
'bundle' => 'page',
'widget' => array(
'type' => 'options_select',
)
));
// Test that field_test_field_widget_properties_user_alter() replaces
// the widget and that field_test_field_widget_form_alter() reports the
// correct widget name when the form is displayed.
$this->drupalGet('admin/config/people/accounts/fields/alter_test_options');
$this->assertText('Widget type: options_buttons', 'Widget type is altered for users in hook_field_widget_form_alter().');
// Test that the widget is not altered on page nodes.
$this->drupalGet('admin/structure/types/manage/page/fields/alter_test_options');
$this->assertText('Widget type: options_select', 'Widget type is not altered for pages in hook_field_widget_form_alter().');
}
}
$grants['node']);
}
}
/**
* @file
* Admin stylesheet for file module.
*/
/**
* Managed file element styles.
*/
.form-managed-file .form-file,
.form-managed-file .form-submit {
margin: 0;
}
.form-managed-file input.progress-disabled {
float: none;
display: inline;
}
.form-managed-file div.ajax-progress,
.form-managed-file div.throbber {
display: inline;
float: none;
padding: 1px 5px 2px 5px;
}
.form-managed-file div.ajax-progress-bar {
display: none;
margin-top: 4px;
width: 28em;
padding: 0;
}
.form-managed-file div.ajax-progress-bar div.bar {
margin: 0;
}
array(
'label' => t('File'),
'description' => t('This field stores the ID of a file as an integer value.'),
'settings' => array(
'display_field' => 0,
'display_default' => 0,
'uri_scheme' => variable_get('file_default_scheme', 'public'),
),
'instance_settings' => array(
'file_extensions' => 'txt',
'file_directory' => '[date:custom:Y]-[date:custom:m]',
'max_filesize' => '',
'description_field' => 0,
),
'default_widget' => 'file_generic',
'default_formatter' => 'file_default',
),
);
}
/**
* Implements hook_field_settings_form().
*/
function file_field_settings_form($field, $instance, $has_data) {
$defaults = field_info_field_settings($field['type']);
$settings = array_merge($defaults, $field['settings']);
$form['#attached']['js'][] = drupal_get_path('module', 'file') . '/file.js';
$form['display_field'] = array(
'#type' => 'checkbox',
'#title' => t('Enable Display field'),
'#default_value' => $settings['display_field'],
'#description' => t('The display option allows users to choose if a file should be shown when viewing the content.'),
);
$form['display_default'] = array(
'#type' => 'checkbox',
'#title' => t('Files displayed by default'),
'#default_value' => $settings['display_default'],
'#description' => t('This setting only has an effect if the display option is enabled.'),
);
$scheme_options = array();
foreach (file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE) as $scheme => $stream_wrapper) {
$scheme_options[$scheme] = $stream_wrapper['name'];
}
$form['uri_scheme'] = array(
'#type' => 'radios',
'#title' => t('Upload destination'),
'#options' => $scheme_options,
'#default_value' => $settings['uri_scheme'],
'#description' => t('Select where the final files should be stored. Private file storage has significantly more overhead than public files, but allows restricted access to files within this field.'),
'#disabled' => $has_data,
);
return $form;
}
/**
* Implements hook_field_instance_settings_form().
*/
function file_field_instance_settings_form($field, $instance) {
$settings = $instance['settings'];
$form['file_directory'] = array(
'#type' => 'textfield',
'#title' => t('File directory'),
'#default_value' => $settings['file_directory'],
'#description' => t('Optional subdirectory within the upload destination where files will be stored. Do not include preceding or trailing slashes.'),
'#element_validate' => array('_file_generic_settings_file_directory_validate'),
'#weight' => 3,
);
// Make the extension list a little more human-friendly by comma-separation.
$extensions = str_replace(' ', ', ', $settings['file_extensions']);
$form['file_extensions'] = array(
'#type' => 'textfield',
'#title' => t('Allowed file extensions'),
'#default_value' => $extensions,
'#description' => t('Separate extensions with a space or comma and do not include the leading dot.'),
'#element_validate' => array('_file_generic_settings_extensions'),
'#weight' => 1,
'#maxlength' => 256,
// By making this field required, we prevent a potential security issue
// that would allow files of any type to be uploaded.
'#required' => TRUE,
);
$form['max_filesize'] = array(
'#type' => 'textfield',
'#title' => t('Maximum upload size'),
'#default_value' => $settings['max_filesize'],
'#description' => t('Enter a value like "512" (bytes), "80 KB" (kilobytes) or "50 MB" (megabytes) in order to restrict the allowed file size. If left empty the file sizes will be limited only by PHP\'s maximum post and file upload sizes (current limit %limit ).', array('%limit' => format_size(file_upload_max_size()))),
'#size' => 10,
'#element_validate' => array('_file_generic_settings_max_filesize'),
'#weight' => 5,
);
$form['description_field'] = array(
'#type' => 'checkbox',
'#title' => t('Enable Description field'),
'#default_value' => isset($settings['description_field']) ? $settings['description_field'] : '',
'#description' => t('The description field allows users to enter a description about the uploaded file.'),
'#parents' => array('instance', 'settings', 'description_field'),
'#weight' => 11,
);
return $form;
}
/**
* Element validate callback for the maximum upload size field.
*
* Ensure a size that can be parsed by parse_size() has been entered.
*/
function _file_generic_settings_max_filesize($element, &$form_state) {
if (!empty($element['#value']) && !is_numeric(parse_size($element['#value']))) {
form_error($element, t('The "!name" option must contain a valid value. You may either leave the text field empty or enter a string like "512" (bytes), "80 KB" (kilobytes) or "50 MB" (megabytes).', array('!name' => t($element['title']))));
}
}
/**
* Element validate callback for the allowed file extensions field.
*
* This doubles as a convenience clean-up function and a validation routine.
* Commas are allowed by the end-user, but ultimately the value will be stored
* as a space-separated list for compatibility with file_validate_extensions().
*/
function _file_generic_settings_extensions($element, &$form_state) {
if (!empty($element['#value'])) {
$extensions = preg_replace('/([, ]+\.?)/', ' ', trim(strtolower($element['#value'])));
$extensions = array_filter(explode(' ', $extensions));
$extensions = implode(' ', array_unique($extensions));
if (!preg_match('/^([a-z0-9]+([.][a-z0-9])* ?)+$/', $extensions)) {
form_error($element, t('The list of allowed extensions is not valid, be sure to exclude leading dots and to separate extensions with a comma or space.'));
}
else {
form_set_value($element, $extensions, $form_state);
}
}
}
/**
* Element validate callback for the file destination field.
*
* Remove slashes from the beginning and end of the destination value and ensure
* that the file directory path is not included at the beginning of the value.
*/
function _file_generic_settings_file_directory_validate($element, &$form_state) {
// Strip slashes from the beginning and end of $widget['file_directory'].
$value = trim($element['#value'], '\\/');
form_set_value($element, $value, $form_state);
}
/**
* Implements hook_field_load().
*/
function file_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
$fids = array();
foreach ($entities as $id => $entity) {
// Load the files from the files table.
foreach ($items[$id] as $delta => $item) {
if (!empty($item['fid'])) {
$fids[] = $item['fid'];
}
}
}
$files = file_load_multiple($fids);
foreach ($entities as $id => $entity) {
foreach ($items[$id] as $delta => $item) {
// If the file does not exist, mark the entire item as empty.
if (empty($item['fid']) || !isset($files[$item['fid']])) {
unset($items[$id][$delta]);
}
else {
$items[$id][$delta] = array_merge((array) $files[$item['fid']], $item);
}
}
}
}
/**
* Implements hook_field_prepare_view().
*/
function file_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
// Remove files specified to not be displayed.
foreach ($entities as $id => $entity) {
foreach ($items[$id] as $delta => $item) {
if (!file_field_displayed($item, $field)) {
unset($items[$id][$delta]);
}
}
// Ensure consecutive deltas.
$items[$id] = array_values($items[$id]);
}
}
/**
* Implements hook_field_presave().
*/
function file_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
// Make sure that each file which will be saved with this object has a
// permanent status, so that it will not be removed when temporary files are
// cleaned up.
foreach ($items as $delta => $item) {
if (empty($item['fid'])) {
unset($items[$delta]);
continue;
}
$file = file_load($item['fid']);
if (empty($file)) {
unset($items[$delta]);
continue;
}
if (!$file->status) {
$file->status = FILE_STATUS_PERMANENT;
file_save($file);
}
}
}
/**
* Implements hook_field_insert().
*/
function file_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
// Add a new usage of each uploaded file.
foreach ($items as $item) {
$file = (object) $item;
file_usage_add($file, 'file', $entity_type, $id);
}
}
/**
* Implements hook_field_update().
*
* Checks for files that have been removed from the object.
*/
function file_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) {
// Check whether the field is defined on the object.
if (!isset($entity->{$field['field_name']})) {
// We cannot check for removed files if the field is not defined.
return;
}
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
// On new revisions, all files are considered to be a new usage and no
// deletion of previous file usages are necessary.
if (!empty($entity->revision)) {
foreach ($items as $item) {
$file = (object) $item;
file_usage_add($file, 'file', $entity_type, $id);
}
return;
}
// Build a display of the current FIDs.
$current_fids = array();
foreach ($items as $item) {
$current_fids[] = $item['fid'];
}
// Compare the original field values with the ones that are being saved. Use
// $entity->original to check this when possible, but if it isn't available,
// create a bare-bones entity and load its previous values instead.
if (isset($entity->original)) {
$original = $entity->original;
}
else {
$original = entity_create_stub_entity($entity_type, array($id, $vid, $bundle));
field_attach_load($entity_type, array($id => $original), FIELD_LOAD_CURRENT, array('field_id' => $field['id']));
}
$original_fids = array();
if (!empty($original->{$field['field_name']}[$langcode])) {
foreach ($original->{$field['field_name']}[$langcode] as $original_item) {
$original_fids[] = $original_item['fid'];
if (isset($original_item['fid']) && !in_array($original_item['fid'], $current_fids)) {
// Decrement the file usage count by 1 and delete the file if possible.
file_field_delete_file($original_item, $field, $entity_type, $id);
}
}
}
// Add new usage entries for newly added files.
foreach ($items as $item) {
if (!in_array($item['fid'], $original_fids)) {
$file = (object) $item;
file_usage_add($file, 'file', $entity_type, $id);
}
}
}
/**
* Implements hook_field_delete().
*/
function file_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
// Delete all file usages within this entity.
foreach ($items as $delta => $item) {
file_field_delete_file($item, $field, $entity_type, $id, 0);
}
}
/**
* Implements hook_field_delete_revision().
*/
function file_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, &$items) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
foreach ($items as $delta => $item) {
// Decrement the file usage count by 1 and delete the file if possible.
if (file_field_delete_file($item, $field, $entity_type, $id)) {
$items[$delta] = NULL;
}
}
}
/**
* Decrements the usage count for a file and attempts to delete it.
*
* This function only has an effect if the file being deleted is used only by
* File module.
*
* @param $item
* The field item that contains a file array.
* @param $field
* The field structure for the operation.
* @param $entity_type
* The type of $entity.
* @param $id
* The entity ID which contains the file being deleted.
* @param $count
* (optional) The number of references to decrement from the object
* containing the file. Defaults to 1.
*
* @return
* Boolean TRUE if the file was deleted, or an array of remaining references
* if the file is still in use by other modules. Boolean FALSE if an error
* was encountered.
*/
function file_field_delete_file($item, $field, $entity_type, $id, $count = 1) {
// To prevent the file field from deleting files it doesn't know about, check
// the file reference count. Temporary files can be deleted because they
// are not yet associated with any content at all.
$file = (object) $item;
$file_usage = file_usage_list($file);
if ($file->status == 0 || !empty($file_usage['file'])) {
file_usage_delete($file, 'file', $entity_type, $id, $count);
return file_delete($file);
}
// Even if the file is not deleted, return TRUE to indicate the file field
// record can be removed from the field database tables.
return TRUE;
}
/**
* Implements hook_field_is_empty().
*/
function file_field_is_empty($item, $field) {
return empty($item['fid']);
}
/**
* Determines whether a file should be displayed when outputting field content.
*
* @param $item
* A field item array.
* @param $field
* A field array.
*
* @return
* Boolean TRUE if the file will be displayed, FALSE if the file is hidden.
*/
function file_field_displayed($item, $field) {
if (!empty($field['settings']['display_field'])) {
return (bool) $item['display'];
}
return TRUE;
}
/**
* Implements hook_field_formatter_info().
*/
function file_field_formatter_info() {
return array(
'file_default' => array(
'label' => t('Generic file'),
'field types' => array('file'),
),
'file_table' => array(
'label' => t('Table of files'),
'field types' => array('file'),
),
'file_url_plain' => array(
'label' => t('URL to file'),
'field types' => array('file'),
),
);
}
/**
* Implements hook_field_widget_info().
*/
function file_field_widget_info() {
return array(
'file_generic' => array(
'label' => t('File'),
'field types' => array('file'),
'settings' => array(
'progress_indicator' => 'throbber',
),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_CUSTOM,
'default value' => FIELD_BEHAVIOR_NONE,
),
),
);
}
/**
* Implements hook_field_widget_settings_form().
*/
function file_field_widget_settings_form($field, $instance) {
$widget = $instance['widget'];
$settings = $widget['settings'];
$form['progress_indicator'] = array(
'#type' => 'radios',
'#title' => t('Progress indicator'),
'#options' => array(
'throbber' => t('Throbber'),
'bar' => t('Bar with progress meter'),
),
'#default_value' => $settings['progress_indicator'],
'#description' => t('The throbber display does not show the status of uploads but takes up less space. The progress bar is helpful for monitoring progress on large uploads.'),
'#weight' => 16,
'#access' => file_progress_implementation(),
);
return $form;
}
/**
* Implements hook_field_widget_form().
*/
function file_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
$defaults = array(
'fid' => 0,
'display' => !empty($field['settings']['display_default']),
'description' => '',
);
// Load the items for form rebuilds from the field state as they might not be
// in $form_state['values'] because of validation limitations. Also, they are
// only passed in as $items when editing existing entities.
$field_state = field_form_get_state($element['#field_parents'], $field['field_name'], $langcode, $form_state);
if (isset($field_state['items'])) {
$items = $field_state['items'];
}
// Essentially we use the managed_file type, extended with some enhancements.
$element_info = element_info('managed_file');
$element += array(
'#type' => 'managed_file',
'#upload_location' => file_field_widget_uri($field, $instance),
'#upload_validators' => file_field_widget_upload_validators($field, $instance),
'#value_callback' => 'file_field_widget_value',
'#process' => array_merge($element_info['#process'], array('file_field_widget_process')),
'#progress_indicator' => $instance['widget']['settings']['progress_indicator'],
// Allows this field to return an array instead of a single value.
'#extended' => TRUE,
);
if ($field['cardinality'] == 1) {
// Set the default value.
$element['#default_value'] = !empty($items) ? $items[0] : $defaults;
// If there's only one field, return it as delta 0.
if (empty($element['#default_value']['fid'])) {
$element['#description'] = theme('file_upload_help', array('description' => $element['#description'], 'upload_validators' => $element['#upload_validators']));
}
$elements = array($element);
}
else {
// If there are multiple values, add an element for each existing one.
foreach ($items as $item) {
$elements[$delta] = $element;
$elements[$delta]['#default_value'] = $item;
$elements[$delta]['#weight'] = $delta;
$delta++;
}
// And then add one more empty row for new uploads except when this is a
// programmed form as it is not necessary.
if (($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta < $field['cardinality']) && empty($form_state['programmed'])) {
$elements[$delta] = $element;
$elements[$delta]['#default_value'] = $defaults;
$elements[$delta]['#weight'] = $delta;
$elements[$delta]['#required'] = ($element['#required'] && $delta == 0);
}
// The group of elements all-together need some extra functionality
// after building up the full list (like draggable table rows).
$elements['#file_upload_delta'] = $delta;
$elements['#theme'] = 'file_widget_multiple';
$elements['#theme_wrappers'] = array('fieldset');
$elements['#process'] = array('file_field_widget_process_multiple');
$elements['#title'] = $element['#title'];
$elements['#description'] = $element['#description'];
$elements['#field_name'] = $element['#field_name'];
$elements['#language'] = $element['#language'];
$elements['#display_field'] = $field['settings']['display_field'];
// Add some properties that will eventually be added to the file upload
// field. These are added here so that they may be referenced easily through
// a hook_form_alter().
$elements['#file_upload_title'] = t('Add a new file');
$elements['#file_upload_description'] = theme('file_upload_help', array('description' => '', 'upload_validators' => $elements[0]['#upload_validators']));
}
return $elements;
}
/**
* Retrieves the upload validators for a file field.
*
* @param $field
* A field array.
*
* @return
* An array suitable for passing to file_save_upload() or the file field
* element's '#upload_validators' property.
*/
function file_field_widget_upload_validators($field, $instance) {
// Cap the upload size according to the PHP limit.
$max_filesize = parse_size(file_upload_max_size());
if (!empty($instance['settings']['max_filesize']) && parse_size($instance['settings']['max_filesize']) < $max_filesize) {
$max_filesize = parse_size($instance['settings']['max_filesize']);
}
$validators = array();
// There is always a file size limit due to the PHP server limit.
$validators['file_validate_size'] = array($max_filesize);
// Add the extension check if necessary.
if (!empty($instance['settings']['file_extensions'])) {
$validators['file_validate_extensions'] = array($instance['settings']['file_extensions']);
}
return $validators;
}
/**
* Determines the URI for a file field instance.
*
* @param $field
* A field array.
* @param $instance
* A field instance array.
* @param $data
* An array of token objects to pass to token_replace().
*
* @return
* A file directory URI with tokens replaced.
*
* @see token_replace()
*/
function file_field_widget_uri($field, $instance, $data = array()) {
$destination = trim($instance['settings']['file_directory'], '/');
// Replace tokens.
$destination = token_replace($destination, $data);
return $field['settings']['uri_scheme'] . '://' . $destination;
}
/**
* The #value_callback for the file_generic field element.
*/
function file_field_widget_value($element, $input = FALSE, $form_state = array()) {
if ($input) {
// Checkboxes lose their value when empty.
// If the display field is present make sure its unchecked value is saved.
$field = field_widget_field($element, $form_state);
if (empty($input['display'])) {
$input['display'] = !empty($field['settings']['display_field']) ? 0 : 1;
}
}
// We depend on the managed file element to handle uploads.
$return = file_managed_file_value($element, $input, $form_state);
// Ensure that all the required properties are returned even if empty.
$return += array(
'fid' => 0,
'display' => 1,
'description' => '',
);
return $return;
}
/**
* An element #process callback for the file_generic field type.
*
* Expands the file_generic type to include the description and display fields.
*/
function file_field_widget_process($element, &$form_state, $form) {
$item = $element['#value'];
$item['fid'] = $element['fid']['#value'];
$field = field_widget_field($element, $form_state);
$instance = field_widget_instance($element, $form_state);
$settings = $instance['widget']['settings'];
$element['#theme'] = 'file_widget';
// Add the display field if enabled.
if (!empty($field['settings']['display_field'])) {
$element['display'] = array(
'#type' => empty($item['fid']) ? 'hidden' : 'checkbox',
'#title' => t('Include file in display'),
'#value' => isset($item['display']) ? $item['display'] : $field['settings']['display_default'],
'#attributes' => array('class' => array('file-display')),
);
}
else {
$element['display'] = array(
'#type' => 'hidden',
'#value' => '1',
);
}
// Add the description field if enabled.
if (!empty($instance['settings']['description_field']) && $item['fid']) {
$element['description'] = array(
'#type' => variable_get('file_description_type', 'textfield'),
'#title' => t('Description'),
'#value' => isset($item['description']) ? $item['description'] : '',
'#maxlength' => variable_get('file_description_length', 128),
'#description' => t('The description may be used as the label of the link to the file.'),
);
}
// Adjust the Ajax settings so that on upload and remove of any individual
// file, the entire group of file fields is updated together.
if ($field['cardinality'] != 1) {
$parents = array_slice($element['#array_parents'], 0, -1);
$new_path = 'file/ajax/' . implode('/', $parents) . '/' . $form['form_build_id']['#value'];
$field_element = drupal_array_get_nested_value($form, $parents);
$new_wrapper = $field_element['#id'] . '-ajax-wrapper';
foreach (element_children($element) as $key) {
if (isset($element[$key]['#ajax'])) {
$element[$key]['#ajax']['path'] = $new_path;
$element[$key]['#ajax']['wrapper'] = $new_wrapper;
}
}
unset($element['#prefix'], $element['#suffix']);
}
// Add another submit handler to the upload and remove buttons, to implement
// functionality needed by the field widget. This submit handler, along with
// the rebuild logic in file_field_widget_form() requires the entire field,
// not just the individual item, to be valid.
foreach (array('upload_button', 'remove_button') as $key) {
$element[$key]['#submit'][] = 'file_field_widget_submit';
$element[$key]['#limit_validation_errors'] = array(array_slice($element['#parents'], 0, -1));
}
return $element;
}
/**
* An element #process callback for a group of file_generic fields.
*
* Adds the weight field to each row so it can be ordered and adds a new Ajax
* wrapper around the entire group so it can be replaced all at once.
*/
function file_field_widget_process_multiple($element, &$form_state, $form) {
$element_children = element_children($element, TRUE);
$count = count($element_children);
foreach ($element_children as $delta => $key) {
if ($key != $element['#file_upload_delta']) {
$description = _file_field_get_description_from_element($element[$key]);
$element[$key]['_weight'] = array(
'#type' => 'weight',
'#title' => $description ? t('Weight for @title', array('@title' => $description)) : t('Weight for new file'),
'#title_display' => 'invisible',
'#delta' => $count,
'#default_value' => $delta,
);
}
else {
// The title needs to be assigned to the upload field so that validation
// errors include the correct widget label.
$element[$key]['#title'] = $element['#title'];
$element[$key]['_weight'] = array(
'#type' => 'hidden',
'#default_value' => $delta,
);
}
}
// Add a new wrapper around all the elements for Ajax replacement.
$element['#prefix'] = '';
$element['#suffix'] = '
';
return $element;
}
/**
* Retrieves the file description from a field field element.
*
* This helper function is used by file_field_widget_process_multiple().
*
* @param $element
* The element being processed.
*
* @return
* A description of the file suitable for use in the administrative interface.
*/
function _file_field_get_description_from_element($element) {
// Use the actual file description, if it's available.
if (!empty($element['#default_value']['description'])) {
return $element['#default_value']['description'];
}
// Otherwise, fall back to the filename.
if (!empty($element['#default_value']['filename'])) {
return $element['#default_value']['filename'];
}
// This is probably a newly uploaded file; no description is available.
return FALSE;
}
/**
* Form submission handler for upload/remove button of file_field_widget_form().
*
* This runs in addition to and after file_managed_file_submit().
*
* @see file_managed_file_submit()
* @see file_field_widget_form()
* @see file_field_widget_process()
*/
function file_field_widget_submit($form, &$form_state) {
// During the form rebuild, file_field_widget_form() will create field item
// widget elements using re-indexed deltas, so clear out $form_state['input']
// to avoid a mismatch between old and new deltas. The rebuilt elements will
// have #default_value set appropriately for the current state of the field,
// so nothing is lost in doing this.
$parents = array_slice($form_state['triggering_element']['#parents'], 0, -2);
drupal_array_set_nested_value($form_state['input'], $parents, NULL);
$button = $form_state['triggering_element'];
// Go one level up in the form, to the widgets container.
$element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -1));
$field_name = $element['#field_name'];
$langcode = $element['#language'];
$parents = $element['#field_parents'];
$submitted_values = drupal_array_get_nested_value($form_state['values'], array_slice($button['#parents'], 0, -2));
foreach ($submitted_values as $delta => $submitted_value) {
if (!$submitted_value['fid']) {
unset($submitted_values[$delta]);
}
}
// Re-index deltas after removing empty items.
$submitted_values = array_values($submitted_values);
// Update form_state values.
drupal_array_set_nested_value($form_state['values'], array_slice($button['#parents'], 0, -2), $submitted_values);
// Update items.
$field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
$field_state['items'] = $submitted_values;
field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
}
/**
* Returns HTML for an individual file upload widget.
*
* @param $variables
* An associative array containing:
* - element: A render element representing the widget.
*
* @ingroup themeable
*/
function theme_file_widget($variables) {
$element = $variables['element'];
$output = '';
// The "form-managed-file" class is required for proper Ajax functionality.
$output .= '';
if ($element['fid']['#value'] != 0) {
// Add the file size after the file name.
$element['filename']['#markup'] .= ' (' . format_size($element['#file']->filesize) . ') ';
}
$output .= drupal_render_children($element);
$output .= '
';
return $output;
}
/**
* Returns HTML for a group of file upload widgets.
*
* @param $variables
* An associative array containing:
* - element: A render element representing the widgets.
*
* @ingroup themeable
*/
function theme_file_widget_multiple($variables) {
$element = $variables['element'];
// Special ID and classes for draggable tables.
$weight_class = $element['#id'] . '-weight';
$table_id = $element['#id'] . '-table';
// Build up a table of applicable fields.
$headers = array();
$headers[] = t('File information');
if ($element['#display_field']) {
$headers[] = array(
'data' => t('Display'),
'class' => array('checkbox'),
);
}
$headers[] = t('Weight');
$headers[] = t('Operations');
// Get our list of widgets in order (needed when the form comes back after
// preview or failed validation).
$widgets = array();
foreach (element_children($element) as $key) {
$widgets[] = &$element[$key];
}
usort($widgets, '_field_sort_items_value_helper');
$rows = array();
foreach ($widgets as $key => &$widget) {
// Save the uploading row for last.
if ($widget['#file'] == FALSE) {
$widget['#title'] = $element['#file_upload_title'];
$widget['#description'] = $element['#file_upload_description'];
continue;
}
// Delay rendering of the buttons, so that they can be rendered later in the
// "operations" column.
$operations_elements = array();
foreach (element_children($widget) as $sub_key) {
if (isset($widget[$sub_key]['#type']) && $widget[$sub_key]['#type'] == 'submit') {
hide($widget[$sub_key]);
$operations_elements[] = &$widget[$sub_key];
}
}
// Delay rendering of the "Display" option and the weight selector, so that
// each can be rendered later in its own column.
if ($element['#display_field']) {
hide($widget['display']);
}
hide($widget['_weight']);
// Render everything else together in a column, without the normal wrappers.
$widget['#theme_wrappers'] = array();
$information = drupal_render($widget);
// Render the previously hidden elements, using render() instead of
// drupal_render(), to undo the earlier hide().
$operations = '';
foreach ($operations_elements as $operation_element) {
$operations .= render($operation_element);
}
$display = '';
if ($element['#display_field']) {
unset($widget['display']['#title']);
$display = array(
'data' => render($widget['display']),
'class' => array('checkbox'),
);
}
$widget['_weight']['#attributes']['class'] = array($weight_class);
$weight = render($widget['_weight']);
// Arrange the row with all of the rendered columns.
$row = array();
$row[] = $information;
if ($element['#display_field']) {
$row[] = $display;
}
$row[] = $weight;
$row[] = $operations;
$rows[] = array(
'data' => $row,
'class' => isset($widget['#attributes']['class']) ? array_merge($widget['#attributes']['class'], array('draggable')) : array('draggable'),
);
}
drupal_add_tabledrag($table_id, 'order', 'sibling', $weight_class);
$output = '';
$output = empty($rows) ? '' : theme('table', array('header' => $headers, 'rows' => $rows, 'attributes' => array('id' => $table_id)));
$output .= drupal_render_children($element);
return $output;
}
/**
* Returns HTML for help text based on file upload validators.
*
* @param $variables
* An associative array containing:
* - description: The normal description for this field, specified by the
* user.
* - upload_validators: An array of upload validators as used in
* $element['#upload_validators'].
*
* @ingroup themeable
*/
function theme_file_upload_help($variables) {
$description = $variables['description'];
$upload_validators = $variables['upload_validators'];
$descriptions = array();
if (!empty($description)) {
$descriptions[] = $description;
}
if (isset($upload_validators['file_validate_size'])) {
$descriptions[] = t('Files must be less than !size.', array('!size' => '' . format_size($upload_validators['file_validate_size'][0]) . ' '));
}
if (isset($upload_validators['file_validate_extensions'])) {
$descriptions[] = t('Allowed file types: !extensions.', array('!extensions' => '' . check_plain($upload_validators['file_validate_extensions'][0]) . ' '));
}
if (isset($upload_validators['file_validate_image_resolution'])) {
$max = $upload_validators['file_validate_image_resolution'][0];
$min = $upload_validators['file_validate_image_resolution'][1];
if ($min && $max) {
$descriptions[] = t('Images must be at least !min pixels. Images larger than !max pixels will be resized.', array('!min' => '' . $min . ' ', '!max' => '' . $max . ' '));
}
elseif ($min) {
$descriptions[] = t('Images must be at least !min pixels.', array('!min' => '' . $min . ' '));
}
elseif ($max) {
$descriptions[] = t('Images larger than !max pixels will be resized.', array('!max' => '' . $max . ' '));
}
}
return implode(' ', $descriptions);
}
/**
* Implements hook_field_formatter_view().
*/
function file_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$element = array();
switch ($display['type']) {
case 'file_default':
foreach ($items as $delta => $item) {
$element[$delta] = array(
'#theme' => 'file_link',
'#file' => (object) $item,
);
}
break;
case 'file_url_plain':
foreach ($items as $delta => $item) {
$element[$delta] = array('#markup' => empty($item['uri']) ? '' : file_create_url($item['uri']));
}
break;
case 'file_table':
if (!empty($items)) {
// Display all values in a single element..
$element[0] = array(
'#theme' => 'file_formatter_table',
'#items' => $items,
);
}
break;
}
return $element;
}
/**
* Returns HTML for a file attachments table.
*
* @param $variables
* An associative array containing:
* - items: An array of file attachments.
*
* @ingroup themeable
*/
function theme_file_formatter_table($variables) {
$header = array(t('Attachment'), t('Size'));
$rows = array();
foreach ($variables['items'] as $delta => $item) {
$rows[] = array(
theme('file_link', array('file' => (object) $item)),
format_size($item['filesize']),
);
}
return empty($rows) ? '' : theme('table', array('header' => $header, 'rows' => $rows));
}
name = File
description = Defines a file field type.
package = Core
version = VERSION
core = 7.x
dependencies[] = field
files[] = tests/file.test
; Information added by Drupal.org packaging script on 2023-03-15
version = "7.95"
project = "drupal"
datestamp = "1678902530"
array(
'fid' => array(
'description' => 'The {file_managed}.fid being referenced in this field.',
'type' => 'int',
'not null' => FALSE,
'unsigned' => TRUE,
),
'display' => array(
'description' => 'Flag to control whether this file should be displayed when viewing content.',
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 1,
),
'description' => array(
'description' => 'A description of the file.',
'type' => 'text',
'not null' => FALSE,
),
),
'indexes' => array(
'fid' => array('fid'),
),
'foreign keys' => array(
'fid' => array(
'table' => 'file_managed',
'columns' => array('fid' => 'fid'),
),
),
);
}
/**
* Implements hook_requirements().
*
* Display information about getting upload progress bars working.
*/
function file_requirements($phase) {
$requirements = array();
// Check the server's ability to indicate upload progress.
if ($phase == 'runtime') {
$description = NULL;
$implementation = file_progress_implementation();
// Test the web server identity.
$server_software = $_SERVER['SERVER_SOFTWARE'];
if (preg_match("/Nginx/i", $server_software)) {
$is_nginx = TRUE;
$is_apache = FALSE;
$fastcgi = FALSE;
}
elseif (preg_match("/Apache/i", $server_software)) {
$is_nginx = FALSE;
$is_apache = TRUE;
$fastcgi = strpos($server_software, 'mod_fastcgi') !== FALSE || strpos($server_software, 'mod_fcgi') !== FALSE;
}
else {
$is_nginx = FALSE;
$is_apache = FALSE;
$fastcgi = FALSE;
}
if (!$is_apache && !$is_nginx) {
$value = t('Not enabled');
$description = t('Your server is not capable of displaying file upload progress. File upload progress requires an Apache server running PHP with mod_php or Nginx with PHP-FPM.');
$severity = REQUIREMENT_INFO;
}
elseif ($fastcgi) {
$value = t('Not enabled');
$description = t('Your server is not capable of displaying file upload progress. File upload progress requires PHP be run with mod_php or PHP-FPM and not as FastCGI.');
$severity = REQUIREMENT_INFO;
}
elseif (!$implementation && extension_loaded('apc')) {
$value = t('Not enabled');
$description = t('Your server is capable of displaying file upload progress through APC, but it is not enabled. Add apc.rfc1867 = 1 to your php.ini configuration. Alternatively, it is recommended to use PECL uploadprogress , which supports more than one simultaneous upload.');
$severity = REQUIREMENT_INFO;
}
elseif (!$implementation) {
$value = t('Not enabled');
$description = t('Your server is capable of displaying file upload progress, but does not have the required libraries. It is recommended to install the PECL uploadprogress library (preferred) or to install APC .');
$severity = REQUIREMENT_INFO;
}
elseif ($implementation == 'apc') {
$value = t('Enabled (APC RFC1867 )');
$description = t('Your server is capable of displaying file upload progress using APC RFC1867. Note that only one upload at a time is supported. It is recommended to use the PECL uploadprogress library if possible.');
$severity = REQUIREMENT_OK;
}
elseif ($implementation == 'uploadprogress') {
$value = t('Enabled (PECL uploadprogress )');
$severity = REQUIREMENT_OK;
}
$requirements['file_progress'] = array(
'title' => t('Upload progress'),
'value' => $value,
'severity' => $severity,
'description' => $description,
);
}
return $requirements;
}
/**
* @file
* Provides JavaScript additions to the managed file field type.
*
* This file provides progress bar support (if available), popup windows for
* file previews, and disabling of other file fields during Ajax uploads (which
* prevents separate file fields from accidentally uploading files).
*/
(function ($) {
/**
* Attach behaviors to managed file element upload fields.
*/
Drupal.behaviors.fileValidateAutoAttach = {
attach: function (context, settings) {
if (settings.file && settings.file.elements) {
$.each(settings.file.elements, function(selector) {
var extensions = settings.file.elements[selector];
$(selector, context).bind('change', {extensions: extensions}, Drupal.file.validateExtension);
});
}
},
detach: function (context, settings) {
if (settings.file && settings.file.elements) {
$.each(settings.file.elements, function(selector) {
$(selector, context).unbind('change', Drupal.file.validateExtension);
});
}
}
};
/**
* Attach behaviors to the file upload and remove buttons.
*/
Drupal.behaviors.fileButtons = {
attach: function (context) {
$('input.form-submit', context).bind('mousedown', Drupal.file.disableFields);
$('div.form-managed-file input.form-submit', context).bind('mousedown', Drupal.file.progressBar);
},
detach: function (context) {
$('input.form-submit', context).unbind('mousedown', Drupal.file.disableFields);
$('div.form-managed-file input.form-submit', context).unbind('mousedown', Drupal.file.progressBar);
}
};
/**
* Attach behaviors to links within managed file elements.
*/
Drupal.behaviors.filePreviewLinks = {
attach: function (context) {
$('div.form-managed-file .file a, .file-widget .file a', context).bind('click',Drupal.file.openInNewWindow);
},
detach: function (context){
$('div.form-managed-file .file a, .file-widget .file a', context).unbind('click', Drupal.file.openInNewWindow);
}
};
/**
* File upload utility functions.
*/
Drupal.file = Drupal.file || {
/**
* Client-side file input validation of file extensions.
*/
validateExtension: function (event) {
// Remove any previous errors.
$('.file-upload-js-error').remove();
// Add client side validation for the input[type=file].
var extensionPattern = event.data.extensions.replace(/,\s*/g, '|');
if (extensionPattern.length > 1 && this.value.length > 0) {
var acceptableMatch = new RegExp('\\.(' + extensionPattern + ')$', 'gi');
if (!acceptableMatch.test(this.value)) {
var error = Drupal.t("The selected file %filename cannot be uploaded. Only files with the following extensions are allowed: %extensions.", {
// According to the specifications of HTML5, a file upload control
// should not reveal the real local path to the file that a user
// has selected. Some web browsers implement this restriction by
// replacing the local path with "C:\fakepath\", which can cause
// confusion by leaving the user thinking perhaps Drupal could not
// find the file because it messed up the file path. To avoid this
// confusion, therefore, we strip out the bogus fakepath string.
'%filename': this.value.replace('C:\\fakepath\\', ''),
'%extensions': extensionPattern.replace(/\|/g, ', ')
});
$(this).closest('div.form-managed-file').prepend('' + error + '
');
this.value = '';
return false;
}
}
},
/**
* Prevent file uploads when using buttons not intended to upload.
*/
disableFields: function (event){
var clickedButton = this;
// Only disable upload fields for Ajax buttons.
if (!$(clickedButton).hasClass('ajax-processed')) {
return;
}
// Check if we're working with an "Upload" button.
var $enabledFields = [];
if ($(this).closest('div.form-managed-file').length > 0) {
$enabledFields = $(this).closest('div.form-managed-file').find('input.form-file');
}
// Temporarily disable upload fields other than the one we're currently
// working with. Filter out fields that are already disabled so that they
// do not get enabled when we re-enable these fields at the end of behavior
// processing. Re-enable in a setTimeout set to a relatively short amount
// of time (1 second). All the other mousedown handlers (like Drupal's Ajax
// behaviors) are excuted before any timeout functions are called, so we
// don't have to worry about the fields being re-enabled too soon.
// @todo If the previous sentence is true, why not set the timeout to 0?
var $fieldsToTemporarilyDisable = $('div.form-managed-file input.form-file').not($enabledFields).not(':disabled');
$fieldsToTemporarilyDisable.attr('disabled', 'disabled');
setTimeout(function (){
$fieldsToTemporarilyDisable.attr('disabled', false);
}, 1000);
},
/**
* Add progress bar support if possible.
*/
progressBar: function (event) {
var clickedButton = this;
var $progressId = $(clickedButton).closest('div.form-managed-file').find('input.file-progress');
if ($progressId.length) {
var originalName = $progressId.attr('name');
// Replace the name with the required identifier.
$progressId.attr('name', originalName.match(/APC_UPLOAD_PROGRESS|UPLOAD_IDENTIFIER/)[0]);
// Restore the original name after the upload begins.
setTimeout(function () {
$progressId.attr('name', originalName);
}, 1000);
}
// Show the progress bar if the upload takes longer than half a second.
setTimeout(function () {
$(clickedButton).closest('div.form-managed-file').find('div.ajax-progress-bar').slideDown();
}, 500);
},
/**
* Open links to files within forms in a new window.
*/
openInNewWindow: function (event) {
$(this).attr('target', '_blank');
window.open(this.href, 'filePreview', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=500,height=550');
return false;
}
};
})(jQuery);
' . t('About') . '';
$output .= '' . t('The File module defines a File field type for the Field module, which lets you manage and validate uploaded files attached to content on your site (see the Field module help page for more information about fields). For more information, see the online handbook entry for File module .', array('@field-help' => url('admin/help/field'), '@file' => 'http://drupal.org/documentation/modules/file')) . '
';
$output .= '' . t('Uses') . ' ';
$output .= '';
$output .= '' . t('Attaching files to content') . ' ';
$output .= '' . t('The File module allows users to attach files to content (e.g., PDF files, spreadsheets, etc.), when a File field is added to a given content type using the Field UI module . You can add validation options to your File field, such as specifying a maximum file size and allowed file extensions.', array('@fieldui-help' => url('admin/help/field_ui'))) . ' ';
$output .= '' . t('Managing attachment display') . ' ';
$output .= '' . t('When you attach a file to content, you can specify whether it is listed or not. Listed files are displayed automatically in a section at the bottom of your content; non-listed files are available for embedding in your content, but are not included in the list at the bottom.') . ' ';
$output .= '' . t('Managing file locations') . ' ';
$output .= '' . t("When you create a File field, you can specify a directory where the files will be stored, which can be within either the public or private files directory. Files in the public directory can be accessed directly through the web server; when public files are listed, direct links to the files are used, and anyone who knows a file's URL can download the file. Files in the private directory are not accessible directly through the web server; when private files are listed, the links are Drupal path requests. This adds to server load and download time, since Drupal must start up and resolve the path for each file download request, but allows for access restrictions.") . ' ';
$output .= ' ';
return $output;
}
}
/**
* Implements hook_menu().
*/
function file_menu() {
$items = array();
$items['file/ajax'] = array(
'page callback' => 'file_ajax_upload',
'delivery callback' => 'ajax_deliver',
'access arguments' => array('access content'),
'theme callback' => 'ajax_base_page_theme',
'type' => MENU_CALLBACK,
);
$items['file/progress'] = array(
'page callback' => 'file_ajax_progress',
'access arguments' => array('access content'),
'theme callback' => 'ajax_base_page_theme',
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Implements hook_element_info().
*
* The managed file element may be used anywhere in Drupal.
*/
function file_element_info() {
$file_path = drupal_get_path('module', 'file');
$types['managed_file'] = array(
'#input' => TRUE,
'#process' => array('file_managed_file_process'),
'#value_callback' => 'file_managed_file_value',
'#element_validate' => array('file_managed_file_validate'),
'#pre_render' => array('file_managed_file_pre_render'),
'#theme' => 'file_managed_file',
'#theme_wrappers' => array('form_element'),
'#progress_indicator' => 'throbber',
'#progress_message' => NULL,
'#upload_validators' => array(),
'#upload_location' => NULL,
'#size' => 22,
'#extended' => FALSE,
'#attached' => array(
'css' => array($file_path . '/file.css'),
'js' => array($file_path . '/file.js'),
),
);
return $types;
}
/**
* Implements hook_theme().
*/
function file_theme() {
return array(
// file.module.
'file_link' => array(
'variables' => array('file' => NULL, 'icon_directory' => NULL),
),
'file_icon' => array(
'variables' => array('file' => NULL, 'icon_directory' => NULL, 'alt' => ''),
),
'file_managed_file' => array(
'render element' => 'element',
),
// file.field.inc.
'file_widget' => array(
'render element' => 'element',
),
'file_widget_multiple' => array(
'render element' => 'element',
),
'file_formatter_table' => array(
'variables' => array('items' => NULL),
),
'file_upload_help' => array(
'variables' => array('description' => NULL, 'upload_validators' => NULL),
),
);
}
/**
* Implements hook_file_download().
*
* This function takes an extra parameter $field_type so that it may
* be re-used by other File-like modules, such as Image.
*/
function file_file_download($uri, $field_type = 'file') {
global $user;
// Get the file record based on the URI. If not in the database just return.
$files = file_load_multiple(array(), array('uri' => $uri));
if (count($files)) {
foreach ($files as $item) {
// Since some database servers sometimes use a case-insensitive comparison
// by default, double check that the filename is an exact match.
if ($item->uri === $uri) {
$file = $item;
break;
}
}
}
if (!isset($file)) {
return;
}
// Find out which (if any) fields of this type contain the file.
$references = file_get_file_references($file, NULL, FIELD_LOAD_CURRENT, $field_type, FALSE);
// Stop processing if there are no references in order to avoid returning
// headers for files controlled by other modules. Make an exception for
// temporary files where the host entity has not yet been saved (for example,
// an image preview on a node/add form) in which case, allow download by the
// file's owner. For anonymous file owners, only the browser session that
// uploaded the file should be granted access.
if (empty($references) && ($file->status == FILE_STATUS_PERMANENT || $file->uid != $user->uid || (!$user->uid && empty($_SESSION['anonymous_allowed_file_ids'][$file->fid])))) {
return;
}
// Default to allow access.
$denied = FALSE;
// Loop through all references of this file. If a reference explicitly allows
// access to the field to which this file belongs, no further checks are done
// and download access is granted. If a reference denies access, eventually
// existing additional references are checked. If all references were checked
// and no reference denied access, access is granted as well. If at least one
// reference denied access, access is denied.
foreach ($references as $field_name => $field_references) {
foreach ($field_references as $entity_type => $type_references) {
foreach ($type_references as $id => $reference) {
// Try to load $entity and $field.
$entity = entity_load($entity_type, array($id));
$entity = reset($entity);
$field = field_info_field($field_name);
// Load the field item that references the file.
$field_item = NULL;
if ($entity) {
// Load all field items for that entity.
$field_items = field_get_items($entity_type, $entity, $field_name);
// Find the field item with the matching URI.
foreach ($field_items as $item) {
if ($item['uri'] == $uri) {
$field_item = $item;
break;
}
}
}
// Check that $entity, $field and $field_item were loaded successfully
// and check if access to that field is not disallowed. If any of these
// checks fail, stop checking access for this reference.
if (empty($entity) || empty($field) || empty($field_item) || !field_access('view', $field, $entity_type, $entity)) {
$denied = TRUE;
break;
}
// Invoke hook and collect grants/denies for download access.
// Default to FALSE and let entities overrule this ruling.
$grants = array('system' => FALSE);
foreach (module_implements('file_download_access') as $module) {
$grants = array_merge($grants, array($module => module_invoke($module, 'file_download_access', $field_item, $entity_type, $entity)));
}
// Allow other modules to alter the returned grants/denies.
drupal_alter('file_download_access', $grants, $field_item, $entity_type, $entity);
if (in_array(TRUE, $grants)) {
// If TRUE is returned, access is granted and no further checks are
// necessary.
$denied = FALSE;
break 3;
}
if (in_array(FALSE, $grants)) {
// If an implementation returns FALSE, access to this entity is denied
// but the file could belong to another entity to which the user might
// have access. Continue with these.
$denied = TRUE;
}
}
}
}
// Access specifically denied.
if ($denied) {
return -1;
}
// Access is granted.
$headers = file_get_content_headers($file);
return $headers;
}
/**
* Menu callback; Shared Ajax callback for file uploads and deletions.
*
* This rebuilds the form element for a particular field item. As long as the
* form processing is properly encapsulated in the widget element the form
* should rebuild correctly using FAPI without the need for additional callbacks
* or processing.
*/
function file_ajax_upload() {
$form_parents = func_get_args();
$form_build_id = (string) array_pop($form_parents);
// Sanitize form parents before using them.
$form_parents = array_filter($form_parents, 'element_child');
if (empty($_POST['form_build_id']) || $form_build_id != $_POST['form_build_id']) {
// Invalid request.
drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error');
$commands = array();
$commands[] = ajax_command_prepend(NULL, theme('status_messages'));
// Unset the problematic file from the input so that user can submit the
// form without reloading the page.
// @see https://www.drupal.org/project/drupal/issues/2749245
$field_name = (string) reset($form_parents);
$wrapper_id = drupal_html_id('edit-' . $field_name);
$commands[] = ajax_command_invoke('#' . $wrapper_id . ' .form-type-managed-file input[type="file"]', 'val', array(''));
return array('#type' => 'ajax', '#commands' => $commands);
}
list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form();
if (!$form) {
// Invalid form_build_id.
drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error');
$commands = array();
$commands[] = ajax_command_prepend(NULL, theme('status_messages'));
return array('#type' => 'ajax', '#commands' => $commands);
}
// Get the current element and count the number of files.
$current_element = $form;
foreach ($form_parents as $parent) {
$current_element = $current_element[$parent];
}
$current_file_count = isset($current_element['#file_upload_delta']) ? $current_element['#file_upload_delta'] : 0;
// Process user input. $form and $form_state are modified in the process.
drupal_process_form($form['#form_id'], $form, $form_state);
// Retrieve the element to be rendered.
foreach ($form_parents as $parent) {
$form = $form[$parent];
}
// Add the special Ajax class if a new file was added.
if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
$form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
}
// Otherwise just add the new content class on a placeholder.
else {
$form['#suffix'] = (isset($form['#suffix']) ? $form['#suffix'] : '') . ' ';
}
$form['#prefix'] = (isset($form['#prefix']) ? $form['#prefix'] : '') . theme('status_messages');
$output = drupal_render($form);
$js = drupal_add_js();
$settings = drupal_array_merge_deep_array($js['settings']['data']);
$commands[] = ajax_command_replace(NULL, $output, $settings);
return array('#type' => 'ajax', '#commands' => $commands);
}
/**
* Menu callback for upload progress.
*
* @param $key
* The unique key for this upload process.
*/
function file_ajax_progress($key) {
$progress = array(
'message' => t('Starting upload...'),
'percentage' => -1,
);
$implementation = file_progress_implementation();
if ($implementation == 'uploadprogress') {
$status = uploadprogress_get_info($key);
if (isset($status['bytes_uploaded']) && !empty($status['bytes_total'])) {
$progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['bytes_uploaded']), '@total' => format_size($status['bytes_total'])));
$progress['percentage'] = round(100 * $status['bytes_uploaded'] / $status['bytes_total']);
}
}
elseif ($implementation == 'apc') {
$status = apc_fetch('upload_' . $key);
if (isset($status['current']) && !empty($status['total'])) {
$progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['current']), '@total' => format_size($status['total'])));
$progress['percentage'] = round(100 * $status['current'] / $status['total']);
}
}
drupal_json_output($progress);
}
/**
* Determines the preferred upload progress implementation.
*
* @return
* A string indicating which upload progress system is available. Either "apc"
* or "uploadprogress". If neither are available, returns FALSE.
*/
function file_progress_implementation() {
static $implementation;
if (!isset($implementation)) {
$implementation = FALSE;
// We prefer the PECL extension uploadprogress because it supports multiple
// simultaneous uploads. APC only supports one at a time.
if (extension_loaded('uploadprogress')) {
$implementation = 'uploadprogress';
}
elseif (extension_loaded('apc') && ini_get('apc.rfc1867')) {
$implementation = 'apc';
}
}
return $implementation;
}
/**
* Implements hook_file_delete().
*/
function file_file_delete($file) {
// TODO: Remove references to a file that is in-use.
}
/**
* Process function to expand the managed_file element type.
*
* Expands the file type to include Upload and Remove buttons, as well as
* support for a default value.
*/
function file_managed_file_process($element, &$form_state, $form) {
$fid = isset($element['#value']['fid']) ? $element['#value']['fid'] : 0;
// Set some default element properties.
$element['#progress_indicator'] = empty($element['#progress_indicator']) ? 'none' : $element['#progress_indicator'];
$element['#file'] = $fid ? file_load($fid) : FALSE;
$element['#tree'] = TRUE;
$ajax_settings = array(
'path' => 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'],
'wrapper' => $element['#id'] . '-ajax-wrapper',
'effect' => 'fade',
'progress' => array(
'type' => $element['#progress_indicator'],
'message' => $element['#progress_message'],
),
);
// Set up the buttons first since we need to check if they were clicked.
$element['upload_button'] = array(
'#name' => implode('_', $element['#parents']) . '_upload_button',
'#type' => 'submit',
'#value' => t('Upload'),
'#validate' => array(),
'#submit' => array('file_managed_file_submit'),
'#limit_validation_errors' => array($element['#parents']),
'#ajax' => $ajax_settings,
'#weight' => -5,
);
// Force the progress indicator for the remove button to be either 'none' or
// 'throbber', even if the upload button is using something else.
$ajax_settings['progress']['type'] = ($element['#progress_indicator'] == 'none') ? 'none' : 'throbber';
$ajax_settings['progress']['message'] = NULL;
$ajax_settings['effect'] = 'none';
$element['remove_button'] = array(
'#name' => implode('_', $element['#parents']) . '_remove_button',
'#type' => 'submit',
'#value' => t('Remove'),
'#validate' => array(),
'#submit' => array('file_managed_file_submit'),
'#limit_validation_errors' => array($element['#parents']),
'#ajax' => $ajax_settings,
'#weight' => -5,
);
$element['fid'] = array(
'#type' => 'hidden',
'#value' => $fid,
);
// Add progress bar support to the upload if possible.
if ($element['#progress_indicator'] == 'bar' && $implementation = file_progress_implementation()) {
$upload_progress_key = mt_rand();
if ($implementation == 'uploadprogress') {
$element['UPLOAD_IDENTIFIER'] = array(
'#type' => 'hidden',
'#value' => $upload_progress_key,
'#attributes' => array('class' => array('file-progress')),
// Uploadprogress extension requires this field to be at the top of the
// form.
'#weight' => -20,
);
}
elseif ($implementation == 'apc') {
$element['APC_UPLOAD_PROGRESS'] = array(
'#type' => 'hidden',
'#value' => $upload_progress_key,
'#attributes' => array('class' => array('file-progress')),
// Uploadprogress extension requires this field to be at the top of the
// form.
'#weight' => -20,
);
}
// Add the upload progress callback.
$element['upload_button']['#ajax']['progress']['path'] = 'file/progress/' . $upload_progress_key;
}
// The file upload field itself.
$element['upload'] = array(
'#name' => 'files[' . implode('_', $element['#parents']) . ']',
'#type' => 'file',
// This #title will not actually be used as the upload field's HTML label,
// since the theme function for upload fields never passes the element
// through theme('form_element'). Instead the parent element's #title is
// used as the label (see below). That is usually a more meaningful label
// anyway.
'#title' => t('Choose a file'),
'#title_display' => 'invisible',
// Set the ID manually so the desired field label can be associated with it
// below. Use the same method for setting the ID that the form API
// autogenerator does.
'#id' => drupal_html_id('edit-' . implode('-', array_merge($element['#parents'], array('upload')))),
'#size' => $element['#size'],
'#theme_wrappers' => array(),
'#weight' => -10,
);
// Indicate that $element['#title'] should be used as the HTML label for the
// file upload field.
$element['#label_for'] = $element['upload']['#id'];
if ($fid && $element['#file']) {
$element['filename'] = array(
'#type' => 'markup',
'#markup' => theme('file_link', array('file' => $element['#file'])) . ' ',
'#weight' => -10,
);
// Anonymous users who have uploaded a temporary file need a
// non-session-based token added so file_managed_file_value() can check
// that they have permission to use this file on subsequent submissions of
// the same form (for example, after an Ajax upload or form validation
// error).
if (!$GLOBALS['user']->uid && $element['#file']->status != FILE_STATUS_PERMANENT) {
$element['fid_token'] = array(
'#type' => 'hidden',
'#value' => drupal_hmac_base64('file-' . $fid, drupal_get_private_key() . drupal_get_hash_salt()),
);
}
}
// Add the extension list to the page as JavaScript settings.
if (isset($element['#upload_validators']['file_validate_extensions'][0])) {
$extension_list = implode(',', array_filter(explode(' ', $element['#upload_validators']['file_validate_extensions'][0])));
$element['upload']['#attached']['js'] = array(
array(
'type' => 'setting',
'data' => array('file' => array('elements' => array('#' . $element['upload']['#id'] => $extension_list)))
)
);
}
// Prefix and suffix used for Ajax replacement.
$element['#prefix'] = '';
$element['#suffix'] = '
';
return $element;
}
/**
* The #value_callback for a managed_file type element.
*/
function file_managed_file_value(&$element, $input = FALSE, $form_state = NULL) {
$fid = 0;
$force_default = FALSE;
// Find the current value of this field from the form state.
$form_state_fid = $form_state['values'];
foreach ($element['#parents'] as $parent) {
$form_state_fid = isset($form_state_fid[$parent]) ? $form_state_fid[$parent] : 0;
}
if ($element['#extended'] && isset($form_state_fid['fid'])) {
$fid = $form_state_fid['fid'];
}
elseif (is_numeric($form_state_fid)) {
$fid = $form_state_fid;
}
// Process any input and save new uploads.
if ($input !== FALSE) {
$return = $input;
// Uploads take priority over all other values.
if ($file = file_managed_file_save_upload($element)) {
$fid = $file->fid;
}
else {
// Check for #filefield_value_callback values.
// Because FAPI does not allow multiple #value_callback values like it
// does for #element_validate and #process, this fills the missing
// functionality to allow File fields to be extended through FAPI.
if (isset($element['#file_value_callbacks'])) {
foreach ($element['#file_value_callbacks'] as $callback) {
$callback($element, $input, $form_state);
}
}
// If a FID was submitted, load the file (and check access if it's not a
// public file) to confirm it exists and that the current user has access
// to it.
if (isset($input['fid']) && ($file = file_load($input['fid']))) {
// By default the public:// file scheme provided by Drupal core is the
// only one that allows files to be publicly accessible to everyone, so
// it is the only one for which the file access checks are bypassed.
// Other modules which provide publicly accessible streams of their own
// in hook_stream_wrappers() can add the corresponding scheme to the
// 'file_public_schema' variable to bypass file access checks for those
// as well. This should only be done for schemes that are completely
// publicly accessible, with no download restrictions; for security
// reasons all other schemes must go through the file_download_access()
// check.
if (!in_array(file_uri_scheme($file->uri), variable_get('file_public_schema', array('public'))) && !file_download_access($file->uri)) {
$force_default = TRUE;
}
// Temporary files that belong to other users should never be allowed.
elseif ($file->status != FILE_STATUS_PERMANENT) {
if ($GLOBALS['user']->uid && $file->uid != $GLOBALS['user']->uid) {
$force_default = TRUE;
}
// Since file ownership can't be determined for anonymous users, they
// are not allowed to reuse temporary files at all. But they do need
// to be able to reuse their own files from earlier submissions of
// the same form, so to allow that, check for the token added by
// file_managed_file_process().
elseif (!$GLOBALS['user']->uid) {
$token = drupal_array_get_nested_value($form_state['input'], array_merge($element['#parents'], array('fid_token')));
if ($token !== drupal_hmac_base64('file-' . $file->fid, drupal_get_private_key() . drupal_get_hash_salt())) {
$force_default = TRUE;
}
}
}
// If all checks pass, allow the file to be changed.
if (!$force_default) {
$fid = $file->fid;
}
}
}
}
// If there is no input or if the default value was requested above, use the
// default value.
if ($input === FALSE || $force_default) {
if ($element['#extended']) {
$default_fid = isset($element['#default_value']['fid']) ? $element['#default_value']['fid'] : 0;
$return = isset($element['#default_value']) ? $element['#default_value'] : array('fid' => 0);
}
else {
$default_fid = isset($element['#default_value']) ? $element['#default_value'] : 0;
$return = array('fid' => 0);
}
// Confirm that the file exists when used as a default value.
if ($default_fid && $file = file_load($default_fid)) {
$fid = $file->fid;
}
}
$return['fid'] = $fid;
return $return;
}
/**
* An #element_validate callback for the managed_file element.
*/
function file_managed_file_validate(&$element, &$form_state) {
// If referencing an existing file, only allow if there are existing
// references. This prevents unmanaged files from being deleted if this
// item were to be deleted.
$clicked_button = end($form_state['triggering_element']['#parents']);
if ($clicked_button != 'remove_button' && !empty($element['fid']['#value'])) {
if ($file = file_load($element['fid']['#value'])) {
if ($file->status == FILE_STATUS_PERMANENT) {
$references = file_usage_list($file);
if (empty($references)) {
form_error($element, t('The file used in the !name field may not be referenced.', array('!name' => $element['#title'])));
}
}
}
else {
form_error($element, t('The file referenced by the !name field does not exist.', array('!name' => $element['#title'])));
}
}
// Check required property based on the FID.
if ($element['#required'] && empty($element['fid']['#value']) && !in_array($clicked_button, array('upload_button', 'remove_button'))) {
form_error($element['upload'], t('!name field is required.', array('!name' => $element['#title'])));
}
// Consolidate the array value of this field to a single FID.
if (!$element['#extended']) {
form_set_value($element, $element['fid']['#value'], $form_state);
}
}
/**
* Form submission handler for upload / remove buttons of managed_file elements.
*
* @see file_managed_file_process()
*/
function file_managed_file_submit($form, &$form_state) {
// Determine whether it was the upload or the remove button that was clicked,
// and set $element to the managed_file element that contains that button.
$parents = $form_state['triggering_element']['#array_parents'];
$button_key = array_pop($parents);
$element = drupal_array_get_nested_value($form, $parents);
// No action is needed here for the upload button, because all file uploads on
// the form are processed by file_managed_file_value() regardless of which
// button was clicked. Action is needed here for the remove button, because we
// only remove a file in response to its remove button being clicked.
if ($button_key == 'remove_button') {
// If it's a temporary file we can safely remove it immediately, otherwise
// it's up to the implementing module to clean up files that are in use.
if ($element['#file'] && $element['#file']->status == 0) {
file_delete($element['#file']);
}
// Update both $form_state['values'] and $form_state['input'] to reflect
// that the file has been removed, so that the form is rebuilt correctly.
// $form_state['values'] must be updated in case additional submit handlers
// run, and for form building functions that run during the rebuild, such as
// when the managed_file element is part of a field widget.
// $form_state['input'] must be updated so that file_managed_file_value()
// has correct information during the rebuild.
$values_element = $element['#extended'] ? $element['fid'] : $element;
form_set_value($values_element, NULL, $form_state);
drupal_array_set_nested_value($form_state['input'], $values_element['#parents'], NULL);
}
// Set the form to rebuild so that $form is correctly updated in response to
// processing the file removal. Since this function did not change $form_state
// if the upload button was clicked, a rebuild isn't necessary in that
// situation and setting $form_state['redirect'] to FALSE would suffice.
// However, we choose to always rebuild, to keep the form processing workflow
// consistent between the two buttons.
$form_state['rebuild'] = TRUE;
}
/**
* Saves any files that have been uploaded into a managed_file element.
*
* @param $element
* The FAPI element whose values are being saved.
*
* @return
* The file object representing the file that was saved, or FALSE if no file
* was saved.
*/
function file_managed_file_save_upload($element) {
$upload_name = implode('_', $element['#parents']);
if (empty($_FILES['files']['name'][$upload_name])) {
return FALSE;
}
$destination = isset($element['#upload_location']) ? $element['#upload_location'] : NULL;
if (isset($destination) && !file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) {
watchdog('file', 'The upload directory %directory for the file field !name could not be created or is not accessible. A newly uploaded file could not be saved in this directory as a consequence, and the upload was canceled.', array('%directory' => $destination, '!name' => $element['#field_name']));
form_set_error($upload_name, t('The file could not be uploaded.'));
return FALSE;
}
if (!$file = file_save_upload($upload_name, $element['#upload_validators'], $destination)) {
watchdog('file', 'The file upload failed. %upload', array('%upload' => $upload_name));
form_set_error($upload_name, t('The file in the !name field was unable to be uploaded.', array('!name' => $element['#title'])));
return FALSE;
}
return $file;
}
/**
* Returns HTML for a managed file element.
*
* @param $variables
* An associative array containing:
* - element: A render element representing the file.
*
* @ingroup themeable
*/
function theme_file_managed_file($variables) {
$element = $variables['element'];
$attributes = array();
if (isset($element['#id'])) {
$attributes['id'] = $element['#id'];
}
if (!empty($element['#attributes']['class'])) {
$attributes['class'] = (array) $element['#attributes']['class'];
}
$attributes['class'][] = 'form-managed-file';
// This wrapper is required to apply JS behaviors and CSS styling.
$output = '';
$output .= '';
$output .= drupal_render_children($element);
$output .= '
';
return $output;
}
/**
* #pre_render callback to hide display of the upload or remove controls.
*
* Upload controls are hidden when a file is already uploaded. Remove controls
* are hidden when there is no file attached. Controls are hidden here instead
* of in file_managed_file_process(), because #access for these buttons depends
* on the managed_file element's #value. See the documentation of form_builder()
* for more detailed information about the relationship between #process,
* #value, and #access.
*
* Because #access is set here, it affects display only and does not prevent
* JavaScript or other untrusted code from submitting the form as though access
* were enabled. The form processing functions for these elements should not
* assume that the buttons can't be "clicked" just because they are not
* displayed.
*
* @see file_managed_file_process()
* @see form_builder()
*/
function file_managed_file_pre_render($element) {
// If we already have a file, we don't want to show the upload controls.
if (!empty($element['#value']['fid'])) {
$element['upload']['#access'] = FALSE;
$element['upload_button']['#access'] = FALSE;
}
// If we don't already have a file, there is nothing to remove.
else {
$element['remove_button']['#access'] = FALSE;
}
return $element;
}
/**
* Returns HTML for a link to a file.
*
* @param $variables
* An associative array containing:
* - file: A file object to which the link will be created.
* - icon_directory: (optional) A path to a directory of icons to be used for
* files. Defaults to the value of the "file_icon_directory" variable.
*
* @ingroup themeable
*/
function theme_file_link($variables) {
$file = $variables['file'];
$icon_directory = $variables['icon_directory'];
$url = file_create_url($file->uri);
// Human-readable names, for use as text-alternatives to icons.
$mime_name = array(
'application/msword' => t('Microsoft Office document icon'),
'application/vnd.ms-excel' => t('Office spreadsheet icon'),
'application/vnd.ms-powerpoint' => t('Office presentation icon'),
'application/pdf' => t('PDF icon'),
'video/quicktime' => t('Movie icon'),
'audio/mpeg' => t('Audio icon'),
'audio/wav' => t('Audio icon'),
'image/jpeg' => t('Image icon'),
'image/png' => t('Image icon'),
'image/gif' => t('Image icon'),
'application/zip' => t('Package icon'),
'text/html' => t('HTML icon'),
'text/plain' => t('Plain text icon'),
'application/octet-stream' => t('Binary Data'),
);
$mimetype = file_get_mimetype($file->uri);
$icon = theme('file_icon', array(
'file' => $file,
'icon_directory' => $icon_directory,
'alt' => !empty($mime_name[$mimetype]) ? $mime_name[$mimetype] : t('File'),
));
// Set options as per anchor format described at
// http://microformats.org/wiki/file-format-examples
$options = array(
'attributes' => array(
'type' => $file->filemime . '; length=' . $file->filesize,
),
);
// Use the description as the link text if available.
if (empty($file->description)) {
$link_text = $file->filename;
}
else {
$link_text = $file->description;
$options['attributes']['title'] = check_plain($file->filename);
}
return '' . $icon . ' ' . l($link_text, $url, $options) . ' ';
}
/**
* Returns HTML for an image with an appropriate icon for the given file.
*
* @param $variables
* An associative array containing:
* - file: A file object for which to make an icon.
* - icon_directory: (optional) A path to a directory of icons to be used for
* files. Defaults to the value of the "file_icon_directory" variable.
* - alt: (optional) The alternative text to represent the icon in text-based
* browsers. Defaults to an empty string.
*
* @ingroup themeable
*/
function theme_file_icon($variables) {
$file = $variables['file'];
$alt = $variables['alt'];
$icon_directory = $variables['icon_directory'];
$mime = check_plain($file->filemime);
$icon_url = file_icon_url($file, $icon_directory);
return ' ';
}
/**
* Creates a URL to the icon for a file object.
*
* @param $file
* A file object.
* @param $icon_directory
* (optional) A path to a directory of icons to be used for files. Defaults to
* the value of the "file_icon_directory" variable.
*
* @return
* A URL string to the icon, or FALSE if an appropriate icon cannot be found.
*/
function file_icon_url($file, $icon_directory = NULL) {
if ($icon_path = file_icon_path($file, $icon_directory)) {
return base_path() . $icon_path;
}
return FALSE;
}
/**
* Creates a path to the icon for a file object.
*
* @param $file
* A file object.
* @param $icon_directory
* (optional) A path to a directory of icons to be used for files. Defaults to
* the value of the "file_icon_directory" variable.
*
* @return
* A string to the icon as a local path, or FALSE if an appropriate icon could
* not be found.
*/
function file_icon_path($file, $icon_directory = NULL) {
// Use the default set of icons if none specified.
if (!isset($icon_directory)) {
$icon_directory = variable_get('file_icon_directory', drupal_get_path('module', 'file') . '/icons');
}
// If there's an icon matching the exact mimetype, go for it.
$dashed_mime = strtr($file->filemime, array('/' => '-'));
$icon_path = $icon_directory . '/' . $dashed_mime . '.png';
if (file_exists($icon_path)) {
return $icon_path;
}
// For a few mimetypes, we can "manually" map to a generic icon.
$generic_mime = (string) file_icon_map($file);
$icon_path = $icon_directory . '/' . $generic_mime . '.png';
if ($generic_mime && file_exists($icon_path)) {
return $icon_path;
}
// Use generic icons for each category that provides such icons.
foreach (array('audio', 'image', 'text', 'video') as $category) {
if (strpos($file->filemime, $category . '/') === 0) {
$icon_path = $icon_directory . '/' . $category . '-x-generic.png';
if (file_exists($icon_path)) {
return $icon_path;
}
}
}
// Try application-octet-stream as last fallback.
$icon_path = $icon_directory . '/application-octet-stream.png';
if (file_exists($icon_path)) {
return $icon_path;
}
// No icon can be found.
return FALSE;
}
/**
* Determines the generic icon MIME package based on a file's MIME type.
*
* @param $file
* A file object.
*
* @return
* The generic icon MIME package expected for this file.
*/
function file_icon_map($file) {
switch ($file->filemime) {
// Word document types.
case 'application/msword':
case 'application/vnd.ms-word.document.macroEnabled.12':
case 'application/vnd.oasis.opendocument.text':
case 'application/vnd.oasis.opendocument.text-template':
case 'application/vnd.oasis.opendocument.text-master':
case 'application/vnd.oasis.opendocument.text-web':
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
case 'application/vnd.stardivision.writer':
case 'application/vnd.sun.xml.writer':
case 'application/vnd.sun.xml.writer.template':
case 'application/vnd.sun.xml.writer.global':
case 'application/vnd.wordperfect':
case 'application/x-abiword':
case 'application/x-applix-word':
case 'application/x-kword':
case 'application/x-kword-crypt':
return 'x-office-document';
// Spreadsheet document types.
case 'application/vnd.ms-excel':
case 'application/vnd.ms-excel.sheet.macroEnabled.12':
case 'application/vnd.oasis.opendocument.spreadsheet':
case 'application/vnd.oasis.opendocument.spreadsheet-template':
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
case 'application/vnd.stardivision.calc':
case 'application/vnd.sun.xml.calc':
case 'application/vnd.sun.xml.calc.template':
case 'application/vnd.lotus-1-2-3':
case 'application/x-applix-spreadsheet':
case 'application/x-gnumeric':
case 'application/x-kspread':
case 'application/x-kspread-crypt':
return 'x-office-spreadsheet';
// Presentation document types.
case 'application/vnd.ms-powerpoint':
case 'application/vnd.ms-powerpoint.presentation.macroEnabled.12':
case 'application/vnd.oasis.opendocument.presentation':
case 'application/vnd.oasis.opendocument.presentation-template':
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
case 'application/vnd.stardivision.impress':
case 'application/vnd.sun.xml.impress':
case 'application/vnd.sun.xml.impress.template':
case 'application/x-kpresenter':
return 'x-office-presentation';
// Compressed archive types.
case 'application/zip':
case 'application/x-zip':
case 'application/stuffit':
case 'application/x-stuffit':
case 'application/x-7z-compressed':
case 'application/x-ace':
case 'application/x-arj':
case 'application/x-bzip':
case 'application/x-bzip-compressed-tar':
case 'application/x-compress':
case 'application/x-compressed-tar':
case 'application/x-cpio-compressed':
case 'application/x-deb':
case 'application/x-gzip':
case 'application/x-java-archive':
case 'application/x-lha':
case 'application/x-lhz':
case 'application/x-lzop':
case 'application/x-rar':
case 'application/x-rpm':
case 'application/x-tzo':
case 'application/x-tar':
case 'application/x-tarz':
case 'application/x-tgz':
return 'package-x-generic';
// Script file types.
case 'application/ecmascript':
case 'application/javascript':
case 'application/mathematica':
case 'application/vnd.mozilla.xul+xml':
case 'application/x-asp':
case 'application/x-awk':
case 'application/x-cgi':
case 'application/x-csh':
case 'application/x-m4':
case 'application/x-perl':
case 'application/x-php':
case 'application/x-ruby':
case 'application/x-shellscript':
case 'text/vnd.wap.wmlscript':
case 'text/x-emacs-lisp':
case 'text/x-haskell':
case 'text/x-literate-haskell':
case 'text/x-lua':
case 'text/x-makefile':
case 'text/x-matlab':
case 'text/x-python':
case 'text/x-sql':
case 'text/x-tcl':
return 'text-x-script';
// HTML aliases.
case 'application/xhtml+xml':
return 'text-html';
// Executable types.
case 'application/x-macbinary':
case 'application/x-ms-dos-executable':
case 'application/x-pef-executable':
return 'application-x-executable';
default:
return FALSE;
}
}
/**
* @defgroup file-module-api File module public API functions
* @{
* These functions may be used to determine if and where a file is in use.
*/
/**
* Retrieves a list of references to a file.
*
* @param $file
* A file object.
* @param $field
* (optional) A field array to be used for this check. If given, limits the
* reference check to the given field.
* @param $age
* (optional) A constant that specifies which references to count. Use
* FIELD_LOAD_REVISION to retrieve all references within all revisions or
* FIELD_LOAD_CURRENT to retrieve references only in the current revisions.
* @param $field_type
* (optional) The name of a field type. If given, limits the reference check
* to fields of the given type.
* @param $check_access
* (optional) A boolean that specifies whether the permissions of the current
* user should be checked when retrieving references. If FALSE, all
* references to the file are returned. If TRUE, only references from
* entities that the current user has access to are returned. Defaults to
* TRUE for backwards compatibility reasons, but FALSE is recommended for
* most situations.
*
* @return
* An integer value.
*/
function file_get_file_references($file, $field = NULL, $age = FIELD_LOAD_REVISION, $field_type = 'file', $check_access = TRUE) {
$references = drupal_static(__FUNCTION__, array());
$fields = isset($field) ? array($field['field_name'] => $field) : field_info_fields();
foreach ($fields as $field_name => $file_field) {
if ((empty($field_type) || $file_field['type'] == $field_type) && !isset($references[$field_name])) {
// Get each time this file is used within a field.
$query = new EntityFieldQuery();
$query
->fieldCondition($file_field, 'fid', $file->fid)
->age($age);
if (!$check_access) {
// Neutralize the 'entity_field_access' query tag added by
// field_sql_storage_field_storage_query().
$query->addTag('DANGEROUS_ACCESS_CHECK_OPT_OUT');
}
$references[$field_name] = $query->execute();
}
}
return isset($field) ? $references[$field['field_name']] : array_filter($references);
}
/**
* @} End of "defgroup file-module-api".
*/
‰PNG
IHDR µú7ê tEXtSoftware Adobe ImageReadyqÉe<