diff --git a/.htaccess b/.htaccess index 9494b53..ac36f2d 100644 --- a/.htaccess +++ b/.htaccess @@ -103,6 +103,13 @@ DirectoryIndex index.php index.html index.htm # uncomment the following line: # RewriteBase / + # Redirect common PHP files to their new locations. + RewriteCond %{REQUEST_URI} ^(.*)?/(update.php) [OR] + RewriteCond %{REQUEST_URI} ^(.*)?/(install.php) [OR] + RewriteCond %{REQUEST_URI} ^(.*)?/(cron.php) + RewriteCond %{REQUEST_URI} !core + RewriteRule ^ %1/core/%2 [L,QSA,R=301] + # 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 diff --git a/INSTALL.txt b/INSTALL.txt deleted file mode 100644 index 8df8075..0000000 --- a/INSTALL.txt +++ /dev/null @@ -1,398 +0,0 @@ - -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.2 (or greater) (http://www.php.net/). -- One of the following databases: - - MySQL 5.0.15 (or greater) (http://www.mysql.com/). - - MariaDB 5.1.44 (or greater) (http://mariadb.org/). MariaDB is a fully - compatible drop-in replacement for MySQL. - - PostgreSQL 8.3 (or greater) (http://www.postgresql.org/). - - SQLite 3.4.2 (or greater) (http://www.sqlite.org/). - -For more detailed information about Drupal requirements, including a list of -PHP extensions and configurations that are required, see "System requirements" -(http://drupal.org/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 install 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. diff --git a/README.txt b/README.txt index 921f99c..904084a 100644 --- a/README.txt +++ b/README.txt @@ -17,7 +17,7 @@ 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. + See LICENSE.txt in the "core" directory. * Learn about the Drupal trademark and logo policy: http://drupal.com/trademark @@ -35,7 +35,7 @@ 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. + See INSTALL.txt and UPGRADE.txt in the "core" directory. * 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 diff --git a/UPGRADE.txt b/UPGRADE.txt deleted file mode 100644 index edef885..0000000 --- a/UPGRADE.txt +++ /dev/null @@ -1,223 +0,0 @@ - -INTRODUCTION ------------- -This document describes how to: - - * Update your Drupal site from one minor 8.x version to another minor 8.x - version; for example, from 8.8 to 8.9, or from 8.6 to 8.10. - - * Upgrade your Drupal site's major version from 7.x to 8.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. - - -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 8.x version of Drupal to any later 8.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 settings.php (this will be noted in - the release announcement). If that's the case, replace your old settings.php - with the new one, and copy the site-specific entries (especially the lines - giving the database name, user, and password) from the old settings.php to - the new settings.php. - -4. Download the latest Drupal 8.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 8.x, after -following the instructions in the INTRODUCTION section at the top of this file: - -1. Check on the Drupal 8 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 8, 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 8 - -2. Update to the latest available version of Drupal 7.x (if your current version - is Drupal 6.x, you have to upgrade to 7.x first). If you need to update, - download Drupal 7.x and follow the instructions in its UPGRADE.txt. This - document only applies for upgrades from 7.x to 8.x. - -3. Log in as user ID 1 (the site maintenance user). - -4. Go to Administer > Site configuration > Site maintenance. Select - "Off-line" and save the configuration. - -5. Go to Administer > Site building > Themes. Enable "Bartik" and select it as - the default theme. - -6. 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 8.x and you - no longer need their data, then you can uninstall them under the Uninstall - tab after disabling them. - -7. On the command line or in your FTP client, remove the file - - sites/default/default.settings.php - -8. 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. - -9. 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 8.x. - -10. Download the latest Drupal 8.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. - -11. Re-apply any modifications to files such as .htaccess or robots.txt. - -12. Make your settings.php file writeable, so that the update process can - convert it to the format of Drupal 8.x. settings.php is usually located in - - sites/default/settings.php - -13. 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. - -14. Backup your database after the core upgrade has run. - -15. Replace and update your non-core modules and themes, following the - procedures at http://drupal.org/node/948216 - -16. Go to Administration > Reports > Status report. Verify that everything is - working as expected. - -17. Ensure that $update_free_access is FALSE in settings.php. - -18. 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 diff --git a/authorize.php b/authorize.php deleted file mode 100644 index 97cdd70..0000000 --- a/authorize.php +++ /dev/null @@ -1,175 +0,0 @@ - $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)); -} - diff --git a/CHANGELOG.txt b/core/CHANGELOG.txt similarity index 100% rename from CHANGELOG.txt rename to core/CHANGELOG.txt diff --git a/COPYRIGHT.txt b/core/COPYRIGHT.txt similarity index 100% rename from COPYRIGHT.txt rename to core/COPYRIGHT.txt diff --git a/INSTALL.mysql.txt b/core/INSTALL.mysql.txt similarity index 100% rename from INSTALL.mysql.txt rename to core/INSTALL.mysql.txt diff --git a/INSTALL.pgsql.txt b/core/INSTALL.pgsql.txt similarity index 100% rename from INSTALL.pgsql.txt rename to core/INSTALL.pgsql.txt diff --git a/INSTALL.sqlite.txt b/core/INSTALL.sqlite.txt similarity index 100% rename from INSTALL.sqlite.txt rename to core/INSTALL.sqlite.txt diff --git a/core/INSTALL.txt b/core/INSTALL.txt new file mode 100644 index 0000000..245e934 --- /dev/null +++ b/core/INSTALL.txt @@ -0,0 +1,398 @@ + +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.2 (or greater) (http://www.php.net/). +- One of the following databases: + - MySQL 5.0.15 (or greater) (http://www.mysql.com/). + - MariaDB 5.1.44 (or greater) (http://mariadb.org/). MariaDB is a fully + compatible drop-in replacement for MySQL. + - PostgreSQL 8.3 (or greater) (http://www.postgresql.org/). + - SQLite 3.4.2 (or greater) (http://www.sqlite.org/). + +For more detailed information about Drupal requirements, including a list of +PHP extensions and configurations that are required, see "System requirements" +(http://drupal.org/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 install 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/core/cron.php?cron_key=YOURKEY + + Replace the text "http://example.com/core/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. diff --git a/LICENSE.txt b/core/LICENSE.txt similarity index 100% rename from LICENSE.txt rename to core/LICENSE.txt diff --git a/MAINTAINERS.txt b/core/MAINTAINERS.txt similarity index 100% rename from MAINTAINERS.txt rename to core/MAINTAINERS.txt diff --git a/core/UPGRADE.txt b/core/UPGRADE.txt new file mode 100644 index 0000000..ba93d42 --- /dev/null +++ b/core/UPGRADE.txt @@ -0,0 +1,223 @@ + +INTRODUCTION +------------ +This document describes how to: + + * Update your Drupal site from one minor 8.x version to another minor 8.x + version; for example, from 8.8 to 8.9, or from 8.6 to 8.10. + + * Upgrade your Drupal site's major version from 7.x to 8.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. + + +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 8.x version of Drupal to any later 8.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 settings.php (this will be noted in + the release announcement). If that's the case, replace your old settings.php + with the new one, and copy the site-specific entries (especially the lines + giving the database name, user, and password) from the old settings.php to + the new settings.php. + +4. Download the latest Drupal 8.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/core/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 8.x, after +following the instructions in the INTRODUCTION section at the top of this file: + +1. Check on the Drupal 8 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 8, 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 8 + +2. Update to the latest available version of Drupal 7.x (if your current version + is Drupal 6.x, you have to upgrade to 7.x first). If you need to update, + download Drupal 7.x and follow the instructions in its UPGRADE.txt. This + document only applies for upgrades from 7.x to 8.x. + +3. Log in as user ID 1 (the site maintenance user). + +4. Go to Administer > Site configuration > Site maintenance. Select + "Off-line" and save the configuration. + +5. Go to Administer > Site building > Themes. Enable "Bartik" and select it as + the default theme. + +6. 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 8.x and you + no longer need their data, then you can uninstall them under the Uninstall + tab after disabling them. + +7. On the command line or in your FTP client, remove the file + + sites/default/default.settings.php + +8. 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. + +9. 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 8.x. + +10. Download the latest Drupal 8.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. + +11. Re-apply any modifications to files such as .htaccess or robots.txt. + +12. Make your settings.php file writeable, so that the update process can + convert it to the format of Drupal 8.x. settings.php is usually located in + + sites/default/settings.php + +13. Run update.php by visiting http://www.example.com/core/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. + +14. Backup your database after the core upgrade has run. + +15. Replace and update your non-core modules and themes, following the + procedures at http://drupal.org/node/948216 + +16. Go to Administration > Reports > Status report. Verify that everything is + working as expected. + +17. Ensure that $update_free_access is FALSE in settings.php. + +18. 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 diff --git a/core/authorize.php b/core/authorize.php new file mode 100644 index 0000000..d0a9d63 --- /dev/null +++ b/core/authorize.php @@ -0,0 +1,178 @@ + $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)); +} + diff --git a/core/cron.php b/core/cron.php new file mode 100644 index 0000000..65cded1 --- /dev/null +++ b/core/cron.php @@ -0,0 +1,29 @@ +direction. + */ +define('LANGUAGE_LTR', 0); + +/** + * Language written right to left. Possible value of $language->direction. + */ +define('LANGUAGE_RTL', 1); + +/** + * For convenience, define a short form of the request time global. + */ +define('REQUEST_TIME', $_SERVER['REQUEST_TIME']); + +/** + * Flag for drupal_set_title(); text is not sanitized, so run check_plain(). + */ +define('CHECK_PLAIN', 0); + +/** + * Flag for drupal_set_title(); text has already been sanitized. + */ +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/en/language.functions.php + */ +define('DRUPAL_PHP_FUNCTION_PATTERN', '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'); + +/** + * Start 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; +} + +/** + * Read 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']; +} + +/** + * Stop 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]; +} + +/** + * Find the appropriate configuration directory. + * + * Try finding a matching configuration directory by stripping the website's + * hostname from left to right and pathname from right to left. The first + * configuration file found will be used; the remaining will ignored. If no + * configuration file is found, return a default value '$confdir/default'. + * + * Example for a fictitious site installed at + * http://www.drupal.org:8080/mysite/test/ the 'settings.php' is searched in + * the following directories: + * + * 1. $confdir/8080.www.drupal.org.mysite.test + * 2. $confdir/www.drupal.org.mysite.test + * 3. $confdir/drupal.org.mysite.test + * 4. $confdir/org.mysite.test + * + * 5. $confdir/8080.www.drupal.org.mysite + * 6. $confdir/www.drupal.org.mysite + * 7. $confdir/drupal.org.mysite + * 8. $confdir/org.mysite + * + * 9. $confdir/8080.www.drupal.org + * 10. $confdir/www.drupal.org + * 11. $confdir/drupal.org + * 12. $confdir/org + * + * 13. $confdir/default + * + * If a file named sites.php is present in the $confdir, it will be loaded + * prior to scanning for directories. It should define an associative array + * named $sites, which maps domains to directories. It should be in the form + * of: + * + * $sites = array( + * 'The url to alias' => 'A directory within the sites directory' + * ); + * + * For example: + * + * $sites = array( + * 'devexample.com' => 'example.com', + * 'localhost.example' => 'example.com', + * ); + * + * The above array will cause Drupal to look for a directory named + * "example.com" in the sites directory whenever a request comes from + * "example.com", "devexample.com", or "localhost/example". That is useful + * on development servers, where the domain name may not be the same as the + * domain of the live server. Since Drupal stores file paths into the database + * (files, system table, etc.) this will ensure the paths are correct while + * accessed on development servers. + * + * @param $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 reset + * Force a full search for matching directories even if one had been + * found previously. + * @return + * The path of the matching directory. + */ +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; +} + +/** + * Set 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; +} + +/** + * Initialize 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. + ini_set('session.cache_limiter', 'none'); + // 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'); +} + +/** + * Validate 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) { + return preg_match('/^\[?(?:[a-zA-Z0-9-:\]_]+\.?)+$/', $host); +} + +/** + * Loads the configuration and sets the base URL, cookie domain, and + * session name correctly. + */ +function drupal_settings_initialize() { + global $base_url, $base_path, $base_root; + + // Export the following 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 = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on'; + + if (isset($base_url)) { + // Parse fixed base URL from settings.php. + $parts = parse_url($base_url); + $http_protocol = $parts['scheme']; + 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']), '\/')) { + // Remove "core" directory if present, allowing install.php, update.php, + // cron.php and others to auto-detect a base path. + $core_position = strrpos($dir, '/core'); + if ($core_position !== FALSE && strlen($dir) - 5 == $core_position) { + $base_path = substr($dir, 0, $core_position); + } + else { + $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 = $_SERVER['HTTP_HOST']; + // Strip leading periods, www., and port numbers from cookie domain. + $cookie_domain = ltrim($cookie_domain, '.'); + if (strpos($cookie_domain, 'www.') === 0) { + $cookie_domain = substr($cookie_domain, 4); + } + $cookie_domain = explode(':', $cookie_domain); + $cookie_domain = '.' . $cookie_domain[0]; + } + } + // 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)); +} + +/** + * Returns and optionally sets the filename for a system item (module, + * theme, etc.). 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 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 (i.e. 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. + * + * @return + * The filename of the requested item. + */ +function drupal_get_filename($type, $name, $filename = NULL) { + // The location of files will not change during the request, so do not use + // drupal_static(). + static $files = array(); + + if (!isset($files[$type])) { + $files[$type] = array(); + } + + if (!empty($filename) && file_exists($filename)) { + $files[$type][$name] = $filename; + } + elseif (isset($files[$type][$name])) { + // nothing + } + // Verify that we have an active database connection, before querying + // the database. This is required because this function is called both + // before we have a database connection (i.e. during installation) and + // when a database connection fails. + else { + 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_exists(DRUPAL_ROOT . '/' . $file)) { + $files[$type][$name] = $file; + } + } + } + catch (Exception $e) { + // The database table may not exist because Drupal is not yet installed, + // or the database might be down. We have a fallback for this case so we + // hide the error completely. + } + // Fallback to searching the filesystem if the database could not find the + // file or the file returned by the database is not found. + if (!isset($files[$type][$name])) { + // 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; + } + + if (!function_exists('drupal_system_listing')) { + require_once DRUPAL_ROOT . '/core/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) { + $files[$type][$matched_name] = $file->uri; + } + } + } + + if (isset($files[$type][$name])) { + return $files[$type][$name]; + } +} + +/** + * Load 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. + $name = 'variable_init'; + if (!lock_acquire($name, 1)) { + // Another request is building the variable cache. + // Wait, then re-run this function. + lock_wait($name); + return variable_initialize($conf); + } + else { + // Proceed with variable rebuild. + $variables = array_map('unserialize', db_query('SELECT name, value FROM {variable}')->fetchAllKeyed()); + 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. + * + * @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]); +} + +/** + * Retrieve 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; + } +} + +/** + * Determine the cacheability of the current page. + * + * @param $allow_caching + * Set to FALSE if you want to prevent this page to get 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(); +} + +/** + * Invoke 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; +} + +/** + * Set 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); +} + +/** + * Get 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; + } +} + +/** + * 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; +} + +/** + * Send the HTTP response headers previously set using drupal_add_http_header(). + * Add default headers, unless they have been replaced or unset using + * drupal_add_http_header(). + * + * @param $default_headers + * An array of headers as name/value pairs. + * @param $single + * If TRUE and headers have already be sent, send only the specified header. + */ +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) { + header($header_names[$name_lower] . ': ' . $value); + } + } +} + +/** + * Set 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. + * + * Also give each page a unique ETag. This will force clients to include both + * an If-Modified-Since header and an If-None-Match header when doing + * conditional requests for the page (required by RFC 2616, section 13.3.4), + * making the validation more robust. This is a workaround for a bug in Mozilla + * Firefox that is triggered when Drupal's caching is enabled and the user + * accesses Drupal via an HTTP proxy (see + * https://bugzilla.mozilla.org/show_bug.cgi?id=269303): When an authenticated + * user requests a page, and then logs out and requests the same page again, + * Firefox may send a conditional request based on the page that was cached + * locally when the user was logged in. If this page did not have an ETag + * header, the request only contains an If-Modified-Since header. The date will + * be recent, because with authenticated users the Last-Modified header always + * refers to the time of the request. If the user accesses Drupal via a proxy + * server, and the proxy already has a cached copy of the anonymous page with an + * older Last-Modified date, the proxy may respond with 304 Not Modified, making + * the client think that the anonymous and authenticated pageviews are + * identical. + * + * @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', + 'Last-Modified' => gmdate(DATE_RFC1123, REQUEST_TIME), + 'Cache-Control' => 'no-cache, must-revalidate, post-check=0, pre-check=0', + 'ETag' => '"' . REQUEST_TIME . '"', + ); + drupal_send_headers($default_headers); +} + +/** + * Set 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 = variable_get('page_compression', TRUE) && extension_loaded('zlib'); + $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_RFC1123, $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']; +} + +/** + * Define 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. So, + * 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. + * + * 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 $args below or + * the Localization API pages referenced above for details). Translators can + * then rearrange the string as necessary for the language (e.g., in Spanish, + * it might be "blog de @name"). + * + * 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. + * + * @param $string + * A string containing the English string to translate. + * @param $args + * An associative array of replacements to make after translation. + * Occurrences in $string of any key in $args are replaced with the + * corresponding value, after sanitization. The sanitization function depends + * on the first character of the key: + * - !variable: Inserted as is. Use this for text that has already been + * sanitized. + * - @variable: Escaped to HTML using check_plain(). Use this for anything + * displayed on a page on the site. + * - %variable: Escaped as a placeholder for user-submitted content using + * drupal_placeholder(), which shows up as emphasized text. + * @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): The context the source string + * belongs to. + * + * @return + * The translated string. + * + * @see st() + * @see get_t() + * @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 { + // 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); + } +} + +/** + * Encode 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 $text + * The text to be checked or processed. + * + * @return + * An HTML safe version of $text, or an empty string if $text is not + * valid UTF-8. + * + * @see drupal_validate_utf8() + * @ingroup sanitization + */ +function check_plain($text) { + return htmlspecialchars($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($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); +} + +/** + * Since $_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; +} + +/** + * Log 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 = LOG_ERR, $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 . '/core/includes/errors.inc'; + $variables += _drupal_decode_exception($exception); + watchdog($type, $message, $variables, $severity, $link); +} + +/** + * Log 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, as per RFC 3164. Possible values are + * LOG_ERR, LOG_WARNING, etc. + * @param $link + * A link to associate with the message. + * + * @see watchdog_severity_levels() + * @see hook_watchdog() + */ +function watchdog($type, $message, $variables = array(), $severity = LOG_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_implements')) { + $in_error_state = TRUE; + + // Prepare the fields to be logged + $log_entry = array( + 'type' => $type, + 'message' => $message, + 'variables' => $variables, + 'severity' => $severity, + 'link' => $link, + 'user' => $user, + 'request_uri' => $base_root . request_uri(), + 'referer' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '', + 'ip' => ip_address(), + 'timestamp' => REQUEST_TIME, + ); + + // Call the logging hooks to log/process the message + foreach (module_implements('watchdog') as $module) { + module_invoke($module, '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; + } +} + +/** + * Set a message which reflects the status of the performed operation. + * + * If the function is called with no arguments, this function returns all set + * messages without clearing them. + * + * @param $message + * The 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 $type + * The type of the message. One of the following values are possible: + * - 'status' + * - 'warning' + * - 'error' + * @param $repeat + * If this is FALSE and the message is already set, then the message won't + * be repeated. + */ +function drupal_set_message($message = NULL, $type = 'status', $repeat = TRUE) { + if ($message) { + 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; +} + +/** + * Return all messages that have been set. + * + * @param $type + * (optional) Only return messages of this type. + * @param $clear_queue + * (optional) Set to FALSE if you do not want to clear the messages queue + * @return + * An associative array, the key is the message type, the value an array + * of messages. If the $type parameter is passed, you get only that type, + * or an empty array if there are no such messages. If $type is not passed, + * all message types are returned, or an empty array if none exist. + */ +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(); +} + +/** + * Get the title of the current page, for display 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; +} + +/** + * Set the title of the current page, for display 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; +} + +/** + * Check 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; +} + +/** + * Handle 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 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; + // 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 = ''; + } + if (strlen($bytes) < $count) { + // /dev/urandom is available on many *nix systems and is considered the + // best commonly available pseudo-random source. + if ($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, $count)); + fclose($fh); + } + // If /dev/urandom is not available or returns no bytes, this loop 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. + while (strlen($bytes) < $count) { + $random_state = hash('sha256', microtime() . mt_rand() . $random_state); + $bytes .= hash('sha256', mt_rand() . $random_state, TRUE); + } + } + $output = substr($bytes, 0, $count); + $bytes = substr($bytes, $count); + return $output; +} + +/** + * Calculate a base-64 encoded, URL-safe sha-256 hmac. + * + * @param $data + * String to be validated with the hmac. + * @param $key + * A secret string key. + * + * @return + * A base-64 encoded sha-256 hmac, with + replaced with -, / with _ and + * any = padding characters removed. + */ +function drupal_hmac_base64($data, $key) { + $hmac = base64_encode(hash_hmac('sha256', $data, $key, TRUE)); + // Modify the hmac so it's safe to use in URLs. + return strtr($hmac, array('+' => '-', '/' => '_', '=' => '')); +} + +/** + * Calculate 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() { + return drupal_array_merge_deep_array(func_get_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 = new stdClass(); + $user->uid = 0; + $user->hostname = ip_address(); + $user->roles = array(); + $user->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user'; + $user->cache = 0; + return $user; +} + +/** + * A string describing a phase of Drupal to load. Each phase adds to the + * previous one, so invoking a later phase automatically runs the earlier + * phases too. The most important usage is that if you want to access the + * Drupal database from a script without loading anything else, you can + * include bootstrap.inc, and call drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE). + * + * @param $phase + * A constant. Allowed values are the DRUPAL_BOOTSTRAP_* constants. + * @param $new_phase + * A boolean, set to FALSE if calling drupal_bootstrap from inside a + * function called from drupal_bootstrap (recursion). + * @return + * 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; + + // When not recursing, store the phase name so it's not forgotten while + // recursing. + if ($new_phase) { + $final_phase = $phase; + } + if (isset($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: + _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', 'core/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 . '/core/includes/common.inc'; + _drupal_bootstrap_full(); + break; + } + } + } + return $stored_phase; +} + +/** + * Return 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()); + } +} + +/** + * Custom PHP error handler. + * + * @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. + * @param $context + * An array that points to the active symbol table at the point the error occurred. + */ +function _drupal_error_handler($error_level, $message, $filename, $line, $context) { + require_once DRUPAL_ROOT . '/core/includes/errors.inc'; + _drupal_error_handler_real($error_level, $message, $filename, $line, $context); +} + +/** + * Custom PHP exception handler. + * + * 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 . '/core/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) { + // 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) . '


'; + } + } +} + +/** + * Bootstrap configuration: Setup script environment and load 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(); +} + +/** + * Bootstrap page cache: Try to serve a page from 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 . '/core/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'); + } + } +} + +/** + * Bootstrap database: Initialize database system and register 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 . '/core/includes/install.inc'; + install_goto('core/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 . '/core/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'); +} + +/** + * Bootstrap variables: Load 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', 'core/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 . '/core/includes/module.inc'; + module_load_all(TRUE); +} + +/** + * Bootstrap page header: Invoke hook_boot(), initialize locking system, and send default 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(); +} + +/** + * Checks the current User-Agent string to see if this is an internal request + * from SimpleTest. If so, returns the test prefix for this test. + * + * @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() { + global $drupal_hash_salt; + // 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_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; + } + } + + return FALSE; +} + +/** + * Generate a user agent string with a HMAC and timestamp for simpletest. + */ +function drupal_generate_test_ua($prefix) { + global $drupal_hash_salt; + 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_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 . '/core/includes/theme.maintenance.inc'; + _drupal_maintenance_theme(); +} + +/** + * Return 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 the + * module administration page. + * + * Example useage: + * @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; +} + +/** + * Initialize 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 . '/core/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'); + } +} + +/** + * 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, + ); +} + +/** + * Return true if there is more than one language 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; +} + +/** + * Return an array of the available language types. + */ +function language_types() { + return array_keys(variable_get('language_types', drupal_language_types())); +} + +/** + * Get a list of languages set up indexed by the specified key + * + * @param $field The field to index the list with. + */ +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]; +} + +/** + * Default language used on the site + * + * @param $property + * Optional property of the language object to return + */ +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'])) { + // 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; +} + +/** + * Return 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, please 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]; + } +} + +/** + * 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); + + // The right-most IP is the most specific we can trust. + $ip_address = array_pop($untrusted); + } + } + } + + return $ip_address; +} + +/** + * @ingroup schemaapi + * @{ + */ + +/** + * Get 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(). + * + * @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 = 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 . '/core/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 (!isset($table)) { + return $schema; + } + elseif (isset($schema[$table])) { + return $schema[$table]; + } + else { + return FALSE; + } +} + +/** + * @} End of "ingroup schemaapi". + */ + + +/** + * @ingroup registry + * @{ + */ + +/** + * Confirm 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); +} + +/** + * Confirm 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); +} + +/** + * Helper to check 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)) { + 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]) { + require_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')->query("SELECT filename FROM {registry} WHERE name = :name AND type = :type", array( + ':name' => $name, + ':type' => $type, + )) + ->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) { + require_once DRUPAL_ROOT . '/' . $file; + return TRUE; + } + else { + return FALSE; + } +} + +/** + * Rescan all enabled modules and rebuild 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(); +} + +/** + * Update 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. + * + * @see registry_rebuild() + */ +function registry_update() { + require_once DRUPAL_ROOT . '/core/includes/registry.inc'; + _registry_update(); +} + +/** + * @} End of "ingroup registry". + */ + +/** + * 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/en/language.variables.scope.php#language.variables.scope.static + * - http://php.net/manual/en/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 a specific named variable, or all variables if $name is NULL. + * Resetting every variable should only be used, for example, for running + * unit tests with a clean environment. Should be used only though via + * function drupal_static_reset() and the return value should not be used in + * this case. + * + * @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; +} + +/** + * Reset one or all centrally stored static variable(s). + * + * @param $name + * Name of the static variable to reset. Omit to reset all variables. + */ +function drupal_static_reset($name = NULL) { + drupal_static($name, NULL, TRUE); +} + +/** + * Detect 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 t(). + * + * @param $text + * The text to format (plain-text). + * + * @return + * The formatted text (html). + */ +function drupal_placeholder($text) { + return '' . check_plain($text) . ''; +} + +/** + * Register 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; +} + +/** + * Internal function used to execute 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 { + while (list($key, $callback) = each($callbacks)) { + call_user_func_array($callback['callback'], $callback['arguments']); + } + } + 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 . '/core/includes/errors.inc'; + if (error_displayable()) { + print '

Uncaught exception thrown in shutdown function.

'; + print '

' . _drupal_render_exception_safe($exception) . '


'; + } + } +} diff --git a/includes/cache-install.inc b/core/includes/cache-install.inc similarity index 100% rename from includes/cache-install.inc rename to core/includes/cache-install.inc diff --git a/includes/cache.inc b/core/includes/cache.inc similarity index 100% rename from includes/cache.inc rename to core/includes/cache.inc diff --git a/core/includes/common.inc b/core/includes/common.inc new file mode 100644 index 0000000..1db708c --- /dev/null +++ b/core/includes/common.inc @@ -0,0 +1,7844 @@ + $uri) { + $xml_rdf_namespaces[] = 'xmlns:' . $prefix . '="' . $uri . '"'; + } + } + return count($xml_rdf_namespaces) ? "\n " . implode("\n ", $xml_rdf_namespaces) : ''; +} + +/** + * Add output to the head tag of the HTML page. + * + * This function can be called as long 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; +} + +/** + * Retrieve 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); +} + +/** + * Add 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; +} + +/** + * Get 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($feeds, $delimiter); +} + +/** + * @defgroup http_handling HTTP handling + * @{ + * Functions to properly handle HTTP responses. + */ + +/** + * Process 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; +} + +/** + * Split an 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); + $result[$param[0]] = isset($param[1]) ? rawurldecode($param[1]) : ''; + } + } + return $result; +} + +/** + * Parse 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); +} + +/** + * Prepare a 'destination' URL query parameter for use in combination 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. + * + * @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; +} + +/** + * Wrapper around parse_url() to parse a system URL string into an associative array, suitable for url(). + * + * This function should only be used for URLs that have been generated by the + * system, resp. url(). It should not be used for URLs that come from external + * sources, or URLs that link to external resources. + * + * The returned array contains a 'path' that may be passed separately to url(). + * For example: + * @code + * $options = drupal_parse_url($_GET['destination']); + * $my_url = url($options['path'], $options); + * $my_link = l('Example link', $options['path'], $options); + * @endcode + * + * This is required, because url() does not support relative URLs containing a + * query string or fragment in its $path argument. Instead, any query string + * needs to be parsed into an associative query parameter array in + * $options['query'] and the fragment into $options['fragment']. + * + * @param $url + * The URL string to parse, f.e. $_GET['destination']. + * + * @return + * An associative array containing the keys: + * - 'path': The path of the URL. If the given $url is external, this includes + * the scheme and host. + * - 'query': An array of query parameters of $url, if existent. + * - 'fragment': The fragment of $url, if existent. + * + * @see url() + * @see drupal_goto() + * @ingroup php_wrappers + */ +function drupal_parse_url($url) { + $options = array( + 'path' => NULL, + 'query' => array(), + 'fragment' => '', + ); + + // External URLs: not using parse_url() here, so we do not have to rebuild + // the scheme, host, and path without having any use for it. + if (strpos($url, '://') !== FALSE) { + // Split off everything before the query string into 'path'. + $parts = explode('?', $url); + $options['path'] = $parts[0]; + // If there is a query string, transform it into keyed query parameters. + if (isset($parts[1])) { + $query_parts = explode('#', $parts[1]); + parse_str($query_parts[0], $options['query']); + // Take over the fragment, if there is any. + if (isset($query_parts[1])) { + $options['fragment'] = $query_parts[1]; + } + } + } + // Internal URLs. + else { + // 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. + if (isset($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($path)); +} + +/** + * Send the user to a different Drupal page. + * + * This issues an on-site HTTP redirect. The function makes sure the redirected + * URL is formatted correctly. + * + * If a destination was specified in the current request's URI (i.e., + * $_GET['destination']) then it will override the $path and $options values + * passed to this function. This provides the flexibility to build a link to + * user/login and override the default redirection so that the user is + * redirected to a specific path after logging in: + * @code + * $query = array('destination' => "node/$node->nid"); + * $link = l(t('Log in'), 'user/login', array('query' => $query)); + * @endcode + * + * 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 + * A Drupal path or a full URL. + * @param $options + * An associative array of additional URL options to pass to url(). + * @param $http_response_code + * Valid values for an actual "goto" as per RFC 2616 section 10.3 are: + * - 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 (alternative to "503 Site Down for Maintenance") + * Note: Other values are defined by RFC 2616, but are rarely used and poorly + * supported. + * + * @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']); + $path = $destination['path']; + $options['query'] = $destination['query']; + $options['fragment'] = $destination['fragment']; + } + + 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); +} + +/** + * Deliver 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); +} + +/** + * Deliver 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); +} + +/** + * Deliver a "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); +} + +/** + * Perform 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: A string containing the request body, formatted as + * 'param=value¶m=value&...'. Defaults to NULL. + * - max_redirects: An integer representing how many times a redirect + * may be followed. Defaults to 3. + * - timeout: A float representing the maximum number of seconds the function + * call may take. The default is 30 seconds. If a timeout occurs, the error + * code is set to the HTTP_REQUEST_TIMEOUT constant. + * - context: A context resource created with stream_context_create(). + * + * @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 redirection location. + * - 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. + */ +function drupal_http_request($url, array $options = array()) { + $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, + ); + // stream_socket_client() requires timeout to be a float. + $options['timeout'] = (float) $options['timeout']; + + switch ($uri['scheme']) { + 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. + $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; + $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']; + } + + // Merge the default headers. + $options['headers'] += array( + 'User-Agent' => 'Drupal (+http://drupal.org/)', + ); + + // 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($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'] . (!empty($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. + list($protocol, $code, $status_message) = explode(' ', trim(array_shift($response)), 3); + $result->protocol = $protocol; + $result->status_message = $status_message; + + $result->headers = array(); + + // Parse the response headers. + while ($line = trim(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 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']--; + $result = drupal_http_request($location, $options); + $result->redirect_code = $code; + } + $result->redirect_url = $location; + break; + default: + $result->error = $status_message; + } + + return $result; +} +/** + * @} End of "HTTP handling". + */ + +function _fix_gpc_magic(&$item) { + if (is_array($item)) { + array_walk($item, '_fix_gpc_magic'); + } + else { + $item = stripslashes($item); + } +} + +/** + * Helper function to strip slashes from $_FILES skipping over the tmp_name keys + * since PHP generates single backslashes for file paths on Windows systems. + * + * tmp_name does not have backslashes added see + * http://php.net/manual/en/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); + } + } +} + +/** + * Fix double-escaping problems caused by "magic quotes" in some PHP installations. + */ +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. + */ + +/** + * Verify the syntax of the given e-mail address. + * + * Empty e-mail addresses are allowed. See RFC 2822 for details. + * + * @param $mail + * A string containing an e-mail address. + * @return + * TRUE if the address is in a valid format. + */ +function valid_email_address($mail) { + return (bool)filter_var($mail, FILTER_VALIDATE_EMAIL); +} + +/** + * Verify 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". + */ + +/** + * Register 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(); +} + +/** + * Make the flood control mechanism forget about 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 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 (e.g. 'javascript:') from a URI and encodes it for output to an HTML attribute value. + * + * @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)); +} + +/** + * 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', 'b', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'div', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'ul', 'var')); +} + +/** + * Filters an HTML string 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() + * @ingroup sanitization + */ +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); + // 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})+;)/', '&#x\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]+)([^>]*)>?|()$%', $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 ""; + } + + // 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'); + $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 ensures it does not contain an URL with a disallowed protocol (e.g. javascript:). + * + * @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 . '/core/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 &amp;). + $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; +} + +/** + * Format 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; +} + +/** + * Format 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 + * + * In both cases, 'value' can be a simple string, or it can be another array + * with the same format as $array itself for nesting. + */ +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']) : check_plain($value['value'])) . '\n"; + } + else { + $output .= " />\n"; + } + } + } + else { + $output .= ' <' . $key . '>' . (is_array($value) ? format_xml_elements($value) : check_plain($value)) . "\n"; + } + } + return $output; +} + +/** + * Format 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. Please 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. Please 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. Incidences + * 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: + * - !variable: inserted as is + * - @variable: escape plain text to HTML (check_plain) + * - %variable: escape text and theme as a placeholder for user-submitted + * content (check_plain + drupal_placeholder) + * 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, with the following keys: + * - 'langcode' (default to the current language) The language code to + * translate to a language other than what is used to display the page. + * - 'context' (default to the empty context) The context the source string + * belongs to. + * @return + * A translated 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; + // Backwards compatibility. + 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); + } + } +} + +/** + * Parse 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 = 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); + } +} + +/** + * Generate 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); + } +} + +/** + * Format a time interval with the requested granularity. + * + * @param $timestamp + * 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($timestamp, $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 ($timestamp >= $value) { + $output .= ($output ? ' ' : '') . format_plural(floor($timestamp / $value), $key[0], $key[1], array(), array('langcode' => $langcode)); + $timestamp %= $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/en/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. + * + * 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 + * The internal path or external URL being linked to, such as "node/34" or + * "http://example.com/foo". 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 + * 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' => '' + ); + + if (!isset($options['external'])) { + // Return an external link if $path contains an allowed absolute URL. Only + // call the slow drupal_strip_dangerous_protocols() if $path contains a ':' + // before any / ? or #. Note: we could use url_is_external($path) here, but + // that would require another function call, and performance inside url() is + // critical. + $colonpos = strpos($path, ':'); + $options['external'] = ($colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $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']; + } + + 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 : ''; + $alias = drupal_get_path_alias($original_path, $language); + if ($alias != $original_path) { + $path = $alias; + } + } + + $base = $options['absolute'] ? $options['base_url'] . '/' : base_path(); + $prefix = empty($path) ? rtrim($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']; + } +} + +/** + * Return 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) { + $colonpos = strpos($path, ':'); + // Only call the slow drupal_strip_dangerous_protocols() if $path contains a + // ':' before any / ? or #. + return $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path; +} + +/** + * Format an attribute string for a 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 attribute string for use in XML/HTML tags. + * + * 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. + * + * @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. + * + * @param $text + * The link text for the anchor tag. + * @param $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, with 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. + * - '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 + * An HTML string containing a link to the given path. + */ +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(); + // 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; + } + } + 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']), LOG_ERR); + } +} + +/** + * Package and send 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'); + } + + // 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, LOG_WARNING); + + // Keep old path for reference, and to allow forms to redirect to it. + if (!isset($_GET['destination'])) { + $_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 could not be found.'); + } + + 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, LOG_WARNING); + + // Keep old path for reference, and to allow forms to redirect to it. + if (!isset($_GET['destination'])) { + $_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(); +} + +/** + * Perform 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(); + system_run_automated_cron(); +} + +/** + * Perform 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; +} + +/** + * Form 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. + * + * It also means that 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')) { + @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. + */ +function drupal_get_path($type, $name) { + return dirname(drupal_get_filename($type, $name)); +} + +/** + * Return the base URL path (i.e., directory) of the Drupal installation. + * + * base_path() prefixes and suffixes a "/" onto 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']; +} + +/** + * Add a LINK tag with a distinct 'rel' attribute 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' and + * 'href' 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 = '<' . check_plain($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'] . ':' . $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(), e.g., + * "modules/devel/devel.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 + * 'core/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: 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()); + + // 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($css) / 1000; + + // 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 that should be attached 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'); + + // 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'] : 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, + ); + + // Provide the page with information about the individual CSS files used, + // information not otherwise available when CSS aggregation is enabled. + $setting['ajaxPageState']['css'] = array_fill_keys(array_keys($css), 1); + $styles['#attached']['js'][] = array('type' => 'setting', 'data' => $setting); + + return drupal_render($styles); +} + +/** + * Function used by uasort to sort the array structures returned by drupal_add_css() and drupal_add_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. + */ +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 accomodate 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() + */ +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() + */ +function drupal_aggregate_css(&$css_groups) { + $preprocess_css = (variable_get('preprocess_css', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update')); + + // 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; + $element['#value'] = implode("\n", $import_batch); + $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()); + $key = hash('sha256', serialize($css)); + 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; +} + +/** + * Helper function for drupal_build_css_cache(). + * + * This function will prefix all paths within a CSS file. + */ +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 . $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; + } + $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. + if ($contents = @file_get_contents($file)) { + // Return the processed stylesheet. + return drupal_load_stylesheet_content($contents, $_optimize); + } + + return ''; +} + +/** + * Process 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/en/regexp.reference.subpatterns.php + $contents = preg_replace('< + # 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+ + >xS', + // Only one of the three 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', + $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]+:|\/+)/i', 'url(\1'. $directory, $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); + } +} + +/** + * Prepare a string for use as a valid 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(' ' => '-', '_' => '-', '/' => '-', '[' => '-', ']' => '')) { + // 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; +} + +/** + * Prepare 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) { + return drupal_clean_css_identifier(drupal_strtolower($class)); +} + +/** + * Prepare a string for use as a valid HTML ID and guarantee 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. + $seen_ids_init = &drupal_static(__FUNCTION__ . ':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. + foreach ($_POST['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; + } + } + } + } + $seen_ids = &drupal_static(__FUNCTION__, $seen_ids_init); + + $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('core/misc/collapse.js'); + * drupal_add_js('core/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: + * - '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. + * - defer: If set to TRUE, the defer attribute is set on the <script> + * tag. Defaults to FALSE. + * - cache: If set to FALSE, the JavaScript file is loaded anew on every page + * call; in other words, it is not cached. Used only when 'type' references + * a JavaScript file. Defaults to TRUE. + * - preprocess: If TRUE and JavaScript aggregation is enabled, the script + * file will be aggregated. Defaults to TRUE. + * + * @return + * The current array of JavaScript files, settings, and in-line code, + * including Drupal defaults, anything previously added with calls to + * drupal_add_js(), and this function call's additions. + * + * @see drupal_get_js() + */ +function drupal_add_js($data = NULL, $options = NULL) { + $javascript = &drupal_static(__FUNCTION__, array()); + + // Construct the options, taking the defaults into consideration. + if (isset($options)) { + if (!is_array($options)) { + $options = array('type' => $options); + } + } + else { + $options = array(); + } + $options += drupal_js_defaults($data); + + // Preprocess can only be set if caching is enabled. + $options['preprocess'] = $options['cache'] ? $options['preprocess'] : FALSE; + + // Tweak the weight so that files of the same weight are included in the + // order of the calls to drupal_add_js(). + $options['weight'] += count($javascript) / 1000; + + if (isset($data)) { + // Add jquery.js and drupal.js, as well as the basePath setting, the + // first time a JavaScript file is added. + if (empty($javascript)) { + // url() generates the prefix using hook_url_outbound_alter(). Instead of + // running the hook_url_outbound_alter() again here, extract the prefix + // from url(). + url('', array('prefix' => &$prefix)); + $javascript = array( + 'settings' => array( + 'data' => array( + array('basePath' => base_path()), + array('pathPrefix' => empty($prefix) ? '' : $prefix), + ), + 'type' => 'setting', + 'scope' => 'header', + 'group' => JS_LIBRARY, + 'every_page' => TRUE, + 'weight' => 0, + ), + 'core/misc/drupal.js' => array( + 'data' => 'core/misc/drupal.js', + 'type' => 'file', + 'scope' => 'header', + 'group' => JS_LIBRARY, + 'every_page' => TRUE, + 'weight' => -1, + 'preprocess' => TRUE, + 'cache' => TRUE, + 'defer' => FALSE, + ), + ); + // Register all required libraries. + drupal_add_library('system', 'jquery', TRUE); + drupal_add_library('system', 'jquery.once', TRUE); + } + + switch ($options['type']) { + case 'setting': + // All JavaScript settings are placed in the header of the page with + // the library weight so that inline scripts appear afterwards. + $javascript['settings']['data'][] = $data; + break; + + case 'inline': + $javascript[] = $options; + break; + + default: // 'file' and 'external' + // Local and external files must keep their name as the associative key + // so the same JavaScript file is not added twice. + $javascript[$options['data']] = $options; + } + } + return $javascript; +} + +/** + * Constructs an array of the defaults that are used for JavaScript items. + * + * @param $data + * (optional) The default data parameter for the JavaScript item array. + * @see drupal_get_js() + * @see drupal_add_js() + */ +function drupal_js_defaults($data = NULL) { + return array( + 'type' => 'file', + 'group' => JS_DEFAULT, + 'every_page' => FALSE, + 'weight' => 0, + 'scope' => 'header', + 'cache' => TRUE, + 'defer' => FALSE, + 'preprocess' => TRUE, + 'version' => NULL, + 'data' => $data, + ); +} + +/** + * Returns a themed presentation of all JavaScript code for the current page. + * + * References to JavaScript files are placed in a certain order: first, all + * 'core' files, then all 'module' and finally all 'theme' JavaScript files + * are added to the page. Then, all settings are output, followed by 'inline' + * JavaScript code. If running update.php, all preprocessing is disabled. + * + * Note that hook_js_alter(&$javascript) is called during this function call + * to allow alterations of the JavaScript during its presentation. Calls to + * drupal_add_js() from hook_js_alter() will not be added to the output + * presentation. The correct way to add JavaScript during hook_js_alter() + * is to add another element to the $javascript array, deriving from + * drupal_js_defaults(). See locale_js_alter() for an example of this. + * + * @param $scope + * (optional) The scope for which the JavaScript rules should be returned. + * Defaults to 'header'. + * @param $javascript + * (optional) An array with all JavaScript code. Defaults to the default + * JavaScript array for the given scope. + * @param $skip_alter + * (optional) If set to TRUE, this function skips calling drupal_alter() on + * $javascript, useful when the calling function passes a $javascript array + * that has already been altered. + * @return + * All JavaScript code segments and includes for the scope as HTML tags. + * @see drupal_add_js() + * @see locale_js_alter() + * @see drupal_js_defaults() + */ +function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALSE) { + if (!isset($javascript)) { + $javascript = drupal_add_js(); + } + if (empty($javascript)) { + return ''; + } + + // Allow modules to alter the JavaScript. + if (!$skip_alter) { + drupal_alter('js', $javascript); + } + + // Filter out elements of the given scope. + $items = array(); + foreach ($javascript as $key => $item) { + if ($item['scope'] == $scope) { + $items[$key] = $item; + } + } + + $output = ''; + // The index counter is used to keep aggregated and non-aggregated files in + // order by weight. + $index = 1; + $processed = array(); + $files = array(); + $preprocess_js = (variable_get('preprocess_js', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update')); + + // 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. Files that should not be cached (see drupal_add_js()) + // get REQUEST_TIME as query-string instead, to enforce reload on every + // page request. + $default_query_string = variable_get('css_js_query_string', '0'); + + // For inline JavaScript to validate as XHTML, all JavaScript 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"; + + // Since JavaScript may look for arguments in the URL and act on them, some + // third-party code might require the use of a different query string. + $js_version_string = variable_get('drupal_js_version_query_string', 'v='); + + // Sort the JavaScript so that it appears in the correct order. + uasort($items, 'drupal_sort_css_js'); + + // Provide the page with information about the individual JavaScript files + // used, information not otherwise available when aggregation is enabled. + $setting['ajaxPageState']['js'] = array_fill_keys(array_keys($items), 1); + unset($setting['ajaxPageState']['js']['settings']); + drupal_add_js($setting, 'setting'); + + // If we're outputting the header scope, then this might be the final time + // that drupal_get_js() is running, so add the setting to this output as well + // as to the drupal_add_js() cache. If $items['settings'] doesn't exist, it's + // because drupal_get_js() was intentionally passed a $javascript argument + // stripped off settings, potentially in order to override how settings get + // output, so in this case, do not add the setting to this output. + if ($scope == 'header' && isset($items['settings'])) { + $items['settings']['data'][] = $setting; + } + + // Loop through the JavaScript to construct the rendered output. + $element = array( + '#tag' => 'script', + '#value' => '', + '#attributes' => array( + 'type' => 'text/javascript', + ), + ); + foreach ($items as $item) { + $query_string = empty($item['version']) ? $default_query_string : $js_version_string . $item['version']; + + switch ($item['type']) { + case 'setting': + $js_element = $element; + $js_element['#value_prefix'] = $embed_prefix; + $js_element['#value'] = 'jQuery.extend(Drupal.settings, ' . drupal_json_encode(drupal_array_merge_deep_array($item['data'])) . ");"; + $js_element['#value_suffix'] = $embed_suffix; + $output .= theme('html_tag', array('element' => $js_element)); + break; + + case 'inline': + $js_element = $element; + if ($item['defer']) { + $js_element['#attributes']['defer'] = 'defer'; + } + $js_element['#value_prefix'] = $embed_prefix; + $js_element['#value'] = $item['data']; + $js_element['#value_suffix'] = $embed_suffix; + $processed[$index++] = theme('html_tag', array('element' => $js_element)); + break; + + case 'file': + $js_element = $element; + if (!$item['preprocess'] || !$preprocess_js) { + if ($item['defer']) { + $js_element['#attributes']['defer'] = 'defer'; + } + $query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?'; + $js_element['#attributes']['src'] = file_create_url($item['data']) . $query_string_separator . ($item['cache'] ? $query_string : REQUEST_TIME); + $processed[$index++] = theme('html_tag', array('element' => $js_element)); + } + else { + // By increasing the index for each aggregated file, we maintain + // the relative ordering of JS by weight. We also set the key such + // that groups are split by items sharing the same 'group' value and + // 'every_page' flag. While this potentially results in more aggregate + // files, it helps make each one more reusable across a site visit, + // leading to better front-end performance of a website as a whole. + // See drupal_add_js() for details. + $key = 'aggregate_' . $item['group'] . '_' . $item['every_page'] . '_' . $index; + $processed[$key] = ''; + $files[$key][$item['data']] = $item; + } + break; + + case 'external': + $js_element = $element; + // Preprocessing for external JavaScript files is ignored. + if ($item['defer']) { + $js_element['#attributes']['defer'] = 'defer'; + } + $js_element['#attributes']['src'] = $item['data']; + $processed[$index++] = theme('html_tag', array('element' => $js_element)); + break; + } + } + + // Aggregate any remaining JS files that haven't already been output. + if ($preprocess_js && count($files) > 0) { + foreach ($files as $key => $file_set) { + $uri = drupal_build_js_cache($file_set); + // Only include the file if was written successfully. Errors are logged + // using watchdog. + if ($uri) { + $preprocess_file = file_create_url($uri); + $js_element = $element; + $js_element['#attributes']['src'] = $preprocess_file; + $processed[$key] = theme('html_tag', array('element' => $js_element)); + } + } + } + + // Keep the order of JS files consistent as some are preprocessed and others are not. + // Make sure any inline or JS setting variables appear last after libraries have loaded. + return implode('', $processed) . $output; +} + +/** + * Adds attachments to a render() structure. + * + * Libraries, JavaScript, CSS and other types of custom structures are attached + * to elements using the #attached property. The #attached property is an + * associative array, where the keys are the the attachment types and the values + * are the attached data. For example: + * @code + * $build['#attached'] = array( + * 'js' => array(drupal_get_path('module', 'taxonomy') . '/taxonomy.js'), + * 'css' => array(drupal_get_path('module', 'taxonomy') . '/taxonomy.css'), + * ); + * @endcode + * + * 'js', 'css', and 'library' are types that get special handling. For any + * other kind of attached data, the array key must be the full name of the + * callback function and each value an array of arguments. For example: + * @code + * $build['#attached']['drupal_add_http_header'] = array( + * array('Content-Type', 'application/rss+xml; charset=utf-8'), + * ); + * @endcode + * + * External 'js' and 'css' files can also be loaded. For example: + * @code + * $build['#attached']['js'] = array( + * 'http://code.jquery.com/jquery-1.4.2.min.js' => array( + * 'type' => 'external', + * ), + * ); + * @endcode + * + * @param $elements + * The structured array describing the data being rendered. + * @param $group + * The default group of JavaScript and CSS being added. This is only applied + * to the stylesheets and JavaScript items that don't have an explicit group + * assigned to them. + * @param $dependency_check + * When TRUE, will exit if a given library's dependencies are missing. When + * set to FALSE, will continue to add the libraries, even though one or more + * dependencies are missing. Defaults to FALSE. + * @param $every_page + * Set to TRUE to indicate that the attachments are added to every page on the + * site. Only attachments with the every_page flag set to TRUE can participate + * in JavaScript/CSS aggregation. + * + * @return + * FALSE if there were any missing library dependencies; TRUE if all library + * dependencies were met. + * + * @see drupal_add_library() + * @see drupal_add_js() + * @see drupal_add_css() + * @see drupal_render() + */ +function drupal_process_attached($elements, $group = JS_DEFAULT, $dependency_check = FALSE, $every_page = NULL) { + // Add defaults to the special attached structures that should be processed differently. + $elements['#attached'] += array( + 'library' => array(), + 'js' => array(), + 'css' => array(), + ); + + // Add the libraries first. + $success = TRUE; + foreach ($elements['#attached']['library'] as $library) { + if (drupal_add_library($library[0], $library[1], $every_page) === FALSE) { + $success = FALSE; + // Exit if the dependency is missing. + if ($dependency_check) { + return $success; + } + } + } + unset($elements['#attached']['library']); + + // Add both the JavaScript and the CSS. + // The parameters for drupal_add_js() and drupal_add_css() require special + // handling. + foreach (array('js', 'css') as $type) { + foreach ($elements['#attached'][$type] as $data => $options) { + // If the value is not an array, it's a filename and passed as first + // (and only) argument. + if (!is_array($options)) { + $data = $options; + $options = NULL; + } + // In some cases, the first parameter ($data) is an array. Arrays can't be + // passed as keys in PHP, so we have to get $data from the value array. + if (is_numeric($data)) { + $data = $options['data']; + unset($options['data']); + } + // Apply the default group if it isn't explicitly given. + if (!isset($options['group'])) { + $options['group'] = $group; + } + // Set the every_page flag if one was passed. + if (isset($every_page)) { + $options['every_page'] = $every_page; + } + call_user_func('drupal_add_' . $type, $data, $options); + } + unset($elements['#attached'][$type]); + } + + // Add additional types of attachments specified in the render() structure. + // Libraries, JavaScript and CSS have been added already, as they require + // special handling. + foreach ($elements['#attached'] as $callback => $options) { + if (function_exists($callback)) { + foreach ($elements['#attached'][$callback] as $args) { + call_user_func_array($callback, $args); + } + } + } + + return $success; +} + +/** + * Adds JavaScript to change the state of an element based on another element. + * + * A "state" means a certain property on a DOM element, such as "visible" or + * "checked". A state can be applied to an element, depending on the state of + * another element on the page. In general, states depend on HTML attributes and + * DOM element properties, which change due to user interaction. + * + * Since states are driven by JavaScript only, it is important to understand + * that all states are applied on presentation only, none of the states force + * any server-side logic, and that they will not be applied for site visitors + * without JavaScript support. All modules implementing states have to make + * sure that the intended logic also works without JavaScript being enabled. + * + * #states is an associative array in the form of: + * @code + * array( + * STATE1 => CONDITIONS_ARRAY1, + * STATE2 => CONDITIONS_ARRAY2, + * ... + * ) + * @endcode + * Each key is the name of a state to apply to the element, such as 'visible'. + * Each value is a list of conditions that denote when the state should be + * applied. + * + * Multiple different states may be specified to act on complex conditions: + * @code + * array( + * 'visible' => CONDITIONS, + * 'checked' => OTHER_CONDITIONS, + * ) + * @endcode + * + * Every condition is a key/value pair, whose key is a jQuery selector that + * denotes another element on the page, and whose value is an array of + * conditions, which must bet met on that element: + * @code + * array( + * 'visible' => array( + * JQUERY_SELECTOR => REMOTE_CONDITIONS, + * JQUERY_SELECTOR => REMOTE_CONDITIONS, + * ... + * ), + * ) + * @endcode + * All conditions must be met for the state to be applied. + * + * Each remote condition is a key/value pair specifying conditions on the other + * element that need to be met to apply the state to the element: + * @code + * array( + * 'visible' => array( + * ':input[name="remote_checkbox"]' => array('checked' => TRUE), + * ), + * ) + * @endcode + * + * For example, to show a textfield only when a checkbox is checked: + * @code + * $form['toggle_me'] = array( + * '#type' => 'checkbox', + * '#title' => t('Tick this box to type'), + * ); + * $form['settings'] = array( + * '#type' => 'textfield', + * '#states' => array( + * // Only show this field when the 'toggle_me' checkbox is enabled. + * 'visible' => array( + * ':input[name="toggle_me"]' => array('checked' => TRUE), + * ), + * ), + * ); + * @endcode + * + * The following states may be applied to an element: + * - enabled + * - disabled + * - visible + * - invisible + * - checked + * - unchecked + * - expanded + * - collapsed + * + * The following states may be used in remote conditions: + * - enabled + * - disabled + * - visible + * - invisible + * - checked + * - unchecked + * - value + * + * The following states exist for both states and remote conditions, but are not + * fully implemented and may not change anything on the element: + * - required + * - optional + * - relevant + * - irrelevant + * - valid + * - invalid + * - touched + * - untouched + * - filled + * - empty + * - readwrite + * - readonly + * + * When referencing select lists and radio buttons in remote conditions, a + * 'value' condition must be used: + * @code + * '#states' => array( + * // Show the settings if 'bar' has been selected for 'foo'. + * 'visible' => array( + * ':input[name="foo"]' => array('value' => 'bar'), + * ), + * ), + * @endcode + * + * @param $elements + * A renderable array element having a #states property as described above. + * + * @see form_example_states_form() + */ +function drupal_process_states(&$elements) { + $elements['#attached']['library'][] = array('system', 'drupal.states'); + $elements['#attached']['js'][] = array( + 'type' => 'setting', + 'data' => array('states' => array('#' . $elements['#id'] => $elements['#states'])), + ); +} + +/** + * Adds multiple JavaScript or CSS files at the same time. + * + * A library defines a set of JavaScript and/or CSS files, optionally using + * settings, and optionally requiring another library. For example, a library + * can be a jQuery plugin, a JavaScript framework, or a CSS framework. This + * function allows modules to load a library defined/shipped by itself or a + * depending module, without having to add all files of the library separately. + * Each library is only loaded once. + * + * @param $module + * The name of the module that registered the library. + * @param $name + * The name of the library to add. + * @param $every_page + * Set to TRUE if this library is added to every page on the site. Only items + * with the every_page flag set to TRUE can participate in aggregation. + * + * @return + * TRUE if the library was successfully added; FALSE if the library or one of + * its dependencies could not be added. + * + * @see drupal_get_library() + * @see hook_library() + * @see hook_library_alter() + */ +function drupal_add_library($module, $name, $every_page = NULL) { + $added = &drupal_static(__FUNCTION__, array()); + + // Only process the library if it exists and it was not added already. + if (!isset($added[$module][$name])) { + if ($library = drupal_get_library($module, $name)) { + // Add all components within the library. + $elements['#attached'] = array( + 'library' => $library['dependencies'], + 'js' => $library['js'], + 'css' => $library['css'], + ); + $added[$module][$name] = drupal_process_attached($elements, JS_LIBRARY, TRUE, $every_page); + } + else { + // Requested library does not exist. + $added[$module][$name] = FALSE; + } + } + + return $added[$module][$name]; +} + +/** + * Retrieves information for a JavaScript/CSS library. + * + * Library information is statically cached. Libraries are keyed by module for + * several reasons: + * - Libraries are not unique. Multiple modules might ship with the same library + * in a different version or variant. This registry cannot (and does not + * attempt to) prevent library conflicts. + * - Modules implementing and thereby depending on a library that is registered + * by another module can only rely on that module's library. + * - Two (or more) modules can still register the same library and use it + * without conflicts in case the libraries are loaded on certain pages only. + * + * @param $module + * The name of a module that registered a library. + * @param $name + * (optional) The name of a registered library to retrieve. By default, all + * libraries registered by $module are returned. + * + * @return + * The definition of the requested library, if $name was passed and it exists, + * or FALSE if it does not exist. If no $name was passed, an associative array + * of libraries registered by $module is returned (which may be empty). + * + * @see drupal_add_library() + * @see hook_library() + * @see hook_library_alter() + * + * @todo The purpose of drupal_get_*() is completely different to other page + * requisite API functions; find and use a different name. + */ +function drupal_get_library($module, $name = NULL) { + $libraries = &drupal_static(__FUNCTION__, array()); + + if (!isset($libraries[$module])) { + // Retrieve all libraries associated with the module. + $module_libraries = module_invoke($module, 'library'); + if (empty($module_libraries)) { + $module_libraries = array(); + } + // Allow modules to alter the module's registered libraries. + drupal_alter('library', $module_libraries, $module); + + foreach ($module_libraries as $key => $data) { + if (is_array($data)) { + // Add default elements to allow for easier processing. + $module_libraries[$key] += array('dependencies' => array(), 'js' => array(), 'css' => array()); + foreach ($module_libraries[$key]['js'] as $file => $options) { + $module_libraries[$key]['js'][$file]['version'] = $module_libraries[$key]['version']; + } + } + } + $libraries[$module] = $module_libraries; + } + if (isset($name)) { + if (!isset($libraries[$module][$name])) { + $libraries[$module][$name] = FALSE; + } + return $libraries[$module][$name]; + } + return $libraries[$module]; +} + +/** + * Assist in adding the tableDrag JavaScript behavior to a themed table. + * + * Draggable tables should be used wherever an outline or list of sortable items + * needs to be arranged by an end-user. Draggable tables are very flexible and + * can manipulate the value of form elements placed within individual columns. + * + * To set up a table to use drag and drop in place of weight select-lists or + * in place of a form that contains parent relationships, the form must be + * themed into a table. The table must have an id attribute set. If using + * theme_table(), the id may be set as such: + * @code + * $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'my-module-table'))); + * return $output; + * @endcode + * + * In the theme function for the form, a special class must be added to each + * form element within the same column, "grouping" them together. + * + * In a situation where a single weight column is being sorted in the table, the + * classes could be added like this (in the theme function): + * @code + * $form['my_elements'][$delta]['weight']['#attributes']['class'] = array('my-elements-weight'); + * @endcode + * + * Each row of the table must also have a class of "draggable" in order to enable the + * drag handles: + * @code + * $row = array(...); + * $rows[] = array( + * 'data' => $row, + * 'class' => array('draggable'), + * ); + * @endcode + * + * When tree relationships are present, the two additional classes + * 'tabledrag-leaf' and 'tabledrag-root' can be used to refine the behavior: + * - Rows with the 'tabledrag-leaf' class cannot have child rows. + * - Rows with the 'tabledrag-root' class cannot be nested under a parent row. + * + * Calling drupal_add_tabledrag() would then be written as such: + * @code + * drupal_add_tabledrag('my-module-table', 'order', 'sibling', 'my-elements-weight'); + * @endcode + * + * In a more complex case where there are several groups in one column (such as + * the block regions on the admin/structure/block page), a separate subgroup class + * must also be added to differentiate the groups. + * @code + * $form['my_elements'][$region][$delta]['weight']['#attributes']['class'] = array('my-elements-weight', 'my-elements-weight-' . $region); + * @endcode + * + * $group is still 'my-element-weight', and the additional $subgroup variable + * will be passed in as 'my-elements-weight-' . $region. This also means that + * you'll need to call drupal_add_tabledrag() once for every region added. + * + * @code + * foreach ($regions as $region) { + * drupal_add_tabledrag('my-module-table', 'order', 'sibling', 'my-elements-weight', 'my-elements-weight-' . $region); + * } + * @endcode + * + * In a situation where tree relationships are present, adding multiple + * subgroups is not necessary, because the table will contain indentations that + * provide enough information about the sibling and parent relationships. + * See theme_menu_overview_form() for an example creating a table containing + * parent relationships. + * + * Please note that this function should be called from the theme layer, such as + * in a .tpl.php file, theme_ function, or in a template_preprocess function, + * not in a form declaration. Though the same JavaScript could be added to the + * page using drupal_add_js() directly, this function helps keep template files + * clean and readable. It also prevents tabledrag.js from being added twice + * accidentally. + * + * @param $table_id + * String containing the target table's id attribute. If the table does not + * have an id, one will need to be set, such as . + * @param $action + * String describing the action to be done on the form item. Either 'match' + * 'depth', or 'order'. Match is typically used for parent relationships. + * Order is typically used to set weights on other form elements with the same + * group. Depth updates the target element with the current indentation. + * @param $relationship + * String describing where the $action variable should be performed. Either + * 'parent', 'sibling', 'group', or 'self'. Parent will only look for fields + * up the tree. Sibling will look for fields in the same group in rows above + * and below it. Self affects the dragged row itself. Group affects the + * dragged row, plus any children below it (the entire dragged group). + * @param $group + * A class name applied on all related form elements for this action. + * @param $subgroup + * (optional) If the group has several subgroups within it, this string should + * contain the class name identifying fields in the same subgroup. + * @param $source + * (optional) If the $action is 'match', this string should contain the class + * name identifying what field will be used as the source value when matching + * the value in $subgroup. + * @param $hidden + * (optional) The column containing the field elements may be entirely hidden + * from view dynamically when the JavaScript is loaded. Set to FALSE if the + * column should not be hidden. + * @param $limit + * (optional) Limit the maximum amount of parenting in this table. + * @see block-admin-display-form.tpl.php + * @see theme_menu_overview_form() + */ +function drupal_add_tabledrag($table_id, $action, $relationship, $group, $subgroup = NULL, $source = NULL, $hidden = TRUE, $limit = 0) { + $js_added = &drupal_static(__FUNCTION__, FALSE); + if (!$js_added) { + // Add the table drag JavaScript to the page before the module JavaScript + // to ensure that table drag behaviors are registered before any module + // uses it. + drupal_add_library('system', 'jquery.cookie'); + drupal_add_js('core/misc/tabledrag.js', array('weight' => -1)); + $js_added = TRUE; + } + + // If a subgroup or source isn't set, assume it is the same as the group. + $target = isset($subgroup) ? $subgroup : $group; + $source = isset($source) ? $source : $target; + $settings['tableDrag'][$table_id][$group][] = array( + 'target' => $target, + 'source' => $source, + 'relationship' => $relationship, + 'action' => $action, + 'hidden' => $hidden, + 'limit' => $limit, + ); + drupal_add_js($settings, 'setting'); +} + +/** + * Aggregates JavaScript files into a cache file in the files directory. + * + * The file name for the JavaScript cache file is generated from the hash of + * the aggregated contents of the files in $files. This forces proxies and + * browsers to download new JavaScript when the JavaScript 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 names in + * $files 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 $files 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 $files + * An array of JavaScript files to aggregate and compress into one file. + * + * @return + * The URI of the cache file, or FALSE if the file could not be saved. + */ +function drupal_build_js_cache($files) { + $contents = ''; + $uri = ''; + $map = variable_get('drupal_js_cache_files', array()); + $key = hash('sha256', serialize($files)); + if (isset($map[$key])) { + $uri = $map[$key]; + } + + if (empty($uri) || !file_exists($uri)) { + // Build aggregate JS file. + foreach ($files as $path => $info) { + if ($info['preprocess']) { + // Append a ';' and a newline after each JS file to prevent them from running together. + $contents .= file_get_contents($path) . ";\n"; + } + } + // Prefix filename to prevent blocking by firewalls which reject files + // starting with "ad*". + $filename = 'js_' . drupal_hash_base64($contents) . '.js'; + // Create the js/ within the files folder. + $jspath = 'public://js'; + $uri = $jspath . '/' . $filename; + // Create the JS file. + file_prepare_directory($jspath, FILE_CREATE_DIRECTORY); + if (!file_exists($uri) && !file_unmanaged_save_data($contents, $uri, FILE_EXISTS_REPLACE)) { + return FALSE; + } + // If JS 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('js_gzip_compression', TRUE) && variable_get('clean_url', 0) && extension_loaded('zlib')) { + if (!file_exists($uri . '.gz') && !file_unmanaged_save_data(gzencode($contents, 9, FORCE_GZIP), $uri . '.gz', FILE_EXISTS_REPLACE)) { + return FALSE; + } + } + $map[$key] = $uri; + variable_set('drupal_js_cache_files', $map); + } + return $uri; +} + +/** + * Deletes old cached JavaScript files and variables. + */ +function drupal_clear_js_cache() { + variable_del('javascript_parsed'); + variable_del('drupal_js_cache_files'); + file_scan_directory('public://js', '/.*/', array('callback' => 'drupal_delete_file_if_stale')); +} + +/** + * Converts a PHP variable into its JavaScript equivalent. + * + * We use HTML-safe strings, i.e. with <, > and & escaped. + * + * @see drupal_json_decode() + * @ingroup php_wrappers + */ +function drupal_json_encode($var) { + // json_encode() does not escape <, > and &, so we do it with str_replace(). + return str_replace(array('<', '>', '&'), array('\u003c', '\u003e', '\u0026'), json_encode($var)); +} + +/** + * Converts an HTML-safe JSON string into its PHP equivalent. + * + * @see drupal_json_encode() + * @ingroup php_wrappers + */ +function drupal_json_decode($var) { + return json_decode($var, TRUE); +} + +/** + * Return data in JSON format. + * + * This function should be used for JavaScript callback functions returning + * data in JSON format. It sets the header for JavaScript output. + * + * @param $var + * (optional) If set, the variable will be converted to JSON and output. + */ +function drupal_json_output($var = NULL) { + // We are returning JSON, so tell the browser. + drupal_add_http_header('Content-Type', 'application/json'); + + if (isset($var)) { + echo drupal_json_encode($var); + } +} + +/** + * Get 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; +} + +/** + * Ensure the private key variable used to generate tokens is set. + * + * @return + * The private key. + */ +function drupal_get_private_key() { + if (!($key = variable_get('drupal_private_key', 0))) { + $key = drupal_hash_base64(drupal_random_bytes(55)); + variable_set('drupal_private_key', $key); + } + return $key; +} + +/** + * Generate a token based on $value, the current user session and private key. + * + * @param $value + * An additional value to base the token on. + */ +function drupal_get_token($value = '') { + return drupal_hmac_base64($value, session_id() . drupal_get_private_key() . drupal_get_hash_salt()); +} + +/** + * Validate a token based on $value, the current user session and private key. + * + * @param $token + * The token to be validated. + * @param $value + * An additional value to base the token on. + * @param $skip_anonymous + * Set to true to skip token validation for anonymous users. + * @return + * True for a valid token, false for an invalid token. When $skip_anonymous + * is true, the return value will always be true for anonymous users. + */ +function drupal_valid_token($token, $value = '', $skip_anonymous = FALSE) { + global $user; + return (($skip_anonymous && $user->uid == 0) || ($token == drupal_get_token($value))); +} + +function _drupal_bootstrap_full() { + static $called = FALSE; + + if ($called) { + return; + } + $called = TRUE; + require_once DRUPAL_ROOT . '/' . variable_get('path_inc', 'core/includes/path.inc'); + require_once DRUPAL_ROOT . '/core/includes/theme.inc'; + require_once DRUPAL_ROOT . '/core/includes/pager.inc'; + require_once DRUPAL_ROOT . '/' . variable_get('menu_inc', 'core/includes/menu.inc'); + require_once DRUPAL_ROOT . '/core/includes/tablesort.inc'; + require_once DRUPAL_ROOT . '/core/includes/file.inc'; + require_once DRUPAL_ROOT . '/core/includes/unicode.inc'; + require_once DRUPAL_ROOT . '/core/includes/image.inc'; + require_once DRUPAL_ROOT . '/core/includes/form.inc'; + require_once DRUPAL_ROOT . '/core/includes/mail.inc'; + require_once DRUPAL_ROOT . '/core/includes/actions.inc'; + require_once DRUPAL_ROOT . '/core/includes/ajax.inc'; + require_once DRUPAL_ROOT . '/core/includes/token.inc'; + require_once DRUPAL_ROOT . '/core/includes/errors.inc'; + + // Detect string handling method + unicode_check(); + // Undo magic quotes + fix_gpc_magic(); + // Load all enabled modules + module_load_all(); + // Make sure all stream wrappers are registered. + file_get_stream_wrappers(); + + $test_info = &$GLOBALS['drupal_test_info']; + if (!empty($test_info['in_child_site'])) { + // Running inside the simpletest child site, log fatal errors to test + // specific file directory. + ini_set('log_errors', 1); + ini_set('error_log', 'public://error.log'); + } + + // Initialize $_GET['q'] prior to invoking hook_init(). + drupal_path_initialize(); + + // Let all modules take action before the menu system handles the request. + // We do not want this while running update.php. + if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') { + // Prior to invoking hook_init(), initialize the theme (potentially a custom + // one for this page), so that: + // - Modules with hook_init() implementations that call theme() or + // theme_get_registry() don't initialize the incorrect theme. + // - The theme can have hook_*_alter() implementations affect page building + // (e.g., hook_form_alter(), hook_node_view_alter(), hook_page_alter()), + // ahead of when rendering starts. + menu_set_custom_theme(); + drupal_theme_initialize(); + module_invoke_all('init'); + } +} + +/** + * Store the current page in the cache. + * + * If page_compression is enabled, a gzipped version of the page is stored in + * the cache to avoid compressing the output on each request. The cache entry + * is unzipped in the relatively rare event that the page is requested by a + * client without gzip support. + * + * Page compression requires the PHP zlib extension + * (http://php.net/manual/en/ref.zlib.php). + * + * @see drupal_page_header() + */ +function drupal_page_set_cache() { + global $base_root; + + if (drupal_page_is_cacheable()) { + $cache = (object) array( + 'cid' => $base_root . request_uri(), + 'data' => array( + 'path' => $_GET['q'], + 'body' => ob_get_clean(), + 'title' => drupal_get_title(), + 'headers' => array(), + ), + 'expire' => CACHE_TEMPORARY, + 'created' => REQUEST_TIME, + ); + + // Restore preferred header names based on the lower-case names returned + // by drupal_get_http_header(). + $header_names = _drupal_set_preferred_header_name(); + foreach (drupal_get_http_header() as $name_lower => $value) { + $cache->data['headers'][$header_names[$name_lower]] = $value; + if ($name_lower == 'expires') { + // Use the actual timestamp from an Expires header if available. + $cache->expire = strtotime($value); + } + } + + if ($cache->data['body']) { + if (variable_get('page_compression', TRUE) && extension_loaded('zlib')) { + $cache->data['body'] = gzencode($cache->data['body'], 9, FORCE_GZIP); + } + cache_set($cache->cid, $cache->data, 'cache_page', $cache->expire); + } + return $cache; + } +} + +/** + * Executes a cron run when called. + * + * Do not call this function from test, use $this->cronRun() instead. + * + * @return + * Returns TRUE if ran successfully + */ +function drupal_cron_run() { + // Allow execution to continue even if the request gets canceled. + @ignore_user_abort(TRUE); + + // Prevent session information from being saved while cron is running. + drupal_save_session(FALSE); + + // Force the current user to anonymous to ensure consistent permissions on + // cron runs. + $original_user = $GLOBALS['user']; + $GLOBALS['user'] = drupal_anonymous_user(); + + // Try to allocate enough time to run all the hook_cron implementations. + drupal_set_time_limit(240); + + $return = FALSE; + // Grab the defined cron queues. + $queues = module_invoke_all('cron_queue_info'); + drupal_alter('cron_queue_info', $queues); + + // Try to acquire cron lock. + if (!lock_acquire('cron', 240.0)) { + // Cron is still running normally. + watchdog('cron', 'Attempting to re-run cron while it is already running.', array(), LOG_WARNING); + } + else { + // Make sure every queue exists. There is no harm in trying to recreate an + // existing queue. + foreach ($queues as $queue_name => $info) { + DrupalQueue::get($queue_name)->createQueue(); + } + // Register shutdown callback + drupal_register_shutdown_function('drupal_cron_cleanup'); + + // Iterate through the modules calling their cron handlers (if any): + module_invoke_all('cron'); + + // Record cron time + variable_set('cron_last', REQUEST_TIME); + watchdog('cron', 'Cron run completed.', array(), LOG_NOTICE); + + // Release cron lock. + lock_release('cron'); + + // Return TRUE so other functions can check if it did run successfully + $return = TRUE; + } + + foreach ($queues as $queue_name => $info) { + $function = $info['worker callback']; + $end = time() + (isset($info['time']) ? $info['time'] : 15); + $queue = DrupalQueue::get($queue_name); + while (time() < $end && ($item = $queue->claimItem())) { + $function($item->data); + $queue->deleteItem($item); + } + } + // Restore the user. + $GLOBALS['user'] = $original_user; + drupal_save_session(TRUE); + + return $return; +} + +/** + * Shutdown function for cron cleanup. + */ +function drupal_cron_cleanup() { + // See if the semaphore is still locked. + if (variable_get('cron_semaphore', FALSE)) { + watchdog('cron', 'Cron run exceeded the time limit and was aborted.', array(), LOG_WARNING); + + // Release cron semaphore + variable_del('cron_semaphore'); + } +} + +/** + * Returns information about system object files (modules, themes, etc.). + * + * This function is used to find all or some system object files (module files, + * theme files, etc.) that exist on the site. It searches in several locations, + * depending on what type of object you are looking for. For instance, if you + * are looking for modules and call: + * @code + * drupal_system_listing("/\.module$/", "modules", 'name', 0); + * @endcode + * this function will search the site-wide modules directory (i.e., /modules/), + * your install profile's directory (i.e., + * /profiles/your_site_profile/modules/), the all-sites directory (i.e., + * /sites/all/modules/), and your site-specific directory (i.e., + * /sites/your_site_dir/modules/), in that order, and return information about + * all of the files ending in .module in those directories. + * + * The information is returned in an associative array, which can be keyed on + * the file name ($key = 'filename'), the file name without the extension ($key + * = 'name'), or the full file stream URI ($key = 'uri'). If you use a key of + * 'filename' or 'name', files found later in the search will take precedence + * over files found earlier (unless they belong to a module or theme not + * compatible with Drupal core); if you choose a key of 'uri', you will get all + * files found. + * + * @param string $mask + * The preg_match() regular expression for the files to find. + * @param string $directory + * The subdirectory name in which the files are found. For example, + * 'core/modules' will search in sub-directories of the /core/modules + * directory, sub-directories of /sites/all/modules/, etc. + * @param string $key + * The key to be used for the associative array returned. Possible values are + * 'uri', for the file's URI; 'filename', for the basename of the file; and + * 'name' for the name of the file without the extension. If you choose 'name' + * or 'filename', only the highest-precedence file will be returned. + * @param int $min_depth + * Minimum depth of directories to return files from, relative to each + * directory searched. For instance, a minimum depth of 2 would find modules + * inside /core/modules/node/tests, but not modules directly in + * /core/modules/node. + * + * @return array + * An associative array of file objects, keyed on the chosen key. Each element + * in the array is an object containing file information, with properties: + * - 'uri': Full URI of the file. + * - 'filename': File name. + * - 'name': Name of file without the extension. + */ +function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1) { + $config = conf_path(); + $files = array(); + + // Search for the directory in core. + $searchdir = array('core/' . $directory); + + // The 'profiles' directory contains pristine collections of modules and + // themes as organized by a distribution. It is pristine in the same way + // that /modules is pristine for core; users should avoid changing anything + // there in favor of sites/all or sites/ directories. + $profile = drupal_get_profile(); + if (file_exists("profiles/$profile/$directory")) { + $searchdir[] = "profiles/$profile/$directory"; + } + + // Always search sites/all/* as well as the global directories + $searchdir[] = 'sites/all/' . $directory; + + if (file_exists("$config/$directory")) { + $searchdir[] = "$config/$directory"; + } + + // Get current list of items + if (!function_exists('file_scan_directory')) { + require_once DRUPAL_ROOT . '/core/includes/file.inc'; + } + foreach ($searchdir as $dir) { + $files_to_add = file_scan_directory($dir, $mask, array('key' => $key, 'min_depth' => $min_depth)); + + // Duplicate files found in later search directories take precedence over + // earlier ones, so we want them to overwrite keys in our resulting + // $files array. + // The exception to this is if the later file is from a module or theme not + // compatible with Drupal core. This may occur during upgrades of Drupal + // core when new modules exist in core while older contrib modules with the + // same name exist in a directory such as sites/all/modules/. + foreach (array_intersect_key($files_to_add, $files) as $file_key => $file) { + // If it has no info file, then we just behave liberally and accept the + // new resource on the list for merging. + if (file_exists($info_file = dirname($file->uri) . '/' . $file->name . '.info')) { + // Get the .info file for the module or theme this file belongs to. + $info = drupal_parse_info_file($info_file); + + // If the module or theme is incompatible with Drupal core, remove it + // from the array for the current search directory, so it is not + // overwritten when merged with the $files array. + if (isset($info['core']) && $info['core'] != DRUPAL_CORE_COMPATIBILITY) { + unset($files_to_add[$file_key]); + } + } + } + $files = array_merge($files, $files_to_add); + } + + return $files; +} + +/** + * Set the main page content value for later use. + * + * Given the nature of the Drupal page handling, this will be called once with + * a string or array. We store that and return it later as the block is being + * displayed. + * + * @param $content + * A string or renderable array representing the body of the page. + * @return + * If called without $content, a renderable array representing the body of + * the page. + */ +function drupal_set_page_content($content = NULL) { + $content_block = &drupal_static(__FUNCTION__, NULL); + $main_content_display = &drupal_static('system_main_content_added', FALSE); + + if (!empty($content)) { + $content_block = (is_array($content) ? $content : array('main' => array('#markup' => $content))); + } + else { + // Indicate that the main content has been requested. We assume that + // the module requesting the content will be adding it to the page. + // A module can indicate that it does not handle the content by setting + // the static variable back to FALSE after calling this function. + $main_content_display = TRUE; + return $content_block; + } +} + +/** + * #pre_render callback to render #browsers into #prefix and #suffix. + * + * @param $elements + * A render array with a '#browsers' property. The '#browsers' property can + * contain any or all of the following keys: + * - 'IE': If FALSE, the element is not rendered by Internet Explorer. If + * TRUE, the element is rendered by Internet Explorer. Can also be a string + * containing an expression for Internet Explorer to evaluate as part of a + * conditional comment. For example, this can be set to 'lt IE 7' for the + * element to be rendered in Internet Explorer 6, but not in Internet + * Explorer 7 or higher. Defaults to TRUE. + * - '!IE': If FALSE, the element is not rendered by browsers other than + * Internet Explorer. If TRUE, the element is rendered by those browsers. + * Defaults to TRUE. + * Examples: + * - To render an element in all browsers, '#browsers' can be left out or set + * to array('IE' => TRUE, '!IE' => TRUE). + * - To render an element in Internet Explorer only, '#browsers' can be set + * to array('!IE' => FALSE). + * - To render an element in Internet Explorer 6 only, '#browsers' can be set + * to array('IE' => 'lt IE 7', '!IE' => FALSE). + * - To render an element in Internet Explorer 8 and higher and in all other + * browsers, '#browsers' can be set to array('IE' => 'gte IE 8'). + * + * @return + * The passed-in element with markup for conditional comments potentially + * added to '#prefix' and '#suffix'. + */ +function drupal_pre_render_conditional_comments($elements) { + $browsers = isset($elements['#browsers']) ? $elements['#browsers'] : array(); + $browsers += array( + 'IE' => TRUE, + '!IE' => TRUE, + ); + + // If rendering in all browsers, no need for conditional comments. + if ($browsers['IE'] === TRUE && $browsers['!IE']) { + return $elements; + } + + // Determine the conditional comment expression for Internet Explorer to + // evaluate. + if ($browsers['IE'] === TRUE) { + $expression = 'IE'; + } + elseif ($browsers['IE'] === FALSE) { + $expression = '!IE'; + } + else { + $expression = $browsers['IE']; + } + + // Wrap the element's potentially existing #prefix and #suffix properties with + // conditional comment markup. The conditional comment expression is evaluated + // by Internet Explorer only. To control the rendering by other browsers, + // either the "downlevel-hidden" or "downlevel-revealed" technique must be + // used. See http://en.wikipedia.org/wiki/Conditional_comment for details. + $elements += array( + '#prefix' => '', + '#suffix' => '', + ); + if (!$browsers['!IE']) { + // "downlevel-hidden". + $elements['#prefix'] = "\n\n"; + } + else { + // "downlevel-revealed". + $elements['#prefix'] = "\n\n" . $elements['#prefix']; + $elements['#suffix'] .= "\n"; + } + + return $elements; +} + +/** + * #pre_render callback to render a link into #markup. + * + * Doing so during pre_render gives modules a chance to alter the link parts. + * + * @param $elements + * A structured array whose keys form the arguments to l(): + * - #title: The link text to pass as argument to l(). + * - #href: The URL path component to pass as argument to l(). + * - #options: (optional) An array of options to pass to l(). + * + * @return + * The passed-in elements containing a rendered link in '#markup'. + */ +function drupal_pre_render_link($element) { + // By default, link options to pass to l() are normally set in #options. + $element += array('#options' => array()); + // However, within the scope of renderable elements, #attributes is a valid + // way to specify attributes, too. Take them into account, but do not override + // attributes from #options. + if (isset($element['#attributes'])) { + $element['#options'] += array('attributes' => array()); + $element['#options']['attributes'] += $element['#attributes']; + } + + // This #pre_render callback can be invoked from inside or outside of a Form + // API context, and depending on that, a HTML ID may be already set in + // different locations. #options should have precedence over Form API's #id. + // #attributes have been taken over into #options above already. + if (isset($element['#options']['attributes']['id'])) { + $element['#id'] = $element['#options']['attributes']['id']; + } + elseif (isset($element['#id'])) { + $element['#options']['attributes']['id'] = $element['#id']; + } + + // Conditionally invoke ajax_pre_render_element(), if #ajax is set. + if (isset($element['#ajax']) && !isset($element['#ajax_processed'])) { + // If no HTML ID was found above, automatically create one. + if (!isset($element['#id'])) { + $element['#id'] = $element['#options']['attributes']['id'] = drupal_html_id('ajax-link'); + } + // If #ajax['path] was not specified, use the href as Ajax request URL. + if (!isset($element['#ajax']['path'])) { + $element['#ajax']['path'] = $element['#href']; + $element['#ajax']['options'] = $element['#options']; + } + $element = ajax_pre_render_element($element); + } + + $element['#markup'] = l($element['#title'], $element['#href'], $element['#options']); + return $element; +} + +/** + * #pre_render callback that collects child links into a single array. + * + * This function can be added as a pre_render callback for a renderable array, + * usually one which will be themed by theme_links(). It iterates through all + * unrendered children of the element, collects any #links properties it finds, + * merges them into the parent element's #links array, and prevents those + * children from being rendered separately. + * + * The purpose of this is to allow links to be logically grouped into related + * categories, so that each child group can be rendered as its own list of + * links if drupal_render() is called on it, but calling drupal_render() on the + * parent element will still produce a single list containing all the remaining + * links, regardless of what group they were in. + * + * A typical example comes from node links, which are stored in a renderable + * array similar to this: + * @code + * $node->content['links'] = array( + * '#theme' => 'links__node', + * '#pre_render' = array('drupal_pre_render_links'), + * 'comment' => array( + * '#theme' => 'links__node__comment', + * '#links' => array( + * // An array of links associated with node comments, suitable for + * // passing in to theme_links(). + * ), + * ), + * 'statistics' => array( + * '#theme' => 'links__node__statistics', + * '#links' => array( + * // An array of links associated with node statistics, suitable for + * // passing in to theme_links(). + * ), + * ), + * 'translation' => array( + * '#theme' => 'links__node__translation', + * '#links' => array( + * // An array of links associated with node translation, suitable for + * // passing in to theme_links(). + * ), + * ), + * ); + * @endcode + * + * In this example, the links are grouped by functionality, which can be + * helpful to themers who want to display certain kinds of links independently. + * For example, adding this code to node.tpl.php will result in the comment + * links being rendered as a single list: + * @code + * print render($content['links']['comment']); + * @endcode + * + * (where $node->content has been transformed into $content before handing + * control to the node.tpl.php template). + * + * The pre_render function defined here allows the above flexibility, but also + * allows the following code to be used to render all remaining links into a + * single list, regardless of their group: + * @code + * print render($content['links']); + * @endcode + * + * In the above example, this will result in the statistics and translation + * links being rendered together in a single list (but not the comment links, + * which were rendered previously on their own). + * + * Because of the way this function works, the individual properties of each + * group (for example, a group-specific #theme property such as + * 'links__node__comment' in the example above, or any other property such as + * #attributes or #pre_render that is attached to it) are only used when that + * group is rendered on its own. When the group is rendered together with other + * children, these child-specific properties are ignored, and only the overall + * properties of the parent are used. + */ +function drupal_pre_render_links($element) { + $element += array('#links' => array()); + foreach (element_children($element) as $key) { + $child = &$element[$key]; + // If the child has links which have not been printed yet and the user has + // access to it, merge its links in to the parent. + if (isset($child['#links']) && empty($child['#printed']) && (!isset($child['#access']) || $child['#access'])) { + $element['#links'] += $child['#links']; + // Mark the child as having been printed already (so that its links + // cannot be mistakenly rendered twice). + $child['#printed'] = TRUE; + } + } + return $element; +} + +/** + * #pre_render callback to append contents in #markup to #children. + * + * This needs to be a #pre_render callback, because eventually assigned + * #theme_wrappers will expect the element's rendered content in #children. + * Note that if also a #theme is defined for the element, then the result of + * the theme callback will override #children. + * + * @see drupal_render() + * + * @param $elements + * A structured array using the #markup key. + * + * @return + * The passed-in elements, but #markup appended to #children. + */ +function drupal_pre_render_markup($elements) { + $elements['#children'] = $elements['#markup']; + return $elements; +} + +/** + * Renders the page, including all theming. + * + * @param $page + * A string or array representing the content of a page. The array consists of + * the following keys: + * - #type: Value is always 'page'. This pushes the theming through page.tpl.php (required). + * - #show_messages: Suppress drupal_get_message() items. Used by Batch API (optional). + * + * @see hook_page_alter() + * @see element_info() + */ +function drupal_render_page($page) { + $main_content_display = &drupal_static('system_main_content_added', FALSE); + + // Allow menu callbacks to return strings or arbitrary arrays to render. + // If the array returned is not of #type page directly, we need to fill + // in the page with defaults. + if (is_string($page) || (is_array($page) && (!isset($page['#type']) || ($page['#type'] != 'page')))) { + drupal_set_page_content($page); + $page = element_info('page'); + } + + // Modules can add elements to $page as needed in hook_page_build(). + foreach (module_implements('page_build') as $module) { + $function = $module . '_page_build'; + $function($page); + } + // Modules alter the $page as needed. Blocks are populated into regions like + // 'sidebar_first', 'footer', etc. + drupal_alter('page', $page); + + // If no module has taken care of the main content, add it to the page now. + // This allows the site to still be usable even if no modules that + // control page regions (for example, the Block module) are enabled. + if (!$main_content_display) { + $page['content']['system_main'] = drupal_set_page_content(); + } + + return drupal_render($page); +} + +/** + * Renders HTML given a structured array tree. + * + * Recursively iterates over each of the array elements, generating HTML code. + * + * HTML generation is controlled by two properties containing theme functions, + * #theme and #theme_wrappers. + * + * #theme is the theme function called first. If it is set and the element has + * any children, they have to be rendered there. For elements that are not + * allowed to have any children, e.g. buttons or textfields, it can be used to + * render the element itself. If #theme is not present and the element has + * children, they are rendered and concatenated into a string by + * drupal_render_children(). + * + * The #theme_wrappers property contains an array of theme functions which will + * be called, in order, after #theme has run. These can be used to add further + * markup around the rendered children; e.g., fieldsets add the required markup + * for a fieldset around their rendered child elements. All wrapper theme + * functions have to include the element's #children property in their output, + * as it contains the output of the previous theme functions and the rendered + * children. + * + * For example, for the form element type, by default only the #theme_wrappers + * property is set, which adds the form markup around the rendered child + * elements of the form. This allows you to set the #theme property on a + * specific form to a custom theme function, giving you complete control over + * the placement of the form's children while not at all having to deal with + * the form markup itself. + * + * drupal_render() can optionally cache the rendered output of elements to + * improve performance. To use drupal_render() caching, set the element's #cache + * property to an associative array with one or several of the following keys: + * - 'keys': An array of one or more keys that identify the element. If 'keys' + * is set, the cache ID is created automatically from these keys. See + * drupal_render_cid_create(). + * - 'granularity' (optional): Define the cache granularity using binary + * combinations of the cache granularity constants, e.g. DRUPAL_CACHE_PER_USER + * to cache for each user separately or + * DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE to cache separately for each + * page and role. If not specified the element is cached globally for each + * theme and language. + * - 'cid': Specify the cache ID directly. Either 'keys' or 'cid' is required. + * If 'cid' is set, 'keys' and 'granularity' are ignored. Use only if you + * have special requirements. + * - 'expire': Set to one of the cache lifetime constants. + * - 'bin': Specify a cache bin to cache the element in. Defaults to 'cache'. + * + * This function is usually called from within another function, like + * drupal_get_form() or a theme function. Elements are sorted internally + * using uasort(). Since this is expensive, when passing already sorted + * elements to drupal_render(), for example from a database query, set + * $elements['#sorted'] = TRUE to avoid sorting them a second time. + * + * drupal_render() flags each element with a '#printed' status to indicate that + * the element has been rendered, which allows individual elements of a given + * array to be rendered independently and prevents them from being rendered + * more than once on subsequent calls to drupal_render() (e.g., as part of a + * larger array). If the same array or array element is passed more than once + * to drupal_render(), it simply returns a NULL value. + * + * @param $elements + * The structured array describing the data to be rendered. + * @return + * The rendered HTML. + */ +function drupal_render(&$elements) { + // Early-return nothing if user does not have access. + if (empty($elements) || (isset($elements['#access']) && !$elements['#access'])) { + return; + } + + // Do not print elements twice. + if (!empty($elements['#printed'])) { + return; + } + + // Try to fetch the element's markup from cache and return. + if (isset($elements['#cache']) && $cached_output = drupal_render_cache_get($elements)) { + return $cached_output; + } + + // If #markup is set, ensure #type is set. This allows to specify just #markup + // on an element without setting #type. + if (isset($elements['#markup']) && !isset($elements['#type'])) { + $elements['#type'] = 'markup'; + } + + // If the default values for this element have not been loaded yet, populate + // them. + if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { + $elements += element_info($elements['#type']); + } + + // Make any final changes to the element before it is rendered. This means + // that the $element or the children can be altered or corrected before the + // element is rendered into the final text. + if (isset($elements['#pre_render'])) { + foreach ($elements['#pre_render'] as $function) { + if (function_exists($function)) { + $elements = $function($elements); + } + } + } + + // Allow #pre_render to abort rendering. + if (!empty($elements['#printed'])) { + return; + } + + // Get the children of the element, sorted by weight. + $children = element_children($elements, TRUE); + + // Initialize this element's #children, unless a #pre_render callback already + // preset #children. + if (!isset($elements['#children'])) { + $elements['#children'] = ''; + } + // Call the element's #theme function if it is set. Then any children of the + // element have to be rendered there. + if (isset($elements['#theme'])) { + $elements['#children'] = theme($elements['#theme'], $elements); + } + // If #theme was not set and the element has children, render them now. + // This is the same process as drupal_render_children() but is inlined + // for speed. + if ($elements['#children'] == '') { + foreach ($children as $key) { + $elements['#children'] .= drupal_render($elements[$key]); + } + } + + // Let the theme functions in #theme_wrappers add markup around the rendered + // children. + if (isset($elements['#theme_wrappers'])) { + foreach ($elements['#theme_wrappers'] as $theme_wrapper) { + $elements['#children'] = theme($theme_wrapper, $elements); + } + } + + // Filter the outputted content and make any last changes before the + // content is sent to the browser. The changes are made on $content + // which allows the output'ed text to be filtered. + if (isset($elements['#post_render'])) { + foreach ($elements['#post_render'] as $function) { + if (function_exists($function)) { + $elements['#children'] = $function($elements['#children'], $elements); + } + } + } + + // Add any JavaScript state information associated with the element. + if (!empty($elements['#states'])) { + drupal_process_states($elements); + } + + // Add additional libraries, CSS, JavaScript an other custom + // attached data associated with this element. + if (!empty($elements['#attached'])) { + drupal_process_attached($elements); + } + + $prefix = isset($elements['#prefix']) ? $elements['#prefix'] : ''; + $suffix = isset($elements['#suffix']) ? $elements['#suffix'] : ''; + $output = $prefix . $elements['#children'] . $suffix; + + // Cache the processed element if #cache is set. + if (isset($elements['#cache'])) { + drupal_render_cache_set($output, $elements); + } + + $elements['#printed'] = TRUE; + return $output; +} + +/** + * Render children of an element and concatenate them. + * + * This renders all children of an element using drupal_render() and then + * joins them together into a single string. + * + * @param $element + * The structured array whose children shall be rendered. + * @param $children_keys + * If the keys of the element's children are already known, they can be passed + * in to save another run of element_children(). + */ +function drupal_render_children(&$element, $children_keys = NULL) { + if ($children_keys === NULL) { + $children_keys = element_children($element); + } + $output = ''; + foreach ($children_keys as $key) { + if (!empty($element[$key])) { + $output .= drupal_render($element[$key]); + } + } + return $output; +} + +/** + * Render an element. + * + * This function renders an element using drupal_render(). The top level + * element is shown with show() before rendering, so it will always be rendered + * even if hide() had been previously used on it. + * + * @param $element + * The element to be rendered. + * + * @return + * The rendered element. + * + * @see drupal_render() + * @see show() + * @see hide() + */ +function render(&$element) { + if (is_array($element)) { + show($element); + return drupal_render($element); + } + else { + // Safe-guard for inappropriate use of render() on flat variables: return + // the variable as-is. + return $element; + } +} + +/** + * Hide an element from later rendering. + * + * The first time render() or drupal_render() is called on an element tree, + * as each element in the tree is rendered, it is marked with a #printed flag + * and the rendered children of the element are cached. Subsequent calls to + * render() or drupal_render() will not traverse the child tree of this element + * again: they will just use the cached children. So if you want to hide an + * element, be sure to call hide() on the element before its parent tree is + * rendered for the first time, as it will have no effect on subsequent + * renderings of the parent tree. + * + * @param $element + * The element to be hidden. + * + * @return + * The element. + * + * @see render() + * @see show() + */ +function hide(&$element) { + $element['#printed'] = TRUE; + return $element; +} + +/** + * Show a hidden element for later rendering. + * + * You can also use render($element), which shows the element while rendering + * it. + * + * The first time render() or drupal_render() is called on an element tree, + * as each element in the tree is rendered, it is marked with a #printed flag + * and the rendered children of the element are cached. Subsequent calls to + * render() or drupal_render() will not traverse the child tree of this element + * again: they will just use the cached children. So if you want to show an + * element, be sure to call show() on the element before its parent tree is + * rendered for the first time, as it will have no effect on subsequent + * renderings of the parent tree. + * + * @param $element + * The element to be shown. + * + * @return + * The element. + * + * @see render() + * @see hide() + */ +function show(&$element) { + $element['#printed'] = FALSE; + return $element; +} + +/** + * Get the rendered output of a renderable element from cache. + * + * @see drupal_render() + * @see drupal_render_cache_set() + * + * @param $elements + * A renderable array. + * @return + * A markup string containing the rendered content of the element, or FALSE + * if no cached copy of the element is available. + */ +function drupal_render_cache_get($elements) { + if (!in_array($_SERVER['REQUEST_METHOD'], array('GET', 'HEAD')) || !$cid = drupal_render_cid_create($elements)) { + return FALSE; + } + $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'cache'; + + if (!empty($cid) && $cache = cache_get($cid, $bin)) { + // Add additional libraries, JavaScript, CSS and other data attached + // to this element. + if (isset($cache->data['#attached'])) { + drupal_process_attached($cache->data); + } + // Return the rendered output. + return $cache->data['#markup']; + } + return FALSE; +} + +/** + * Cache the rendered output of a renderable element. + * + * This is called by drupal_render() if the #cache property is set on an element. + * + * @see drupal_render() + * @see drupal_render_cache_get() + * + * @param $markup + * The rendered output string of $elements. + * @param $elements + * A renderable array. + */ +function drupal_render_cache_set(&$markup, $elements) { + // Create the cache ID for the element. + if (!in_array($_SERVER['REQUEST_METHOD'], array('GET', 'HEAD')) || !$cid = drupal_render_cid_create($elements)) { + return FALSE; + } + + // Cache implementations are allowed to modify the markup, to support + // replacing markup with edge-side include commands. The supporting cache + // backend will store the markup in some other key (like + // $data['#real-value']) and return an include command instead. When the + // ESI command is executed by the content accelerator, the real value can + // be retrieved and used. + $data['#markup'] = &$markup; + // Persist attached data associated with this element. + $attached = drupal_render_collect_attached($elements, TRUE); + if ($attached) { + $data['#attached'] = $attached; + } + $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'cache'; + $expire = isset($elements['#cache']['expire']) ? $elements['#cache']['expire'] : CACHE_PERMANENT; + cache_set($cid, $data, $bin, $expire); +} + +/** + * Collect #attached for an element and all child elements into a single array. + * + * When caching elements, it is necessary to collect all libraries, JavaScript + * and CSS into a single array, from both the element itself and all child + * elements. This allows drupal_render() to add these back to the page when the + * element is returned from cache. + * + * @param $elements + * The element to collect #attached from. + * @param $return + * Whether to return the attached elements and reset the internal static. + * + * @return + * The #attached array for this element and its descendants. + */ +function drupal_render_collect_attached($elements, $return = FALSE) { + $attached = &drupal_static(__FUNCTION__, array()); + + // Collect all #attached for this element. + if (isset($elements['#attached'])) { + foreach ($elements['#attached'] as $key => $value) { + if (!isset($attached[$key])) { + $attached[$key] = array(); + } + $attached[$key] = array_merge($attached[$key], $value); + } + } + if ($children = element_children($elements)) { + foreach ($children as $child) { + drupal_render_collect_attached($elements[$child]); + } + } + + // If this was the first call to the function, return all attached elements + // and reset the static cache. + if ($return) { + $return = $attached; + $attached = array(); + return $return; + } +} + +/** + * Prepare an element for caching based on a query. This smart caching strategy + * saves Drupal from querying and rendering to HTML when the underlying query is + * unchanged. + * + * Expensive queries should use the query builder to create the query and then + * call this function. Executing the query and formatting results should happen + * in a #pre_render callback. + * + * @param $query + * A select query object as returned by db_select(). + * @param $function + * The name of the function doing this caching. A _pre_render suffix will be + * added to this string and is also part of the cache key in + * drupal_render_cache_set() and drupal_render_cache_get(). + * @param $expire + * The cache expire time, passed eventually to cache_set(). + * @param $granularity + * One or more granularity constants passed to drupal_render_cid_parts(). + * + * @return + * A renderable array with the following keys and values: + * - #query: The passed-in $query. + * - #pre_render: $function with a _pre_render suffix. + * - #cache: An associative array prepared for drupal_render_cache_set(). + */ +function drupal_render_cache_by_query($query, $function, $expire = CACHE_TEMPORARY, $granularity = NULL) { + $cache_keys = array_merge(array($function), drupal_render_cid_parts($granularity)); + $query->preExecute(); + $cache_keys[] = hash('sha256', serialize(array((string) $query, $query->getArguments()))); + return array( + '#query' => $query, + '#pre_render' => array($function . '_pre_render'), + '#cache' => array( + 'keys' => $cache_keys, + 'expire' => $expire, + ), + ); +} + +/** + * Helper function for building cache ids. + * + * @param $granularity + * One or more cache granularity constants, e.g. DRUPAL_CACHE_PER_USER to cache + * for each user separately or DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE to + * cache separately for each page and role. + * + * @return + * An array of cache ID parts, always containing the active theme. If the + * locale module is enabled it also contains the active language. If + * $granularity was passed in, more parts are added. + */ +function drupal_render_cid_parts($granularity = NULL) { + global $theme, $base_root, $user; + + $cid_parts[] = $theme; + // If Locale is enabled but we have only one language we do not need it as cid + // part. + if (drupal_multilingual()) { + foreach (language_types_configurable() as $language_type) { + $cid_parts[] = $GLOBALS[$language_type]->language; + } + } + + if (!empty($granularity)) { + // 'PER_ROLE' and 'PER_USER' are mutually exclusive. 'PER_USER' can be a + // resource drag for sites with many users, so when a module is being + // equivocal, we favor the less expensive 'PER_ROLE' pattern. + if ($granularity & DRUPAL_CACHE_PER_ROLE) { + $cid_parts[] = 'r.' . implode(',', array_keys($user->roles)); + } + elseif ($granularity & DRUPAL_CACHE_PER_USER) { + $cid_parts[] = "u.$user->uid"; + } + + if ($granularity & DRUPAL_CACHE_PER_PAGE) { + $cid_parts[] = $base_root . request_uri(); + } + } + + return $cid_parts; +} + +/** + * Create the cache ID for a renderable element. + * + * This creates the cache ID string, either by returning the #cache['cid'] + * property if present or by building the cache ID out of the #cache['keys'] + * and, optionally, the #cache['granularity'] properties. + * + * @param $elements + * A renderable array. + * + * @return + * The cache ID string, or FALSE if the element may not be cached. + */ +function drupal_render_cid_create($elements) { + if (isset($elements['#cache']['cid'])) { + return $elements['#cache']['cid']; + } + elseif (isset($elements['#cache']['keys'])) { + $granularity = isset($elements['#cache']['granularity']) ? $elements['#cache']['granularity'] : NULL; + // Merge in additional cache ID parts based provided by drupal_render_cid_parts(). + $cid_parts = array_merge($elements['#cache']['keys'], drupal_render_cid_parts($granularity)); + return implode(':', $cid_parts); + } + return FALSE; +} + +/** + * Function used by uasort to sort structured arrays by weight. + */ +function element_sort($a, $b) { + $a_weight = (is_array($a) && isset($a['#weight'])) ? $a['#weight'] : 0; + $b_weight = (is_array($b) && isset($b['#weight'])) ? $b['#weight'] : 0; + if ($a_weight == $b_weight) { + return 0; + } + return ($a_weight < $b_weight) ? -1 : 1; +} + +/** + * Array sorting callback; sorts elements by title. + */ +function element_sort_by_title($a, $b) { + $a_title = (is_array($a) && isset($a['#title'])) ? $a['#title'] : ''; + $b_title = (is_array($b) && isset($b['#title'])) ? $b['#title'] : ''; + return strnatcasecmp($a_title, $b_title); +} + +/** + * Retrieve the default properties for the defined element type. + * + * @param $type + * An element type as defined by hook_element_info(). + */ +function element_info($type) { + // Use the advanced drupal_static() pattern, since this is called very often. + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['cache'] = &drupal_static(__FUNCTION__); + } + $cache = &$drupal_static_fast['cache']; + + if (!isset($cache)) { + $cache = module_invoke_all('element_info'); + foreach ($cache as $element_type => $info) { + $cache[$element_type]['#type'] = $element_type; + } + // Allow modules to alter the element type defaults. + drupal_alter('element_info', $cache); + } + + return isset($cache[$type]) ? $cache[$type] : array(); +} + +/** + * Retrieve a single property for the defined element type. + * + * @param $type + * An element type as defined by hook_element_info(). + * @param $property_name + * The property within the element type that should be returned. + * @param $default + * (Optional) The value to return if the element type does not specify a + * value for the property. Defaults to NULL. + */ +function element_info_property($type, $property_name, $default = NULL) { + return (($info = element_info($type)) && array_key_exists($property_name, $info)) ? $info[$property_name] : $default; +} + +/** + * Function used by uasort to sort structured arrays by weight, without the property weight prefix. + */ +function drupal_sort_weight($a, $b) { + $a_weight = (is_array($a) && isset($a['weight'])) ? $a['weight'] : 0; + $b_weight = (is_array($b) && isset($b['weight'])) ? $b['weight'] : 0; + if ($a_weight == $b_weight) { + return 0; + } + return ($a_weight < $b_weight) ? -1 : 1; +} + +/** + * Array sorting callback; sorts elements by 'title' key. + */ +function drupal_sort_title($a, $b) { + if (!isset($b['title'])) { + return -1; + } + if (!isset($a['title'])) { + return 1; + } + return strcasecmp($a['title'], $b['title']); +} + +/** + * Check if the key is a property. + */ +function element_property($key) { + return $key[0] == '#'; +} + +/** + * Get properties of a structured array element. Properties begin with '#'. + */ +function element_properties($element) { + return array_filter(array_keys((array) $element), 'element_property'); +} + +/** + * Check if the key is a child. + */ +function element_child($key) { + return !isset($key[0]) || $key[0] != '#'; +} + +/** + * Return the children of an element, optionally sorted by weight. + * + * @param $elements + * The element to be sorted. + * @param $sort + * Boolean to indicate whether the children should be sorted by weight. + * @return + * The array keys of the element's children. + */ +function element_children(&$elements, $sort = FALSE) { + // Do not attempt to sort elements which have already been sorted. + $sort = isset($elements['#sorted']) ? !$elements['#sorted'] : $sort; + + // Filter out properties from the element, leaving only children. + $children = array(); + $sortable = FALSE; + foreach ($elements as $key => $value) { + if ($key === '' || $key[0] !== '#') { + $children[$key] = $value; + if (is_array($value) && isset($value['#weight'])) { + $sortable = TRUE; + } + } + } + // Sort the children if necessary. + if ($sort && $sortable) { + uasort($children, 'element_sort'); + // Put the sorted children back into $elements in the correct order, to + // preserve sorting if the same element is passed through + // element_children() twice. + foreach ($children as $key => $child) { + unset($elements[$key]); + $elements[$key] = $child; + } + $elements['#sorted'] = TRUE; + } + + return array_keys($children); +} + +/** + * Return the visibile children of an element. + * + * @param $elements + * The parent element. + * @return + * The array keys of the element's visible children. + */ +function element_get_visible_children(array $elements) { + $visible_children = array(); + + foreach (element_children($elements) as $key) { + $child = $elements[$key]; + + // Skip un-accessible children. + if (isset($child['#access']) && !$child['#access']) { + continue; + } + + // Skip value and hidden elements, since they are not rendered. + if (isset($child['#type']) && in_array($child['#type'], array('value', 'hidden'))) { + continue; + } + + $visible_children[$key] = $child; + } + + return array_keys($visible_children); +} + +/** + * Sets HTML attributes based on element properties. + * + * @param $element + * The renderable element to process. + * @param $map + * An associative array whose keys are element property names and whose values + * are the HTML attribute names to set for corresponding the property; e.g., + * array('#propertyname' => 'attributename'). If both names are identical + * except for the leading '#', then an attribute name value is sufficient and + * no property name needs to be specified. + */ +function element_set_attributes(array &$element, array $map) { + foreach ($map as $property => $attribute) { + // If the key is numeric, the attribute name needs to be taken over. + if (is_int($property)) { + $property = '#' . $attribute; + } + // Do not overwrite already existing attributes. + if (isset($element[$property]) && !isset($element['#attributes'][$attribute])) { + $element['#attributes'][$attribute] = $element[$property]; + } + } +} + +/** + * Sets a value in a nested array with variable depth. + * + * This helper function should be used when the depth of the array element you + * are changing may vary (that is, the number of parent keys is variable). It + * is primarily used for form structures and renderable arrays. + * + * Example: + * @code + * // Assume you have a 'signature' element somewhere in a form. It might be: + * $form['signature_settings']['signature'] = array( + * '#type' => 'text_format', + * '#title' => t('Signature'), + * ); + * // Or, it might be further nested: + * $form['signature_settings']['user']['signature'] = array( + * '#type' => 'text_format', + * '#title' => t('Signature'), + * ); + * @endcode + * + * To deal with the situation, the code needs to figure out the route to the + * element, given an array of parents that is either + * @code array('signature_settings', 'signature') @endcode in the first case or + * @code array('signature_settings', 'user', 'signature') @endcode in the second + * case. + * + * Without this helper function the only way to set the signature element in one + * line would be using eval(), which should be avoided: + * @code + * // Do not do this! Avoid eval(). + * eval('$form[\'' . implode("']['", $parents) . '\'] = $element;'); + * @endcode + * + * Instead, use this helper function: + * @code + * drupal_array_set_nested_value($form, $parents, $element); + * @endcode + * + * However if the number of array parent keys is static, the value should always + * be set directly rather than calling this function. For instance, for the + * first example we could just do: + * @code + * $form['signature_settings']['signature'] = $element; + * @endcode + * + * @param $array + * A reference to the array to modify. + * @param $parents + * An array of parent keys, starting with the outermost key. + * @param $value + * The value to set. + * @param $force + * (Optional) If TRUE, the value is forced into the structure even if it + * requires the deletion of an already existing non-array parent value. If + * FALSE, PHP throws an error if trying to add into a value that is not an + * array. Defaults to FALSE. + * + * @see drupal_array_get_nested_value() + */ +function drupal_array_set_nested_value(array &$array, array $parents, $value, $force = FALSE) { + $ref = &$array; + foreach ($parents as $parent) { + // PHP auto-creates container arrays and NULL entries without error if $ref + // is NULL, but throws an error if $ref is set, but not an array. + if ($force && isset($ref) && !is_array($ref)) { + $ref = array(); + } + $ref = &$ref[$parent]; + } + $ref = $value; +} + +/** + * Retrieves a value from a nested array with variable depth. + * + * This helper function should be used when the depth of the array element being + * retrieved may vary (that is, the number of parent keys is variable). It is + * primarily used for form structures and renderable arrays. + * + * Without this helper function the only way to get a nested array value with + * variable depth in one line would be using eval(), which should be avoided: + * @code + * // Do not do this! Avoid eval(). + * // May also throw a PHP notice, if the variable array keys do not exist. + * eval('$value = $array[\'' . implode("']['", $parents) . "'];"); + * @endcode + * + * Instead, use this helper function: + * @code + * $value = drupal_array_get_nested_value($form, $parents); + * @endcode + * + * The return value will be NULL, regardless of whether the actual value is NULL + * or whether the requested key does not exist. If it is required to know + * whether the nested array key actually exists, pass a third argument that is + * altered by reference: + * @code + * $key_exists = NULL; + * $value = drupal_array_get_nested_value($form, $parents, $key_exists); + * if ($key_exists) { + * // ... do something with $value ... + * } + * @endcode + * + * However if the number of array parent keys is static, the value should always + * be retrieved directly rather than calling this function. For instance: + * @code + * $value = $form['signature_settings']['signature']; + * @endcode + * + * @param $array + * The array from which to get the value. + * @param $parents + * An array of parent keys of the value, starting with the outermost key. + * @param $key_exists + * (optional) If given, an already defined variable that is altered by + * reference. + * + * @return + * The requested nested value. Possibly NULL if the value is NULL or not all + * nested parent keys exist. $key_exists is altered by reference and is a + * Boolean that indicates whether all nested parent keys exist (TRUE) or not + * (FALSE). This allows to distinguish between the two possibilities when NULL + * is returned. + * + * @see drupal_array_set_nested_value() + */ +function drupal_array_get_nested_value(array &$array, array $parents, &$key_exists = NULL) { + $ref = &$array; + foreach ($parents as $parent) { + if (is_array($ref) && array_key_exists($parent, $ref)) { + $ref = &$ref[$parent]; + } + else { + $key_exists = FALSE; + return NULL; + } + } + $key_exists = TRUE; + return $ref; +} + +/** + * Determines whether a nested array with variable depth contains all of the requested keys. + * + * This helper function should be used when the depth of the array element to be + * checked may vary (that is, the number of parent keys is variable). See + * drupal_array_set_nested_value() for details. It is primarily used for form + * structures and renderable arrays. + * + * If it is required to also get the value of the checked nested key, use + * drupal_array_get_nested_value() instead. + * + * If the number of array parent keys is static, this helper function is + * unnecessary and the following code can be used instead: + * @code + * $value_exists = isset($form['signature_settings']['signature']); + * $key_exists = array_key_exists('signature', $form['signature_settings']); + * @endcode + * + * @param $array + * The array with the value to check for. + * @param $parents + * An array of parent keys of the value, starting with the outermost key. + * + * @return + * TRUE if all the parent keys exist, FALSE otherwise. + * + * @see drupal_array_get_nested_value() + */ +function drupal_array_nested_key_exists(array $array, array $parents) { + // Although this function is similar to PHP's array_key_exists(), its + // arguments should be consistent with drupal_array_get_nested_value(). + $key_exists = NULL; + drupal_array_get_nested_value($array, $parents, $key_exists); + return $key_exists; +} + +/** + * Provide theme registration for themes across .inc files. + */ +function drupal_common_theme() { + return array( + // theme.inc + 'html' => array( + 'render element' => 'page', + 'template' => 'html', + ), + 'page' => array( + 'render element' => 'page', + 'template' => 'page', + ), + 'region' => array( + 'render element' => 'elements', + 'template' => 'region', + ), + 'status_messages' => array( + 'variables' => array('display' => NULL), + ), + 'link' => array( + 'variables' => array('text' => NULL, 'path' => NULL, 'options' => array()), + ), + 'links' => array( + 'variables' => array('links' => NULL, 'attributes' => array('class' => array('links')), 'heading' => array()), + ), + 'image' => array( + // HTML 4 and XHTML 1.0 always require an alt attribute. The HTML 5 draft + // allows the alt attribute to be omitted in some cases. Therefore, + // default the alt attribute to an empty string, but allow code calling + // theme('image') to pass explicit NULL for it to be omitted. Usually, + // neither omission nor an empty string satisfies accessibility + // requirements, so it is strongly encouraged for code calling + // theme('image') to pass a meaningful value for the alt variable. + // - http://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8 + // - http://www.w3.org/TR/xhtml1/dtds.html + // - http://dev.w3.org/html5/spec/Overview.html#alt + // The title attribute is optional in all cases, so it is omitted by + // default. + 'variables' => array('path' => NULL, 'width' => NULL, 'height' => NULL, 'alt' => '', 'title' => NULL, 'attributes' => array()), + ), + 'breadcrumb' => array( + 'variables' => array('breadcrumb' => NULL), + ), + 'help' => array( + 'variables' => array(), + ), + 'table' => array( + 'variables' => array('header' => NULL, 'rows' => NULL, 'attributes' => array(), 'caption' => NULL, 'colgroups' => array(), 'sticky' => TRUE, 'empty' => ''), + ), + 'tablesort_indicator' => array( + 'variables' => array('style' => NULL), + ), + 'mark' => array( + 'variables' => array('type' => MARK_NEW), + ), + 'item_list' => array( + 'variables' => array('items' => array(), 'title' => NULL, 'type' => 'ul', 'attributes' => array()), + ), + 'more_help_link' => array( + 'variables' => array('url' => NULL), + ), + 'feed_icon' => array( + 'variables' => array('url' => NULL, 'title' => NULL), + ), + 'more_link' => array( + 'variables' => array('url' => NULL, 'title' => NULL) + ), + 'username' => array( + 'variables' => array('account' => NULL), + ), + 'progress_bar' => array( + 'variables' => array('percent' => NULL, 'message' => NULL), + ), + 'indentation' => array( + 'variables' => array('size' => 1), + ), + 'html_tag' => array( + 'render element' => 'element', + ), + // from theme.maintenance.inc + 'maintenance_page' => array( + 'variables' => array('content' => NULL, 'show_messages' => TRUE), + 'template' => 'maintenance-page', + ), + 'update_page' => array( + 'variables' => array('content' => NULL, 'show_messages' => TRUE), + ), + 'install_page' => array( + 'variables' => array('content' => NULL), + ), + 'task_list' => array( + 'variables' => array('items' => NULL, 'active' => NULL), + ), + 'authorize_message' => array( + 'variables' => array('message' => NULL, 'success' => TRUE), + ), + 'authorize_report' => array( + 'variables' => array('messages' => array()), + ), + // from pager.inc + 'pager' => array( + 'variables' => array('tags' => array(), 'element' => 0, 'parameters' => array(), 'quantity' => 9), + ), + 'pager_first' => array( + 'variables' => array('text' => NULL, 'element' => 0, 'parameters' => array()), + ), + 'pager_previous' => array( + 'variables' => array('text' => NULL, 'element' => 0, 'interval' => 1, 'parameters' => array()), + ), + 'pager_next' => array( + 'variables' => array('text' => NULL, 'element' => 0, 'interval' => 1, 'parameters' => array()), + ), + 'pager_last' => array( + 'variables' => array('text' => NULL, 'element' => 0, 'parameters' => array()), + ), + 'pager_link' => array( + 'variables' => array('text' => NULL, 'page_new' => NULL, 'element' => NULL, 'parameters' => array(), 'attributes' => array()), + ), + // from menu.inc + 'menu_link' => array( + 'render element' => 'element', + ), + 'menu_tree' => array( + 'render element' => 'tree', + ), + 'menu_local_task' => array( + 'render element' => 'element', + ), + 'menu_local_action' => array( + 'render element' => 'element', + ), + 'menu_local_tasks' => array( + 'variables' => array('primary' => array(), 'secondary' => array()), + ), + // from form.inc + 'select' => array( + 'render element' => 'element', + ), + 'fieldset' => array( + 'render element' => 'element', + ), + 'radio' => array( + 'render element' => 'element', + ), + 'radios' => array( + 'render element' => 'element', + ), + 'date' => array( + 'render element' => 'element', + ), + 'exposed_filters' => array( + 'render element' => 'form', + ), + 'checkbox' => array( + 'render element' => 'element', + ), + 'checkboxes' => array( + 'render element' => 'element', + ), + 'button' => array( + 'render element' => 'element', + ), + 'image_button' => array( + 'render element' => 'element', + ), + 'hidden' => array( + 'render element' => 'element', + ), + 'textfield' => array( + 'render element' => 'element', + ), + 'form' => array( + 'render element' => 'element', + ), + 'textarea' => array( + 'render element' => 'element', + ), + 'password' => array( + 'render element' => 'element', + ), + 'file' => array( + 'render element' => 'element', + ), + 'tableselect' => array( + 'render element' => 'element', + ), + 'form_element' => array( + 'render element' => 'element', + ), + 'form_required_marker' => array( + 'render element' => 'element', + ), + 'form_element_label' => array( + 'render element' => 'element', + ), + 'vertical_tabs' => array( + 'render element' => 'element', + ), + 'container' => array( + 'render element' => 'element', + ), + ); +} + +/** + * @ingroup schemaapi + * @{ + */ + +/** + * Creates all tables in a module's hook_schema() implementation. + * + * Note: This function does not pass the module's schema through + * hook_schema_alter(). The module's tables will be created exactly as the + * module defines them. + * + * @param $module + * The module for which the tables will be created. + */ +function drupal_install_schema($module) { + $schema = drupal_get_schema_unprocessed($module); + _drupal_schema_initialize($schema, $module, FALSE); + + foreach ($schema as $name => $table) { + db_create_table($name, $table); + } +} + +/** + * Remove all tables that a module defines in its hook_schema(). + * + * Note: This function does not pass the module's schema through + * hook_schema_alter(). The module's tables will be created exactly as the + * module defines them. + * + * @param $module + * The module for which the tables will be removed. + * @return + * An array of arrays with the following key/value pairs: + * - success: a boolean indicating whether the query succeeded. + * - query: the SQL query(s) executed, passed through check_plain(). + */ +function drupal_uninstall_schema($module) { + $schema = drupal_get_schema_unprocessed($module); + _drupal_schema_initialize($schema, $module, FALSE); + + foreach ($schema as $table) { + if (db_table_exists($table['name'])) { + db_drop_table($table['name']); + } + } +} + +/** + * Returns the unprocessed and unaltered version of a module's schema. + * + * Use this function only if you explicitly need the original + * specification of a schema, as it was defined in a module's + * hook_schema(). No additional default values will be set, + * hook_schema_alter() is not invoked and these unprocessed + * definitions won't be cached. + * + * This function can be used to retrieve a schema specification in + * hook_schema(), so it allows you to derive your tables from existing + * specifications. + * + * It is also used by drupal_install_schema() and + * drupal_uninstall_schema() to ensure that a module's tables are + * created exactly as specified without any changes introduced by a + * module that implements hook_schema_alter(). + * + * @param $module + * The module to which the table belongs. + * @param $table + * The name of the table. If not given, the module's complete schema + * is returned. + */ +function drupal_get_schema_unprocessed($module, $table = NULL) { + // Load the .install file to get hook_schema. + module_load_install($module); + $schema = module_invoke($module, 'schema'); + + if (isset($table) && isset($schema[$table])) { + return $schema[$table]; + } + elseif (!empty($schema)) { + return $schema; + } + return array(); +} + +/** + * Fill in required default values for table definitions returned by hook_schema(). + * + * @param $schema + * The schema definition array as it was returned by the module's + * hook_schema(). + * @param $module + * The module for which hook_schema() was invoked. + * @param $remove_descriptions + * (optional) Whether to additionally remove 'description' keys of all tables + * and fields to improve performance of serialize() and unserialize(). + * Defaults to TRUE. + */ +function _drupal_schema_initialize(&$schema, $module, $remove_descriptions = TRUE) { + // Set the name and module key for all tables. + foreach ($schema as $name => &$table) { + if (empty($table['module'])) { + $table['module'] = $module; + } + if (!isset($table['name'])) { + $table['name'] = $name; + } + if ($remove_descriptions) { + unset($table['description']); + foreach ($table['fields'] as &$field) { + unset($field['description']); + } + } + } +} + +/** + * Retrieve a list of fields from a table schema. The list is suitable for use in a SQL query. + * + * @param $table + * The name of the table from which to retrieve fields. + * @param + * An optional prefix to to all fields. + * + * @return An array of fields. + **/ +function drupal_schema_fields_sql($table, $prefix = NULL) { + $schema = drupal_get_schema($table); + $fields = array_keys($schema['fields']); + if ($prefix) { + $columns = array(); + foreach ($fields as $field) { + $columns[] = "$prefix.$field"; + } + return $columns; + } + else { + return $fields; + } +} + +/** + * Saves (inserts or updates) a record to the database based upon the schema. + * + * @param $table + * The name of the table; this must be defined by a hook_schema() + * implementation. + * @param $record + * An object or array representing the record to write, passed in by + * reference. If inserting a new record, values not provided in $record will + * be populated in $record and in the database with the default values from + * the schema, as well as a single serial (auto-increment) field (if present). + * If updating an existing record, only provided values are updated in the + * database, and $record is not modified. + * @param $primary_keys + * To indicate that this is a new record to be inserted, omit this argument. + * If this is an update, this argument specifies the primary keys' field + * names. If there is only 1 field in the key, you may pass in a string; if + * there are multiple fields in the key, pass in an array. + * + * @return + * If the record insert or update failed, returns FALSE. If it succeeded, + * returns SAVED_NEW or SAVED_UPDATED, depending on the operation performed. + */ +function drupal_write_record($table, &$record, $primary_keys = array()) { + // Standardize $primary_keys to an array. + if (is_string($primary_keys)) { + $primary_keys = array($primary_keys); + } + + $schema = drupal_get_schema($table); + if (empty($schema)) { + return FALSE; + } + + $object = (object) $record; + $fields = array(); + + // Go through the schema to determine fields to write. + foreach ($schema['fields'] as $field => $info) { + if ($info['type'] == 'serial') { + // Skip serial types if we are updating. + if (!empty($primary_keys)) { + continue; + } + // Track serial field so we can helpfully populate them after the query. + // NOTE: Each table should come with one serial field only. + $serial = $field; + } + + // Skip field if it is in $primary_keys as it is unnecessary to update a + // field to the value it is already set to. + if (in_array($field, $primary_keys)) { + continue; + } + + if (!property_exists($object, $field)) { + // Skip fields that are not provided, default values are already known + // by the database. + continue; + } + + // Build array of fields to update or insert. + if (empty($info['serialize'])) { + $fields[$field] = $object->$field; + } + else { + $fields[$field] = serialize($object->$field); + } + + // Type cast to proper datatype, except when the value is NULL and the + // column allows this. + // + // MySQL PDO silently casts e.g. FALSE and '' to 0 when inserting the value + // into an integer column, but PostgreSQL PDO does not. Also type cast NULL + // when the column does not allow this. + if (isset($object->$field) || !empty($info['not null'])) { + if ($info['type'] == 'int' || $info['type'] == 'serial') { + $fields[$field] = (int) $fields[$field]; + } + elseif ($info['type'] == 'float') { + $fields[$field] = (float) $fields[$field]; + } + else { + $fields[$field] = (string) $fields[$field]; + } + } + } + + if (empty($fields)) { + return; + } + + // Build the SQL. + if (empty($primary_keys)) { + // We are doing an insert. + $options = array('return' => Database::RETURN_INSERT_ID); + if (isset($serial) && isset($fields[$serial])) { + // If the serial column has been explicitly set with an ID, then we don't + // require the database to return the last insert id. + if ($fields[$serial]) { + $options['return'] = Database::RETURN_AFFECTED; + } + // If a serial column does exist with no value (i.e. 0) then remove it as + // the database will insert the correct value for us. + else { + unset($fields[$serial]); + } + } + $query = db_insert($table, $options)->fields($fields); + $return = SAVED_NEW; + } + else { + $query = db_update($table)->fields($fields); + foreach ($primary_keys as $key) { + $query->condition($key, $object->$key); + } + $return = SAVED_UPDATED; + } + + // Execute the SQL. + if ($query_return = $query->execute()) { + if (isset($serial)) { + // If the database was not told to return the last insert id, it will be + // because we already know it. + if (isset($options) && $options['return'] != Database::RETURN_INSERT_ID) { + $object->$serial = $fields[$serial]; + } + else { + $object->$serial = $query_return; + } + } + } + // If we have a single-field primary key but got no insert ID, the + // query failed. Note that we explicitly check for FALSE, because + // a valid update query which doesn't change any values will return + // zero (0) affected rows. + elseif ($query_return === FALSE && count($primary_keys) == 1) { + $return = FALSE; + } + + // If we are inserting, populate empty fields with default values. + if (empty($primary_keys)) { + foreach ($schema['fields'] as $field => $info) { + if (isset($info['default']) && !property_exists($object, $field)) { + $object->$field = $info['default']; + } + } + } + + // If we began with an array, convert back. + if (is_array($record)) { + $record = (array) $object; + } + + return $return; +} + +/** + * @} End of "ingroup schemaapi". + */ + +/** + * Parses Drupal module and theme .info files. + * + * Info files are NOT for placing arbitrary theme and module-specific settings. + * Use variable_get() and variable_set() for that. + * + * Information stored in a module .info file: + * - name: The real name of the module for display purposes. + * - description: A brief description of the module. + * - dependencies: An array of shortnames of other modules this module requires. + * - package: The name of the package of modules this module belongs to. + * + * See forum.info for an example of a module .info file. + * + * Information stored in a theme .info file: + * - name: The real name of the theme for display purposes. + * - description: Brief description. + * - screenshot: Path to screenshot relative to the theme's .info file. + * - engine: Theme engine; typically phptemplate. + * - base: Name of a base theme, if applicable; e.g., base = zen. + * - regions: Listed regions; e.g., region[left] = Left sidebar. + * - features: Features available; e.g., features[] = logo. + * - stylesheets: Theme stylesheets; e.g., stylesheets[all][] = my-style.css. + * - scripts: Theme scripts; e.g., scripts[] = my-script.js. + * + * See bartik.info for an example of a theme .info file. + * + * @param $filename + * The file we are parsing. Accepts file with relative or absolute path. + * + * @return + * The info array. + * + * @see drupal_parse_info_format() + */ +function drupal_parse_info_file($filename) { + $info = &drupal_static(__FUNCTION__, array()); + + if (!isset($info[$filename])) { + if (!file_exists($filename)) { + $info[$filename] = array(); + } + else { + $data = file_get_contents($filename); + $info[$filename] = drupal_parse_info_format($data); + } + } + return $info[$filename]; +} + +/** + * Parse data in Drupal's .info format. + * + * Data should be in an .ini-like format to specify values. White-space + * generally doesn't matter, except inside values: + * @code + * key = value + * key = "value" + * key = 'value' + * key = "multi-line + * value" + * key = 'multi-line + * value' + * key + * = + * 'value' + * @endcode + * + * Arrays are created using a HTTP GET alike syntax: + * @code + * key[] = "numeric array" + * key[index] = "associative array" + * key[index][] = "nested numeric array" + * key[index][index] = "nested associative array" + * @endcode + * + * PHP constants are substituted in, but only when used as the entire value. + * Comments should start with a semi-colon at the beginning of a line. + * + * @param $data + * A string to parse. + * @return + * The info array. + * + * @see drupal_parse_info_file() + */ +function drupal_parse_info_format($data) { + $info = array(); + $constants = get_defined_constants(); + + if (preg_match_all(' + @^\s* # Start at the beginning of a line, ignoring leading whitespace + ((?: + [^=;\[\]]| # Key names cannot contain equal signs, semi-colons or square brackets, + \[[^\[\]]*\] # unless they are balanced and not nested + )+?) + \s*=\s* # Key/value pairs are separated by equal signs (ignoring white-space) + (?: + ("(?:[^"]|(?<=\\\\)")*")| # Double-quoted string, which may contain slash-escaped quotes/slashes + (\'(?:[^\']|(?<=\\\\)\')*\')| # Single-quoted string, which may contain slash-escaped quotes/slashes + ([^\r\n]*?) # Non-quoted string + )\s*$ # Stop at the next end of a line, ignoring trailing whitespace + @msx', $data, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + // Fetch the key and value string + $i = 0; + foreach (array('key', 'value1', 'value2', 'value3') as $var) { + $$var = isset($match[++$i]) ? $match[$i] : ''; + } + $value = stripslashes(substr($value1, 1, -1)) . stripslashes(substr($value2, 1, -1)) . $value3; + + // Parse array syntax + $keys = preg_split('/\]?\[/', rtrim($key, ']')); + $last = array_pop($keys); + $parent = &$info; + + // Create nested arrays + foreach ($keys as $key) { + if ($key == '') { + $key = count($parent); + } + if (!isset($parent[$key]) || !is_array($parent[$key])) { + $parent[$key] = array(); + } + $parent = &$parent[$key]; + } + + // Handle PHP constants. + if (isset($constants[$value])) { + $value = $constants[$value]; + } + + // Insert actual value + if ($last == '') { + $last = count($parent); + } + $parent[$last] = $value; + } + } + + return $info; +} + +/** + * Severity levels, as defined in RFC 3164: http://www.ietf.org/rfc/rfc3164.txt. + * + * @return + * Array of the possible severity levels for log messages. + * + * @see watchdog() + */ +function watchdog_severity_levels() { + return array( + LOG_EMERG => t('emergency'), + LOG_ALERT => t('alert'), + LOG_CRIT => t('critical'), + LOG_ERR => t('error'), + LOG_WARNING => t('warning'), + LOG_NOTICE => t('notice'), + LOG_INFO => t('info'), + LOG_DEBUG => t('debug'), + ); +} + + +/** + * Explode a string of given tags into an array. + * + * @see drupal_implode_tags() + */ +function drupal_explode_tags($tags) { + // This regexp allows the following types of user input: + // this, "somecompany, llc", "and ""this"" w,o.rks", foo bar + $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x'; + preg_match_all($regexp, $tags, $matches); + $typed_tags = array_unique($matches[1]); + + $tags = array(); + foreach ($typed_tags as $tag) { + // If a user has escaped a term (to demonstrate that it is a group, + // or includes a comma or quote character), we remove the escape + // formatting so to save the term into the database as the user intends. + $tag = trim(str_replace('""', '"', preg_replace('/^"(.*)"$/', '\1', $tag))); + if ($tag != "") { + $tags[] = $tag; + } + } + + return $tags; +} + +/** + * Implode an array of tags into a string. + * + * @see drupal_explode_tags() + */ +function drupal_implode_tags($tags) { + $encoded_tags = array(); + foreach ($tags as $tag) { + // Commas and quotes in tag names are special cases, so encode them. + if (strpos($tag, ',') !== FALSE || strpos($tag, '"') !== FALSE) { + $tag = '"' . str_replace('"', '""', $tag) . '"'; + } + + $encoded_tags[] = $tag; + } + return implode(', ', $encoded_tags); +} + +/** + * Flush all cached data on the site. + * + * Empties cache tables, rebuilds the menu cache and theme registries, and + * invokes a hook so that other modules' cache data can be cleared as well. + */ +function drupal_flush_all_caches() { + // Change query-strings on css/js files to enforce reload for all users. + _drupal_flush_css_js(); + + registry_rebuild(); + drupal_clear_css_cache(); + drupal_clear_js_cache(); + + // Rebuild the theme data. Note that the module data is rebuilt above, as + // part of registry_rebuild(). + system_rebuild_theme_data(); + drupal_theme_rebuild(); + + node_types_rebuild(); + // node_menu() defines menu items based on node types so it needs to come + // after node types are rebuilt. + menu_rebuild(); + + // Synchronize to catch any actions that were added or removed. + actions_synchronize(); + + // Don't clear cache_form - in-progress form submissions may break. + // Ordered so clearing the page cache will always be the last action. + $core = array('cache', 'cache_filter', 'cache_bootstrap', 'cache_page'); + $cache_tables = array_merge(module_invoke_all('flush_caches'), $core); + foreach ($cache_tables as $table) { + cache_clear_all('*', $table, TRUE); + } + + // Rebuild the bootstrap module list. We do this here so that developers + // can get new hook_boot() implementations registered without having to + // write a hook_update_N() function. + _system_update_bootstrap_status(); +} + +/** + * Helper function to change query-strings on css/js files. + * + * Changes the character added to all css/js files as dummy query-string, so + * that all browsers are forced to reload fresh files. + */ +function _drupal_flush_css_js() { + // The timestamp is converted to base 36 in order to make it more compact. + variable_set('css_js_query_string', base_convert(REQUEST_TIME, 10, 36)); +} + +/** + * Debug function used for outputting debug information. + * + * The debug information is passed on to trigger_error() after being converted + * to a string using _drupal_debug_message(). + * + * @param $data + * Data to be output. + * @param $label + * Label to prefix the data. + * @param $print_r + * Flag to switch between print_r() and var_export() for data conversion to + * string. Set $print_r to TRUE when dealing with a recursive data structure + * as var_export() will generate an error. + */ +function debug($data, $label = NULL, $print_r = FALSE) { + // Print $data contents to string. + $string = check_plain($print_r ? print_r($data, TRUE) : var_export($data, TRUE)); + + // Display values with pre-formatting to increase readability. + $string = '
' . $string . '
'; + + trigger_error(trim($label ? "$label: $string" : $string)); +} + +/** + * Parse a dependency for comparison by drupal_check_incompatibility(). + * + * @param $dependency + * A dependency string, for example 'foo (>=8.x-4.5-beta5, 3.x)'. + * @return + * An associative array with three keys: + * - 'name' includes the name of the thing to depend on (e.g. 'foo'). + * - 'original_version' contains the original version string (which can be + * used in the UI for reporting incompatibilities). + * - 'versions' is a list of associative arrays, each containing the keys + * 'op' and 'version'. 'op' can be one of: '=', '==', '!=', '<>', '<', + * '<=', '>', or '>='. 'version' is one piece like '4.5-beta3'. + * Callers should pass this structure to drupal_check_incompatibility(). + * + * @see drupal_check_incompatibility() + */ +function drupal_parse_dependency($dependency) { + // We use named subpatterns and support every op that version_compare + // supports. Also, op is optional and defaults to equals. + $p_op = '(?P!=|==|=|<|<=|>|>=|<>)?'; + // Core version is always optional: 8.x-2.x and 2.x is treated the same. + $p_core = '(?:' . preg_quote(DRUPAL_CORE_COMPATIBILITY) . '-)?'; + $p_major = '(?P\d+)'; + // By setting the minor version to x, branches can be matched. + $p_minor = '(?P(?:\d+|x)(?:-[A-Za-z]+\d+)?)'; + $value = array(); + $parts = explode('(', $dependency, 2); + $value['name'] = trim($parts[0]); + if (isset($parts[1])) { + $value['original_version'] = ' (' . $parts[1]; + foreach (explode(',', $parts[1]) as $version) { + if (preg_match("/^\s*$p_op\s*$p_core$p_major\.$p_minor/", $version, $matches)) { + $op = !empty($matches['operation']) ? $matches['operation'] : '='; + if ($matches['minor'] == 'x') { + // Drupal considers "2.x" to mean any version that begins with + // "2" (e.g. 2.0, 2.9 are all "2.x"). PHP's version_compare(), + // on the other hand, treats "x" as a string; so to + // version_compare(), "2.x" is considered less than 2.0. This + // means that >=2.x and <2.x are handled by version_compare() + // as we need, but > and <= are not. + if ($op == '>' || $op == '<=') { + $matches['major']++; + } + // Equivalence can be checked by adding two restrictions. + if ($op == '=' || $op == '==') { + $value['versions'][] = array('op' => '<', 'version' => ($matches['major'] + 1) . '.x'); + $op = '>='; + } + } + $value['versions'][] = array('op' => $op, 'version' => $matches['major'] . '.' . $matches['minor']); + } + } + } + return $value; +} + +/** + * Check whether a version is compatible with a given dependency. + * + * @param $v + * The parsed dependency structure from drupal_parse_dependency(). + * @param $current_version + * The version to check against (like 4.2). + * @return + * NULL if compatible, otherwise the original dependency version string that + * caused the incompatibility. + * + * @see drupal_parse_dependency() + */ +function drupal_check_incompatibility($v, $current_version) { + if (!empty($v['versions'])) { + foreach ($v['versions'] as $required_version) { + if ((isset($required_version['op']) && !version_compare($current_version, $required_version['version'], $required_version['op']))) { + return $v['original_version']; + } + } + } +} + +/** + * Get the entity info array of an entity type. + * + * @see hook_entity_info() + * @see hook_entity_info_alter() + * + * @param $entity_type + * The entity type, e.g. node, for which the info shall be returned, or NULL + * to return an array with info about all types. + */ +function entity_get_info($entity_type = NULL) { + global $language; + + // Use the advanced drupal_static() pattern, since this is called very often. + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['entity_info'] = &drupal_static(__FUNCTION__); + } + $entity_info = &$drupal_static_fast['entity_info']; + + // hook_entity_info() includes translated strings, so each language is cached + // separately. + $langcode = $language->language; + + if (empty($entity_info)) { + if ($cache = cache_get("entity_info:$langcode")) { + $entity_info = $cache->data; + } + else { + $entity_info = module_invoke_all('entity_info'); + // Merge in default values. + foreach ($entity_info as $name => $data) { + $entity_info[$name] += array( + 'fieldable' => FALSE, + 'controller class' => 'DrupalDefaultEntityController', + 'static cache' => TRUE, + 'field cache' => TRUE, + 'load hook' => $name . '_load', + 'bundles' => array(), + 'view modes' => array(), + 'entity keys' => array(), + 'translation' => array(), + ); + $entity_info[$name]['entity keys'] += array( + 'revision' => '', + 'bundle' => '', + ); + foreach ($entity_info[$name]['view modes'] as $view_mode => $view_mode_info) { + $entity_info[$name]['view modes'][$view_mode] += array( + 'custom settings' => FALSE, + ); + } + // If no bundle key is provided, assume a single bundle, named after + // the entity type. + if (empty($entity_info[$name]['entity keys']['bundle']) && empty($entity_info[$name]['bundles'])) { + $entity_info[$name]['bundles'] = array($name => array('label' => $entity_info[$name]['label'])); + } + // Prepare entity schema fields SQL info for + // DrupalEntityControllerInterface::buildQuery(). + if (isset($entity_info[$name]['base table'])) { + $entity_info[$name]['schema_fields_sql']['base table'] = drupal_schema_fields_sql($entity_info[$name]['base table']); + if (isset($entity_info[$name]['revision table'])) { + $entity_info[$name]['schema_fields_sql']['revision table'] = drupal_schema_fields_sql($entity_info[$name]['revision table']); + } + } + } + // Let other modules alter the entity info. + drupal_alter('entity_info', $entity_info); + cache_set("entity_info:$langcode", $entity_info); + } + } + + if (empty($entity_type)) { + return $entity_info; + } + elseif (isset($entity_info[$entity_type])) { + return $entity_info[$entity_type]; + } +} + +/** + * Resets the cached information about entity types. + */ +function entity_info_cache_clear() { + drupal_static_reset('entity_get_info'); + // Clear all languages. + cache_clear_all('entity_info:', 'cache', TRUE); +} + +/** + * Helper function to extract id, vid, and bundle name from an entity. + * + * @param $entity_type + * The entity type; e.g. 'node' or 'user'. + * @param $entity + * The entity from which to extract values. + * @return + * A numerically indexed array (not a hash table) containing these + * elements: + * 0: primary id of the entity + * 1: revision id of the entity, or NULL if $entity_type is not versioned + * 2: bundle name of the entity + */ +function entity_extract_ids($entity_type, $entity) { + $info = entity_get_info($entity_type); + // Objects being created might not have id/vid yet. + $id = isset($entity->{$info['entity keys']['id']}) ? $entity->{$info['entity keys']['id']} : NULL; + $vid = ($info['entity keys']['revision'] && isset($entity->{$info['entity keys']['revision']})) ? $entity->{$info['entity keys']['revision']} : NULL; + // If no bundle key provided, then we assume a single bundle, named after the + // entity type. + $bundle = $info['entity keys']['bundle'] ? $entity->{$info['entity keys']['bundle']} : $entity_type; + return array($id, $vid, $bundle); +} + +/** + * Helper function to assemble an object structure with initial ids. + * + * This function can be seen as reciprocal to entity_extract_ids(). + * + * @param $entity_type + * The entity type; e.g. 'node' or 'user'. + * @param $ids + * A numerically indexed array, as returned by entity_extract_ids(), + * containing these elements: + * 0: primary id of the entity + * 1: revision id of the entity, or NULL if $entity_type is not versioned + * 2: bundle name of the entity, or NULL if $entity_type has no bundles + * @return + * An entity structure, initialized with the ids provided. + */ +function entity_create_stub_entity($entity_type, $ids) { + $entity = new stdClass(); + $info = entity_get_info($entity_type); + $entity->{$info['entity keys']['id']} = $ids[0]; + if (!empty($info['entity keys']['revision']) && isset($ids[1])) { + $entity->{$info['entity keys']['revision']} = $ids[1]; + } + if (!empty($info['entity keys']['bundle']) && isset($ids[2])) { + $entity->{$info['entity keys']['bundle']} = $ids[2]; + } + return $entity; +} + +/** + * Load entities from the database. + * + * The entities are stored in a static memory cache, and will not require + * database access if loaded again during the same page request. + * + * The actual loading is done through a class that has to implement the + * DrupalEntityControllerInterface interface. By default, + * DrupalDefaultEntityController is used. Entity types can specify that a + * different class should be used by setting the 'controller class' key in + * hook_entity_info(). These classes can either implement the + * DrupalEntityControllerInterface interface, or, most commonly, extend the + * DrupalDefaultEntityController class. See node_entity_info() and the + * NodeController in node.module as an example. + * + * @see hook_entity_info() + * @see DrupalEntityControllerInterface + * @see DrupalDefaultEntityController + * @see EntityFieldQuery + * + * @param $entity_type + * The entity type to load, e.g. node or user. + * @param $ids + * An array of entity IDs, or FALSE to load all entities. + * @param $conditions + * (deprecated) An associative array of conditions on the base table, where + * the keys are the database fields and the values are the values those + * fields must have. Instead, it is preferable to use EntityFieldQuery to + * retrieve a list of entity IDs loadable by this function. + * @param $reset + * Whether to reset the internal cache for the requested entity type. + * + * @return + * An array of entity objects indexed by their ids. When no results are + * found, an empty array is returned. + * + * @todo Remove $conditions in Drupal 8. + */ +function entity_load($entity_type, $ids = FALSE, $conditions = array(), $reset = FALSE) { + if ($reset) { + entity_get_controller($entity_type)->resetCache(); + } + return entity_get_controller($entity_type)->load($ids, $conditions); +} + +/** + * Loads the unchanged, i.e. not modified, entity from the database. + * + * Unlike entity_load() this function ensures the entity is directly loaded from + * the database, thus bypassing any static cache. In particular, this function + * is useful to determine changes by comparing the entity being saved to the + * stored entity. + * + * @param $entity_type + * The entity type to load, e.g. node or user. + * @param $id + * The id of the entity to load. + * + * @return + * The unchanged entity, or FALSE if the entity cannot be loaded. + */ +function entity_load_unchanged($entity_type, $id) { + entity_get_controller($entity_type)->resetCache(array($id)); + $result = entity_get_controller($entity_type)->load(array($id)); + return reset($result); +} + +/** + * Get the entity controller class for an entity type. + */ +function entity_get_controller($entity_type) { + $controllers = &drupal_static(__FUNCTION__, array()); + if (!isset($controllers[$entity_type])) { + $type_info = entity_get_info($entity_type); + $class = $type_info['controller class']; + $controllers[$entity_type] = new $class($entity_type); + } + return $controllers[$entity_type]; +} + +/** + * Invoke hook_entity_prepare_view(). + * + * If adding a new entity similar to nodes, comments or users, you should + * invoke this function during the ENTITY_build_content() or + * ENTITY_view_multiple() phases of rendering to allow other modules to alter + * the objects during this phase. This is needed for situations where + * information needs to be loaded outside of ENTITY_load() - particularly + * when loading entities into one another - i.e. a user object into a node, due + * to the potential for unwanted side-effects such as caching and infinite + * recursion. By convention, entity_prepare_view() is called after + * field_attach_prepare_view() to allow entity level hooks to act on content + * loaded by field API. + * @see hook_entity_prepare_view() + * + * @param $entity_type + * The type of entity, i.e. 'node', 'user'. + * @param $entities + * The entity objects which are being prepared for view, keyed by object ID. + * @param $langcode + * (optional) A language code to be used for rendering. Defaults to the global + * content language of the current request. + */ +function entity_prepare_view($entity_type, $entities, $langcode = NULL) { + if (!isset($langcode)) { + $langcode = $GLOBALS['language_content']->language; + } + + // To ensure hooks are only run once per entity, check for an + // entity_view_prepared flag and only process items without it. + // @todo: resolve this more generally for both entity and field level hooks. + $prepare = array(); + foreach ($entities as $id => $entity) { + if (empty($entity->entity_view_prepared)) { + // Add this entity to the items to be prepared. + $prepare[$id] = $entity; + + // Mark this item as prepared. + $entity->entity_view_prepared = TRUE; + } + } + + if (!empty($prepare)) { + module_invoke_all('entity_prepare_view', $prepare, $entity_type, $langcode); + } +} + +/** + * Returns the uri elements of an entity. + * + * @param $entity_type + * The entity type; e.g. 'node' or 'user'. + * @param $entity + * The entity for which to generate a path. + * @return + * An array containing the 'path' and 'options' keys used to build the uri of + * the entity, and matching the signature of url(). NULL if the entity has no + * uri of its own. + */ +function entity_uri($entity_type, $entity) { + // This check enables the URI of an entity to be easily overridden from what + // the callback for the entity type or bundle would return, and it helps + // minimize performance overhead when entity_uri() is called multiple times + // for the same entity. + if (!isset($entity->uri)) { + $info = entity_get_info($entity_type); + list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); + + // A bundle-specific callback takes precedence over the generic one for the + // entity type. + if (isset($info['bundles'][$bundle]['uri callback'])) { + $uri_callback = $info['bundles'][$bundle]['uri callback']; + } + elseif (isset($info['uri callback'])) { + $uri_callback = $info['uri callback']; + } + else { + $uri_callback = NULL; + } + + // Invoke the callback to get the URI. If there is no callback, set the + // entity's 'uri' property to FALSE to indicate that it is known to not have + // a URI. + if (isset($uri_callback) && function_exists($uri_callback)) { + $entity->uri = $uri_callback($entity); + if (!isset($entity->uri['options'])) { + $entity->uri['options'] = array(); + } + // Pass the entity data to url() so that alter functions do not need to + // lookup this entity again. + $entity->uri['options']['entity_type'] = $entity_type; + $entity->uri['options']['entity'] = $entity; + } + else { + $entity->uri = FALSE; + } + } + return $entity->uri ? $entity->uri : NULL; +} + +/** + * Returns the label of an entity. + * + * See the 'label callback' component of the hook_entity_info() return value + * for more information. + * + * @param $entity_type + * The entity type; e.g., 'node' or 'user'. + * @param $entity + * The entity for which to generate the label. + * + * @return + * The entity label, or FALSE if not found. + */ +function entity_label($entity_type, $entity) { + $label = FALSE; + $info = entity_get_info($entity_type); + if (isset($info['label callback']) && function_exists($info['label callback'])) { + $label = $info['label callback']($entity_type, $entity); + } + elseif (!empty($info['entity keys']['label']) && isset($entity->{$info['entity keys']['label']})) { + $label = $entity->{$info['entity keys']['label']}; + } + + return $label; +} + +/** + * Helper function for attaching field API validation to entity forms. + */ +function entity_form_field_validate($entity_type, $form, &$form_state) { + // All field attach API functions act on an entity object, but during form + // validation, we don't have one. $form_state contains the entity as it was + // prior to processing the current form submission, and we must not update it + // until we have fully validated the submitted input. Therefore, for + // validation, act on a pseudo entity created out of the form values. + $pseudo_entity = (object) $form_state['values']; + field_attach_form_validate($entity_type, $pseudo_entity, $form, $form_state); +} + +/** + * Helper function for copying submitted values to entity properties for simple entity forms. + * + * During the submission handling of an entity form's "Save", "Preview", and + * possibly other buttons, the form state's entity needs to be updated with the + * submitted form values. Each entity form implements its own builder function + * for doing this, appropriate for the particular entity and form, whereas + * modules may specify additional builder functions in $form['#entity_builders'] + * for copying the form values of added form elements to entity properties. + * Many of the main entity builder functions can call this helper function to + * re-use its logic of copying $form_state['values'][PROPERTY] values to + * $entity->PROPERTY for all entries in $form_state['values'] that are not field + * data, and calling field_attach_submit() to copy field data. Apart from that + * this helper invokes any additional builder functions that have been specified + * in $form['#entity_builders']. + * + * For some entity forms (e.g., forms with complex non-field data and forms that + * simultaneously edit multiple entities), this behavior may be inappropriate, + * so the builder function for such forms needs to implement the required + * functionality instead of calling this function. + */ +function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_state) { + $info = entity_get_info($entity_type); + list(, , $bundle) = entity_extract_ids($entity_type, $entity); + + // Copy top-level form values that are not for fields to entity properties, + // without changing existing entity properties that are not being edited by + // this form. Copying field values must be done using field_attach_submit(). + $values_excluding_fields = $info['fieldable'] ? array_diff_key($form_state['values'], field_info_instances($entity_type, $bundle)) : $form_state['values']; + foreach ($values_excluding_fields as $key => $value) { + $entity->$key = $value; + } + + // Invoke all specified builders for copying form values to entity properties. + if (isset($form['#entity_builders'])) { + foreach ($form['#entity_builders'] as $function) { + $function($entity_type, $entity, $form, $form_state); + } + } + + // Copy field values to the entity. + if ($info['fieldable']) { + field_attach_submit($entity_type, $entity, $form, $form_state); + } +} + +/** + * Performs one or more XML-RPC request(s). + * + * Usage example: + * @code + * $result = xmlrpc('http://example.com/xmlrpc.php', array( + * 'service.methodName' => array($parameter, $second, $third), + * )); + * @endcode + * + * @param $url + * An absolute URL of the XML-RPC endpoint. + * @param $args + * An associative array whose keys are the methods to call and whose values + * are the arguments to pass to the respective method. If multiple methods + * are specified, a system.multicall is performed. + * @param $options + * (optional) An array of options to pass along to drupal_http_request(). + * + * @return + * For one request: + * Either the return value of the method on success, or FALSE. + * If FALSE is returned, see xmlrpc_errno() and xmlrpc_error_msg(). + * For multiple requests: + * An array of results. Each result will either be the result + * returned by the method called, or an xmlrpc_error object if the call + * failed. See xmlrpc_error(). + */ +function xmlrpc($url, $args, $options = array()) { + require_once DRUPAL_ROOT . '/core/includes/xmlrpc.inc'; + return _xmlrpc($url, $args, $options); +} + +/** + * Retrieves a list of all available archivers. + * + * @see hook_archiver_info() + * @see hook_archiver_info_alter() + */ +function archiver_get_info() { + $archiver_info = &drupal_static(__FUNCTION__, array()); + + if (empty($archiver_info)) { + $cache = cache_get('archiver_info'); + if ($cache === FALSE) { + // Rebuild the cache and save it. + $archiver_info = module_invoke_all('archiver_info'); + drupal_alter('archiver_info', $archiver_info); + uasort($archiver_info, 'drupal_sort_weight'); + cache_set('archiver_info', $archiver_info); + } + else { + $archiver_info = $cache->data; + } + } + + return $archiver_info; +} + +/** + * Returns a string of supported archive extensions. + * + * @return + * A space-separated string of extensions suitable for use by the file + * validation system. + */ +function archiver_get_extensions() { + $valid_extensions = array(); + foreach (archiver_get_info() as $archive) { + foreach ($archive['extensions'] as $extension) { + foreach (explode('.', $extension) as $part) { + if (!in_array($part, $valid_extensions)) { + $valid_extensions[] = $part; + } + } + } + } + return implode(' ', $valid_extensions); +} + +/** + * Create the appropriate archiver for the specified file. + * + * @param $file + * The full path of the archive file. Note that stream wrapper + * paths are supported, but not remote ones. + * @return + * A newly created instance of the archiver class appropriate + * for the specified file, already bound to that file. + * If no appropriate archiver class was found, will return FALSE. + */ +function archiver_get_archiver($file) { + // Archivers can only work on local paths + $filepath = drupal_realpath($file); + if (!is_file($filepath)) { + throw new Exception(t('Archivers can only operate on local files: %file not supported', array('%file' => $file))); + } + $archiver_info = archiver_get_info(); + + foreach ($archiver_info as $implementation) { + foreach ($implementation['extensions'] as $extension) { + // Because extensions may be multi-part, such as .tar.gz, + // we cannot use simpler approaches like substr() or pathinfo(). + // This method isn't quite as clean but gets the job done. + // Also note that the file may not yet exist, so we cannot rely + // on fileinfo() or other disk-level utilities. + if (strrpos($filepath, '.' . $extension) === strlen($filepath) - strlen('.' . $extension)) { + return new $implementation['class']($filepath); + } + } + } +} + +/** + * Drupal Updater registry. + * + * An Updater is a class that knows how to update various parts of the Drupal + * file system, for example to update modules that have newer releases, or to + * install a new theme. + * + * @return + * Returns the Drupal Updater class registry. + * + * @see hook_updater_info() + * @see hook_updater_info_alter() + */ +function drupal_get_updaters() { + $updaters = &drupal_static(__FUNCTION__); + if (!isset($updaters)) { + $updaters = module_invoke_all('updater_info'); + drupal_alter('updater_info', $updaters); + uasort($updaters, 'drupal_sort_weight'); + } + return $updaters; +} + +/** + * Drupal FileTransfer registry. + * + * @return + * Returns the Drupal FileTransfer class registry. + * + * @see FileTransfer + * @see hook_filetransfer_info() + * @see hook_filetransfer_info_alter() + */ +function drupal_get_filetransfer_info() { + $info = &drupal_static(__FUNCTION__); + if (!isset($info)) { + // Since we have to manually set the 'file path' default for each + // module separately, we can't use module_invoke_all(). + $info = array(); + foreach (module_implements('filetransfer_info') as $module) { + $function = $module . '_filetransfer_info'; + if (function_exists($function)) { + $result = $function(); + if (isset($result) && is_array($result)) { + foreach ($result as &$values) { + if (empty($values['file path'])) { + $values['file path'] = drupal_get_path('module', $module); + } + } + $info = array_merge_recursive($info, $result); + } + } + } + drupal_alter('filetransfer_info', $info); + uasort($info, 'drupal_sort_weight'); + } + return $info; +} diff --git a/core/includes/database/database.inc b/core/includes/database/database.inc new file mode 100644 index 0000000..3dece65 --- /dev/null +++ b/core/includes/database/database.inc @@ -0,0 +1,2966 @@ + $uid)); + * foreach ($result as $record) { + * // Perform operations on $node->title, etc. here. + * } + * @endcode + * Curly braces are used around "node" to provide table prefixing via + * DatabaseConnection::prefixTables(). The explicit use of a user ID is pulled + * out into an argument passed to db_query() so that SQL injection attacks + * from user input can be caught and nullified. The LIMIT syntax varies between + * database servers, so that is abstracted into db_query_range() arguments. + * Finally, note the PDO-based ability to iterate over the result set using + * foreach (). + * + * All queries are passed as a prepared statement string. A + * prepared statement is a "template" of a query that omits literal or variable + * values in favor of placeholders. The values to place into those + * placeholders are passed separately, and the database driver handles + * inserting the values into the query in a secure fashion. That means you + * should never quote or string-escape a value to be inserted into the query. + * + * There are two formats for placeholders: named and unnamed. Named placeholders + * are strongly preferred in all cases as they are more flexible and + * self-documenting. Named placeholders should start with a colon ":" and can be + * followed by one or more letters, numbers or underscores. + * + * Named placeholders begin with a colon followed by a unique string. Example: + * @code + * SELECT nid, title FROM {node} WHERE uid=:uid; + * @endcode + * + * ":uid" is a placeholder that will be replaced with a literal value when + * the query is executed. A given placeholder label cannot be repeated in a + * given query, even if the value should be the same. When using named + * placeholders, the array of arguments to the query must be an associative + * array where keys are a placeholder label (e.g., :uid) and the value is the + * corresponding value to use. The array may be in any order. + * + * Unnamed placeholders are simply a question mark. Example: + * @code + * SELECT nid, title FROM {node} WHERE uid=?; + * @endcode + * + * In this case, the array of arguments must be an indexed array of values to + * use in the exact same order as the placeholders in the query. + * + * Note that placeholders should be a "complete" value. For example, when + * running a LIKE query the SQL wildcard character, %, should be part of the + * value, not the query itself. Thus, the following is incorrect: + * @code + * SELECT nid, title FROM {node} WHERE title LIKE :title%; + * @endcode + * It should instead read: + * @code + * SELECT nid, title FROM {node} WHERE title LIKE :title; + * @endcode + * and the value for :title should include a % as appropriate. Again, note the + * lack of quotation marks around :title. Because the value is not inserted + * into the query as one big string but as an explicitly separate value, the + * database server knows where the query ends and a value begins. That is + * considerably more secure against SQL injection than trying to remember + * which values need quotation marks and string escaping and which don't. + * + * INSERT, UPDATE, and DELETE queries need special care in order to behave + * consistently across all different databases. Therefore, they use a special + * object-oriented API for defining a query structurally. For example, rather + * than: + * @code + * INSERT INTO node (nid, title, body) VALUES (1, 'my title', 'my body'); + * @endcode + * one would instead write: + * @code + * $fields = array('nid' => 1, 'title' => 'my title', 'body' => 'my body'); + * db_insert('node')->fields($fields)->execute(); + * @endcode + * This method allows databases that need special data type handling to do so, + * while also allowing optimizations such as multi-insert queries. UPDATE and + * DELETE queries have a similar pattern. + * + * Drupal also supports transactions, including a transparent fallback for + * databases that do not support transactions. To start a new transaction, + * simply call $txn = db_transaction(); in your own code. The transaction will + * remain open for as long as the variable $txn remains in scope. When $txn is + * destroyed, the transaction will be committed. If your transaction is nested + * inside of another then Drupal will track each transaction and only commit + * the outer-most transaction when the last transaction object goes out out of + * scope, that is, all relevant queries completed successfully. + * + * Example: + * @code + * function my_transaction_function() { + * // The transaction opens here. + * $txn = db_transaction(); + * + * try { + * $id = db_insert('example') + * ->fields(array( + * 'field1' => 'mystring', + * 'field2' => 5, + * )) + * ->execute(); + * + * my_other_function($id); + * + * return $id; + * } + * catch (Exception $e) { + * // Something went wrong somewhere, so roll back now. + * $txn->rollback(); + * // Log the exception to watchdog. + * watchdog_exception('type', $e); + * } + * + * // $txn goes out of scope here. Unless the transaction was rolled back, it + * // gets automatically commited here. + * } + * + * function my_other_function($id) { + * // The transaction is still open here. + * + * if ($id % 2 == 0) { + * db_update('example') + * ->condition('id', $id) + * ->fields(array('field2' => 10)) + * ->execute(); + * } + * } + * @endcode + * + * @link http://drupal.org/developing/api/database + */ + + +/** + * Base Database API class. + * + * This class provides a Drupal-specific extension of the PDO database + * abstraction class in PHP. Every database driver implementation must provide a + * concrete implementation of it to support special handling required by that + * database. + * + * @see http://php.net/manual/en/book.pdo.php + */ +abstract class DatabaseConnection extends PDO { + + /** + * The database target this connection is for. + * + * We need this information for later auditing and logging. + * + * @var string + */ + protected $target = NULL; + + /** + * The key representing this connection. + * + * The key is a unique string which identifies a database connection. A + * connection can be a single server or a cluster of master and slaves (use + * target to pick between master and slave). + * + * @var string + */ + protected $key = NULL; + + /** + * The current database logging object for this connection. + * + * @var DatabaseLog + */ + protected $logger = NULL; + + /** + * Tracks the number of "layers" of transactions currently active. + * + * On many databases transactions cannot nest. Instead, we track + * nested calls to transactions and collapse them into a single + * transaction. + * + * @var array + */ + protected $transactionLayers = array(); + + /** + * Index of what driver-specific class to use for various operations. + * + * @var array + */ + protected $driverClasses = array(); + + /** + * The name of the Statement class for this connection. + * + * @var string + */ + protected $statementClass = 'DatabaseStatementBase'; + + /** + * Whether this database connection supports transactions. + * + * @var bool + */ + protected $transactionSupport = TRUE; + + /** + * Whether this database connection supports transactional DDL. + * + * Set to FALSE by default because few databases support this feature. + * + * @var bool + */ + protected $transactionalDDLSupport = FALSE; + + /** + * An index used to generate unique temporary table names. + * + * @var integer + */ + protected $temporaryNameIndex = 0; + + /** + * The connection information for this connection object. + * + * @var array + */ + protected $connectionOptions = array(); + + /** + * The schema object for this connection. + * + * @var object + */ + protected $schema = NULL; + + /** + * The default prefix used by this database connection. + * + * Separated from the other prefixes for performance reasons. + * + * @var string + */ + protected $defaultPrefix = ''; + + /** + * The non-default prefixes used by this database connection. + * + * @var array + */ + protected $prefixes = array(); + + function __construct($dsn, $username, $password, $driver_options = array()) { + // Initialize and prepare the connection prefix. + $this->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : ''); + + // Because the other methods don't seem to work right. + $driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; + + // Call PDO::__construct and PDO::setAttribute. + parent::__construct($dsn, $username, $password, $driver_options); + + // Set a specific PDOStatement class if the driver requires that. + if (!empty($this->statementClass)) { + $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this))); + } + } + + /** + * Returns the default query options for any given query. + * + * A given query can be customized with a number of option flags in an + * associative array: + * - target: The database "target" against which to execute a query. Valid + * values are "default" or "slave". The system will first try to open a + * connection to a database specified with the user-supplied key. If one + * is not available, it will silently fall back to the "default" target. + * If multiple databases connections are specified with the same target, + * one will be selected at random for the duration of the request. + * - fetch: This element controls how rows from a result set will be + * returned. Legal values include PDO::FETCH_ASSOC, PDO::FETCH_BOTH, + * PDO::FETCH_OBJ, PDO::FETCH_NUM, or a string representing the name of a + * class. If a string is specified, each record will be fetched into a new + * object of that class. The behavior of all other values is defined by PDO. + * See http://php.net/manual/pdostatement.fetch.php + * - return: Depending on the type of query, different return values may be + * meaningful. This directive instructs the system which type of return + * value is desired. The system will generally set the correct value + * automatically, so it is extremely rare that a module developer will ever + * need to specify this value. Setting it incorrectly will likely lead to + * unpredictable results or fatal errors. Legal values include: + * - Database::RETURN_STATEMENT: Return the prepared statement object for + * the query. This is usually only meaningful for SELECT queries, where + * the statement object is how one accesses the result set returned by the + * query. + * - Database::RETURN_AFFECTED: Return the number of rows affected by an + * UPDATE or DELETE query. Be aware that means the number of rows actually + * changed, not the number of rows matched by the WHERE clause. + * - Database::RETURN_INSERT_ID: Return the sequence ID (primary key) + * created by an INSERT statement on a table that contains a serial + * column. + * - Database::RETURN_NULL: Do not return anything, as there is no + * meaningful value to return. That is the case for INSERT queries on + * tables that do not contain a serial column. + * - throw_exception: By default, the database system will catch any errors + * on a query as an Exception, log it, and then rethrow it so that code + * further up the call chain can take an appropriate action. To suppress + * that behavior and simply return NULL on failure, set this option to + * FALSE. + * + * @return + * An array of default query options. + */ + protected function defaultOptions() { + return array( + 'target' => 'default', + 'fetch' => PDO::FETCH_OBJ, + 'return' => Database::RETURN_STATEMENT, + 'throw_exception' => TRUE, + ); + } + + /** + * Returns the connection information for this connection object. + * + * Note that Database::getConnectionInfo() is for requesting information + * about an arbitrary database connection that is defined. This method + * is for requesting the connection information of this specific + * open connection object. + * + * @return + * An array of the connection information. The exact list of + * properties is driver-dependent. + */ + public function getConnectionOptions() { + return $this->connectionOptions; + } + + /** + * Preprocess the prefixes used by this database connection. + * + * @param $prefix + * The prefixes, in any of the multiple forms documented in + * default.settings.php. + */ + protected function setPrefix($prefix) { + if (is_array($prefix)) { + $this->defaultPrefix = isset($prefix['default']) ? $prefix['default'] : ''; + unset($prefix['default']); + $this->prefixes = $prefix; + } + else { + $this->defaultPrefix = $prefix; + $this->prefixes = array(); + } + } + + /** + * Appends a database prefix to all tables in a query. + * + * Queries sent to Drupal should wrap all table names in curly brackets. This + * function searches for this syntax and adds Drupal's table prefix to all + * tables, allowing Drupal to coexist with other systems in the same database + * and/or schema if necessary. + * + * @param $sql + * A string containing a partial or entire SQL query. + * + * @return + * The properly-prefixed string. + */ + public function prefixTables($sql) { + // Replace specific table prefixes first. + foreach ($this->prefixes as $key => $val) { + $sql = strtr($sql, array('{' . $key . '}' => $val . $key)); + } + // Then replace remaining tables with the default prefix. + return strtr($sql, array('{' => $this->defaultPrefix , '}' => '')); + } + + /** + * Find the prefix for a table. + * + * This function is for when you want to know the prefix of a table. This + * is not used in prefixTables due to performance reasons. + */ + public function tablePrefix($table = 'default') { + if (isset($this->prefixes[$table])) { + return $this->prefixes[$table]; + } + else { + return $this->defaultPrefix; + } + } + + /** + * Prepares a query string and returns the prepared statement. + * + * This method caches prepared statements, reusing them when + * possible. It also prefixes tables names enclosed in curly-braces. + * + * @param $query + * The query string as SQL, with curly-braces surrounding the + * table names. + * + * @return DatabaseStatementInterface + * A PDO prepared statement ready for its execute() method. + */ + public function prepareQuery($query) { + $query = $this->prefixTables($query); + + // Call PDO::prepare. + return parent::prepare($query); + } + + /** + * Tells this connection object what its target value is. + * + * This is needed for logging and auditing. It's sloppy to do in the + * constructor because the constructor for child classes has a different + * signature. We therefore also ensure that this function is only ever + * called once. + * + * @param $target + * The target this connection is for. Set to NULL (default) to disable + * logging entirely. + */ + public function setTarget($target = NULL) { + if (!isset($this->target)) { + $this->target = $target; + } + } + + /** + * Returns the target this connection is associated with. + * + * @return + * The target string of this connection. + */ + public function getTarget() { + return $this->target; + } + + /** + * Tells this connection object what its key is. + * + * @param $target + * The key this connection is for. + */ + public function setKey($key) { + if (!isset($this->key)) { + $this->key = $key; + } + } + + /** + * Returns the key this connection is associated with. + * + * @return + * The key of this connection. + */ + public function getKey() { + return $this->key; + } + + /** + * Associates a logging object with this connection. + * + * @param $logger + * The logging object we want to use. + */ + public function setLogger(DatabaseLog $logger) { + $this->logger = $logger; + } + + /** + * Gets the current logging object for this connection. + * + * @return DatabaseLog + * The current logging object for this connection. If there isn't one, + * NULL is returned. + */ + public function getLogger() { + return $this->logger; + } + + /** + * Creates the appropriate sequence name for a given table and serial field. + * + * This information is exposed to all database drivers, although it is only + * useful on some of them. This method is table prefix-aware. + * + * @param $table + * The table name to use for the sequence. + * @param $field + * The field name to use for the sequence. + * + * @return + * A table prefix-parsed string for the sequence name. + */ + public function makeSequenceName($table, $field) { + return $this->prefixTables('{' . $table . '}_' . $field . '_seq'); + } + + /** + * Flatten an array of query comments into a single comment string. + * + * The comment string will be sanitized to avoid SQL injection attacks. + * + * @param $comments + * An array of query comment strings. + * + * @return + * A sanitized comment string. + */ + public function makeComment($comments) { + if (empty($comments)) + return ''; + + // Flatten the array of comments. + $comment = implode('; ', $comments); + + // Sanitize the comment string so as to avoid SQL injection attacks. + return '/* ' . $this->filterComment($comment) . ' */ '; + } + + /** + * Sanitize a query comment string. + * + * Ensure a query comment does not include strings such as "* /" that might + * terminate the comment early. This avoids SQL injection attacks via the + * query comment. The comment strings in this example are separated by a + * space to avoid PHP parse errors. + * + * For example, the comment: + * @code + * db_update('example') + * ->condition('id', $id) + * ->fields(array('field2' => 10)) + * ->comment('Exploit * / DROP TABLE node; --') + * ->execute() + * @endcode + * + * Would result in the following SQL statement being generated: + * @code + * "/ * Exploit * / DROP TABLE node; -- * / UPDATE example SET field2=..." + * @endcode + * + * Unless the comment is sanitised first, the SQL server would drop the + * node table and ignore the rest of the SQL statement. + * + * @param $comment + * A query comment string. + * + * @return + * A sanitized version of the query comment string. + */ + protected function filterComment($comment = '') { + return preg_replace('/(\/\*\s*)|(\s*\*\/)/', '', $comment); + } + + /** + * Executes a query string against the database. + * + * This method provides a central handler for the actual execution of every + * query. All queries executed by Drupal are executed as PDO prepared + * statements. + * + * @param $query + * The query to execute. In most cases this will be a string containing + * an SQL query with placeholders. An already-prepared instance of + * DatabaseStatementInterface may also be passed in order to allow calling + * code to manually bind variables to a query. If a + * DatabaseStatementInterface is passed, the $args array will be ignored. + * It is extremely rare that module code will need to pass a statement + * object to this method. It is used primarily for database drivers for + * databases that require special LOB field handling. + * @param $args + * An array of arguments for the prepared statement. If the prepared + * statement uses ? placeholders, this array must be an indexed array. + * If it contains named placeholders, it must be an associative array. + * @param $options + * An associative array of options to control how the query is run. See + * the documentation for DatabaseConnection::defaultOptions() for details. + * + * @return DatabaseStatementInterface + * This method will return one of: the executed statement, the number of + * rows affected by the query (not the number matched), or the generated + * insert IT of the last query, depending on the value of + * $options['return']. Typically that value will be set by default or a + * query builder and should not be set by a user. If there is an error, + * this method will return NULL and may throw an exception if + * $options['throw_exception'] is TRUE. + * + * @throws PDOException + */ + public function query($query, array $args = array(), $options = array()) { + + // Use default values if not already set. + $options += $this->defaultOptions(); + + try { + // We allow either a pre-bound statement object or a literal string. + // In either case, we want to end up with an executed statement object, + // which we pass to PDOStatement::execute. + if ($query instanceof DatabaseStatementInterface) { + $stmt = $query; + $stmt->execute(NULL, $options); + } + else { + $this->expandArguments($query, $args); + $stmt = $this->prepareQuery($query); + $stmt->execute($args, $options); + } + + // Depending on the type of query we may need to return a different value. + // See DatabaseConnection::defaultOptions() for a description of each + // value. + switch ($options['return']) { + case Database::RETURN_STATEMENT: + return $stmt; + case Database::RETURN_AFFECTED: + return $stmt->rowCount(); + case Database::RETURN_INSERT_ID: + return $this->lastInsertId(); + case Database::RETURN_NULL: + return; + default: + throw new PDOException('Invalid return directive: ' . $options['return']); + } + } + catch (PDOException $e) { + if ($options['throw_exception']) { + // Add additional debug information. + if ($query instanceof DatabaseStatementInterface) { + $e->query_string = $stmt->getQueryString(); + } + else { + $e->query_string = $query; + } + $e->args = $args; + throw $e; + } + return NULL; + } + } + + /** + * Expands out shorthand placeholders. + * + * Drupal supports an alternate syntax for doing arrays of values. We + * therefore need to expand them out into a full, executable query string. + * + * @param $query + * The query string to modify. + * @param $args + * The arguments for the query. + * + * @return + * TRUE if the query was modified, FALSE otherwise. + */ + protected function expandArguments(&$query, &$args) { + $modified = FALSE; + + // If the placeholder value to insert is an array, assume that we need + // to expand it out into a comma-delimited set of placeholders. + foreach (array_filter($args, 'is_array') as $key => $data) { + $new_keys = array(); + foreach ($data as $i => $value) { + // This assumes that there are no other placeholders that use the same + // name. For example, if the array placeholder is defined as :example + // and there is already an :example_2 placeholder, this will generate + // a duplicate key. We do not account for that as the calling code + // is already broken if that happens. + $new_keys[$key . '_' . $i] = $value; + } + + // Update the query with the new placeholders. + // preg_replace is necessary to ensure the replacement does not affect + // placeholders that start with the same exact text. For example, if the + // query contains the placeholders :foo and :foobar, and :foo has an + // array of values, using str_replace would affect both placeholders, + // but using the following preg_replace would only affect :foo because + // it is followed by a non-word character. + $query = preg_replace('#' . $key . '\b#', implode(', ', array_keys($new_keys)), $query); + + // Update the args array with the new placeholders. + unset($args[$key]); + $args += $new_keys; + + $modified = TRUE; + } + + return $modified; + } + + /** + * Gets the driver-specific override class if any for the specified class. + * + * @param string $class + * The class for which we want the potentially driver-specific class. + * @param array $files + * The name of the files in which the driver-specific class can be. + * @param $use_autoload + * If TRUE, attempt to load classes using PHP's autoload capability + * as well as the manual approach here. + * @return string + * The name of the class that should be used for this driver. + */ + public function getDriverClass($class, array $files = array(), $use_autoload = FALSE) { + if (empty($this->driverClasses[$class])) { + $driver = $this->driver(); + $this->driverClasses[$class] = $class . '_' . $driver; + Database::loadDriverFile($driver, $files); + if (!class_exists($this->driverClasses[$class], $use_autoload)) { + $this->driverClasses[$class] = $class; + } + } + return $this->driverClasses[$class]; + } + + /** + * Prepares and returns a SELECT query object. + * + * @param $table + * The base table for this query, that is, the first table in the FROM + * clause. This table will also be used as the "base" table for query_alter + * hook implementations. + * @param $alias + * The alias of the base table of this query. + * @param $options + * An array of options on the query. + * + * @return SelectQueryInterface + * An appropriate SelectQuery object for this database connection. Note that + * it may be a driver-specific subclass of SelectQuery, depending on the + * driver. + * + * @see SelectQuery + */ + public function select($table, $alias = NULL, array $options = array()) { + $class = $this->getDriverClass('SelectQuery', array('query.inc', 'select.inc')); + return new $class($table, $alias, $this, $options); + } + + /** + * Prepares and returns an INSERT query object. + * + * @param $options + * An array of options on the query. + * + * @return InsertQuery + * A new InsertQuery object. + * + * @see InsertQuery + */ + public function insert($table, array $options = array()) { + $class = $this->getDriverClass('InsertQuery', array('query.inc')); + return new $class($this, $table, $options); + } + + /** + * Prepares and returns a MERGE query object. + * + * @param $options + * An array of options on the query. + * + * @return MergeQuery + * A new MergeQuery object. + * + * @see MergeQuery + */ + public function merge($table, array $options = array()) { + $class = $this->getDriverClass('MergeQuery', array('query.inc')); + return new $class($this, $table, $options); + } + + + /** + * Prepares and returns an UPDATE query object. + * + * @param $options + * An array of options on the query. + * + * @return UpdateQuery + * A new UpdateQuery object. + * + * @see UpdateQuery + */ + public function update($table, array $options = array()) { + $class = $this->getDriverClass('UpdateQuery', array('query.inc')); + return new $class($this, $table, $options); + } + + /** + * Prepares and returns a DELETE query object. + * + * @param $options + * An array of options on the query. + * + * @return DeleteQuery + * A new DeleteQuery object. + * + * @see DeleteQuery + */ + public function delete($table, array $options = array()) { + $class = $this->getDriverClass('DeleteQuery', array('query.inc')); + return new $class($this, $table, $options); + } + + /** + * Prepares and returns a TRUNCATE query object. + * + * @param $options + * An array of options on the query. + * + * @return TruncateQuery + * A new TruncateQuery object. + * + * @see TruncateQuery + */ + public function truncate($table, array $options = array()) { + $class = $this->getDriverClass('TruncateQuery', array('query.inc')); + return new $class($this, $table, $options); + } + + /** + * Returns a DatabaseSchema object for manipulating the schema. + * + * This method will lazy-load the appropriate schema library file. + * + * @return DatabaseSchema + * The DatabaseSchema object for this connection. + */ + public function schema() { + if (empty($this->schema)) { + $class = $this->getDriverClass('DatabaseSchema', array('schema.inc')); + if (class_exists($class)) { + $this->schema = new $class($this); + } + } + return $this->schema; + } + + /** + * Escapes a table name string. + * + * Force all table names to be strictly alphanumeric-plus-underscore. + * For some database drivers, it may also wrap the table name in + * database-specific escape characters. + * + * @return + * The sanitized table name string. + */ + public function escapeTable($table) { + return preg_replace('/[^A-Za-z0-9_.]+/', '', $table); + } + + /** + * Escapes a field name string. + * + * Force all field names to be strictly alphanumeric-plus-underscore. + * For some database drivers, it may also wrap the field name in + * database-specific escape characters. + * + * @return + * The sanitized field name string. + */ + public function escapeField($field) { + return preg_replace('/[^A-Za-z0-9_.]+/', '', $field); + } + + /** + * Escapes an alias name string. + * + * Force all alias names to be strictly alphanumeric-plus-underscore. In + * contrast to DatabaseConnection::escapeField() / + * DatabaseConnection::escapeTable(), this doesn't allow the period (".") + * because that is not allowed in aliases. + * + * @return + * The sanitized field name string. + */ + public function escapeAlias($field) { + return preg_replace('/[^A-Za-z0-9_]+/', '', $field); + } + + /** + * Escapes characters that work as wildcard characters in a LIKE pattern. + * + * The wildcard characters "%" and "_" as well as backslash are prefixed with + * a backslash. Use this to do a search for a verbatim string without any + * wildcard behavior. + * + * For example, the following does a case-insensitive query for all rows whose + * name starts with $prefix: + * @code + * $result = db_query( + * 'SELECT * FROM person WHERE name LIKE :pattern', + * array(':pattern' => db_like($prefix) . '%') + * ); + * @endcode + * + * Backslash is defined as escape character for LIKE patterns in + * DatabaseCondition::mapConditionOperator(). + * + * @param $string + * The string to escape. + * + * @return + * The escaped string. + */ + public function escapeLike($string) { + return addcslashes($string, '\%_'); + } + + /** + * Determines if there is an active transaction open. + * + * @return + * TRUE if we're currently in a transaction, FALSE otherwise. + */ + public function inTransaction() { + return ($this->transactionDepth() > 0); + } + + /** + * Determines current transaction depth. + */ + public function transactionDepth() { + return count($this->transactionLayers); + } + + /** + * Returns a new DatabaseTransaction object on this connection. + * + * @param $name + * Optional name of the savepoint. + * + * @see DatabaseTransaction + */ + public function startTransaction($name = '') { + $class = $this->getDriverClass('DatabaseTransaction'); + return new $class($this, $name); + } + + /** + * Rolls back the transaction entirely or to a named savepoint. + * + * This method throws an exception if no transaction is active. + * + * @param $savepoint_name + * The name of the savepoint. The default, 'drupal_transaction', will roll + * the entire transaction back. + * + * @throws DatabaseTransactionNoActiveException + * + * @see DatabaseTransaction::rollback() + */ + public function rollback($savepoint_name = 'drupal_transaction') { + if (!$this->supportsTransactions()) { + return; + } + if (!$this->inTransaction()) { + throw new DatabaseTransactionNoActiveException(); + } + // A previous rollback to an earlier savepoint may mean that the savepoint + // in question has already been rolled back. + if (!in_array($savepoint_name, $this->transactionLayers)) { + return; + } + + // We need to find the point we're rolling back to, all other savepoints + // before are no longer needed. + while ($savepoint = array_pop($this->transactionLayers)) { + if ($savepoint == $savepoint_name) { + // If it is the last the transaction in the stack, then it is not a + // savepoint, it is the transaction itself so we will need to roll back + // the transaction rather than a savepoint. + if (empty($this->transactionLayers)) { + break; + } + $this->query('ROLLBACK TO SAVEPOINT ' . $savepoint); + return; + } + } + parent::rollBack(); + } + + /** + * Increases the depth of transaction nesting. + * + * If no transaction is already active, we begin a new transaction. + * + * @throws DatabaseTransactionNameNonUniqueException + * + * @see DatabaseTransaction + */ + public function pushTransaction($name) { + if (!$this->supportsTransactions()) { + return; + } + if (isset($this->transactionLayers[$name])) { + throw new DatabaseTransactionNameNonUniqueException($name . " is already in use."); + } + // If we're already in a transaction then we want to create a savepoint + // rather than try to create another transaction. + if ($this->inTransaction()) { + $this->query('SAVEPOINT ' . $name); + } + else { + parent::beginTransaction(); + } + $this->transactionLayers[$name] = $name; + } + + /** + * Decreases the depth of transaction nesting. + * + * If we pop off the last transaction layer, then we either commit or roll + * back the transaction as necessary. If no transaction is active, we return + * because the transaction may have manually been rolled back. + * + * @param $name + * The name of the savepoint + * + * @throws DatabaseTransactionNoActiveException + * @throws DatabaseTransactionCommitFailedException + * + * @see DatabaseTransaction + */ + public function popTransaction($name) { + if (!$this->supportsTransactions()) { + return; + } + if (!$this->inTransaction()) { + throw new DatabaseTransactionNoActiveException(); + } + + // Commit everything since SAVEPOINT $name. + while($savepoint = array_pop($this->transactionLayers)) { + if ($savepoint != $name) continue; + + // If there are no more layers left then we should commit. + if (empty($this->transactionLayers)) { + if (!parent::commit()) { + throw new DatabaseTransactionCommitFailedException(); + } + } + else { + $this->query('RELEASE SAVEPOINT ' . $name); + break; + } + } + } + + /** + * Runs a limited-range query on this database object. + * + * Use this as a substitute for ->query() when a subset of the query is to be + * returned. User-supplied arguments to the query should be passed in as + * separate parameters so that they can be properly escaped to avoid SQL + * injection attacks. + * + * @param $query + * A string containing an SQL query. + * @param $args + * An array of values to substitute into the query at placeholder markers. + * @param $from + * The first result row to return. + * @param $count + * The maximum number of result rows to return. + * @param $options + * An array of options on the query. + * + * @return DatabaseStatementInterface + * A database query result resource, or NULL if the query was not executed + * correctly. + */ + abstract public function queryRange($query, $from, $count, array $args = array(), array $options = array()); + + /** + * Generates a temporary table name. + * + * @return + * A table name. + */ + protected function generateTemporaryTableName() { + return "db_temporary_" . $this->temporaryNameIndex++; + } + + /** + * Runs a SELECT query and stores its results in a temporary table. + * + * Use this as a substitute for ->query() when the results need to stored + * in a temporary table. Temporary tables exist for the duration of the page + * request. User-supplied arguments to the query should be passed in as + * separate parameters so that they can be properly escaped to avoid SQL + * injection attacks. + * + * Note that if you need to know how many results were returned, you should do + * a SELECT COUNT(*) on the temporary table afterwards. + * + * @param $query + * A string containing a normal SELECT SQL query. + * @param $args + * An array of values to substitute into the query at placeholder markers. + * @param $options + * An associative array of options to control how the query is run. See + * the documentation for DatabaseConnection::defaultOptions() for details. + * + * @return + * The name of the temporary table. + */ + abstract function queryTemporary($query, array $args = array(), array $options = array()); + + /** + * Returns the type of database driver. + * + * This is not necessarily the same as the type of the database itself. For + * instance, there could be two MySQL drivers, mysql and mysql_mock. This + * function would return different values for each, but both would return + * "mysql" for databaseType(). + */ + abstract public function driver(); + + /** + * Returns the version of the database server. + */ + public function version() { + return $this->getAttribute(PDO::ATTR_SERVER_VERSION); + } + + /** + * Determines if this driver supports transactions. + * + * @return + * TRUE if this connection supports transactions, FALSE otherwise. + */ + public function supportsTransactions() { + return $this->transactionSupport; + } + + /** + * Determines if this driver supports transactional DDL. + * + * DDL queries are those that change the schema, such as ALTER queries. + * + * @return + * TRUE if this connection supports transactions for DDL queries, FALSE + * otherwise. + */ + public function supportsTransactionalDDL() { + return $this->transactionalDDLSupport; + } + + /** + * Returns the name of the PDO driver for this connection. + */ + abstract public function databaseType(); + + + /** + * Gets any special processing requirements for the condition operator. + * + * Some condition types require special processing, such as IN, because + * the value data they pass in is not a simple value. This is a simple + * overridable lookup function. Database connections should define only + * those operators they wish to be handled differently than the default. + * + * @param $operator + * The condition operator, such as "IN", "BETWEEN", etc. Case-sensitive. + * + * @return + * The extra handling directives for the specified operator, or NULL. + * + * @see DatabaseCondition::compile() + */ + abstract public function mapConditionOperator($operator); + + /** + * Throws an exception to deny direct access to transaction commits. + * + * We do not want to allow users to commit transactions at any time, only + * by destroying the transaction object or allowing it to go out of scope. + * A direct commit bypasses all of the safety checks we've built on top of + * PDO's transaction routines. + * + * @throws DatabaseTransactionExplicitCommitNotAllowedException + * + * @see DatabaseTransaction + */ + public function commit() { + throw new DatabaseTransactionExplicitCommitNotAllowedException(); + } + + /** + * Retrieves an unique id from a given sequence. + * + * Use this function if for some reason you can't use a serial field. For + * example, MySQL has no ways of reading of the current value of a sequence + * and PostgreSQL can not advance the sequence to be larger than a given + * value. Or sometimes you just need a unique integer. + * + * @param $existing_id + * After a database import, it might be that the sequences table is behind, + * so by passing in the maximum existing id, it can be assured that we + * never issue the same id. + * + * @return + * An integer number larger than any number returned by earlier calls and + * also larger than the $existing_id if one was passed in. + */ + abstract public function nextId($existing_id = 0); +} + +/** + * Primary front-controller for the database system. + * + * This class is uninstantiatable and un-extendable. It acts to encapsulate + * all control and shepherding of database connections into a single location + * without the use of globals. + */ +abstract class Database { + + /** + * Flag to indicate a query call should simply return NULL. + * + * This is used for queries that have no reasonable return value anyway, such + * as INSERT statements to a table without a serial primary key. + */ + const RETURN_NULL = 0; + + /** + * Flag to indicate a query call should return the prepared statement. + */ + const RETURN_STATEMENT = 1; + + /** + * Flag to indicate a query call should return the number of affected rows. + */ + const RETURN_AFFECTED = 2; + + /** + * Flag to indicate a query call should return the "last insert id". + */ + const RETURN_INSERT_ID = 3; + + /** + * An nested array of all active connections. It is keyed by database name + * and target. + * + * @var array + */ + static protected $connections = array(); + + /** + * A processed copy of the database connection information from settings.php. + * + * @var array + */ + static protected $databaseInfo = NULL; + + /** + * A list of key/target credentials to simply ignore. + * + * @var array + */ + static protected $ignoreTargets = array(); + + /** + * The key of the currently active database connection. + * + * @var string + */ + static protected $activeKey = 'default'; + + /** + * An array of active query log objects. + * + * Every connection has one and only one logger object for all targets and + * logging keys. + * + * array( + * '$db_key' => DatabaseLog object. + * ); + * + * @var array + */ + static protected $logs = array(); + + /** + * Starts logging a given logging key on the specified connection. + * + * @param $logging_key + * The logging key to log. + * @param $key + * The database connection key for which we want to log. + * + * @return DatabaseLog + * The query log object. Note that the log object does support richer + * methods than the few exposed through the Database class, so in some + * cases it may be desirable to access it directly. + * + * @see DatabaseLog + */ + final public static function startLog($logging_key, $key = 'default') { + if (empty(self::$logs[$key])) { + self::$logs[$key] = new DatabaseLog($key); + + // Every target already active for this connection key needs to have the + // logging object associated with it. + if (!empty(self::$connections[$key])) { + foreach (self::$connections[$key] as $connection) { + $connection->setLogger(self::$logs[$key]); + } + } + } + + self::$logs[$key]->start($logging_key); + return self::$logs[$key]; + } + + /** + * Retrieves the queries logged on for given logging key. + * + * This method also ends logging for the specified key. To get the query log + * to date without ending the logger request the logging object by starting + * it again (which does nothing to an open log key) and call methods on it as + * desired. + * + * @param $logging_key + * The logging key to log. + * @param $key + * The database connection key for which we want to log. + * + * @return array + * The query log for the specified logging key and connection. + * + * @see DatabaseLog + */ + final public static function getLog($logging_key, $key = 'default') { + if (empty(self::$logs[$key])) { + return NULL; + } + $queries = self::$logs[$key]->get($logging_key); + self::$logs[$key]->end($logging_key); + return $queries; + } + + /** + * Gets the connection object for the specified database key and target. + * + * Note: do not use the setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE) on the + * returned object because of http://bugs.php.net/bug.php?id=43139. + * + * @param $target + * The database target name. + * @param $key + * The database connection key. Defaults to NULL which means the active key. + * + * @return DatabaseConnection + * The corresponding connection object. + */ + final public static function getConnection($target = 'default', $key = NULL) { + if (!isset($key)) { + // By default, we want the active connection, set in setActiveConnection. + $key = self::$activeKey; + } + // If the requested target does not exist, or if it is ignored, we fall back + // to the default target. The target is typically either "default" or + // "slave", indicating to use a slave SQL server if one is available. If + // it's not available, then the default/master server is the correct server + // to use. + if (!empty(self::$ignoreTargets[$key][$target]) || !isset(self::$databaseInfo[$key][$target])) { + $target = 'default'; + } + + if (!isset(self::$connections[$key][$target])) { + // If necessary, a new connection is opened. + self::$connections[$key][$target] = self::openConnection($key, $target); + } + return self::$connections[$key][$target]; + } + + /** + * Determines if there is an active connection. + * + * Note that this method will return FALSE if no connection has been + * established yet, even if one could be. + * + * @return + * TRUE if there is at least one database connection established, FALSE + * otherwise. + */ + final public static function isActiveConnection() { + return !empty(self::$activeKey) && !empty(self::$connections) && !empty(self::$connections[self::$activeKey]); + } + + /** + * Sets the active connection to the specified key. + * + * @return + * The previous database connection key. + */ + final public static function setActiveConnection($key = 'default') { + if (empty(self::$databaseInfo)) { + self::parseConnectionInfo(); + } + + if (!empty(self::$databaseInfo[$key])) { + $old_key = self::$activeKey; + self::$activeKey = $key; + return $old_key; + } + } + + /** + * Process the configuration file for database information. + */ + final public static function parseConnectionInfo() { + global $databases; + + $database_info = is_array($databases) ? $databases : array(); + foreach ($database_info as $index => $info) { + foreach ($database_info[$index] as $target => $value) { + // If there is no "driver" property, then we assume it's an array of + // possible connections for this target. Pick one at random. That allows + // us to have, for example, multiple slave servers. + if (empty($value['driver'])) { + $database_info[$index][$target] = $database_info[$index][$target][mt_rand(0, count($database_info[$index][$target]) - 1)]; + } + + // Parse the prefix information. + if (!isset($database_info[$index][$target]['prefix'])) { + // Default to an empty prefix. + $database_info[$index][$target]['prefix'] = array( + 'default' => '', + ); + } + elseif (!is_array($database_info[$index][$target]['prefix'])) { + // Transform the flat form into an array form. + $database_info[$index][$target]['prefix'] = array( + 'default' => $database_info[$index][$target]['prefix'], + ); + } + } + } + + if (!is_array(self::$databaseInfo)) { + self::$databaseInfo = $database_info; + } + + // Merge the new $database_info into the existing. + // array_merge_recursive() cannot be used, as it would make multiple + // database, user, and password keys in the same database array. + else { + foreach ($database_info as $database_key => $database_values) { + foreach ($database_values as $target => $target_values) { + self::$databaseInfo[$database_key][$target] = $target_values; + } + } + } + } + + /** + * Adds database connection information for a given key/target. + * + * This method allows the addition of new connection credentials at runtime. + * Under normal circumstances the preferred way to specify database + * credentials is via settings.php. However, this method allows them to be + * added at arbitrary times, such as during unit tests, when connecting to + * admin-defined third party databases, etc. + * + * If the given key/target pair already exists, this method will be ignored. + * + * @param $key + * The database key. + * @param $target + * The database target name. + * @param $info + * The database connection information, as it would be defined in + * settings.php. Note that the structure of this array will depend on the + * database driver it is connecting to. + */ + public static function addConnectionInfo($key, $target, $info) { + if (empty(self::$databaseInfo[$key][$target])) { + self::$databaseInfo[$key][$target] = $info; + } + } + + /** + * Gets information on the specified database connection. + * + * @param $connection + * The connection key for which we want information. + */ + final public static function getConnectionInfo($key = 'default') { + if (empty(self::$databaseInfo)) { + self::parseConnectionInfo(); + } + + if (!empty(self::$databaseInfo[$key])) { + return self::$databaseInfo[$key]; + } + } + + /** + * Rename a connection and its corresponding connection information. + * + * @param $old_key + * The old connection key. + * @param $new_key + * The new connection key. + * @return + * TRUE in case of success, FALSE otherwise. + */ + final public static function renameConnection($old_key, $new_key) { + if (empty(self::$databaseInfo)) { + self::parseConnectionInfo(); + } + + if (!empty(self::$databaseInfo[$old_key]) && empty(self::$databaseInfo[$new_key])) { + // Migrate the database connection information. + self::$databaseInfo[$new_key] = self::$databaseInfo[$old_key]; + unset(self::$databaseInfo[$old_key]); + + // Migrate over the DatabaseConnection object if it exists. + if (isset(self::$connections[$old_key])) { + self::$connections[$new_key] = self::$connections[$old_key]; + unset(self::$connections[$old_key]); + } + + return TRUE; + } + else { + return FALSE; + } + } + + /** + * Remove a connection and its corresponding connection information. + * + * @param $key + * The connection key. + * @return + * TRUE in case of success, FALSE otherwise. + */ + final public static function removeConnection($key) { + if (isset(self::$databaseInfo[$key])) { + unset(self::$databaseInfo[$key]); + unset(self::$connections[$key]); + return TRUE; + } + else { + return FALSE; + } + } + + /** + * Opens a connection to the server specified by the given key and target. + * + * @param $key + * The database connection key, as specified in settings.php. The default is + * "default". + * @param $target + * The database target to open. + * + * @throws DatabaseConnectionNotDefinedException + * @throws DatabaseDriverNotSpecifiedException + */ + final protected static function openConnection($key, $target) { + if (empty(self::$databaseInfo)) { + self::parseConnectionInfo(); + } + + // If the requested database does not exist then it is an unrecoverable + // error. + if (!isset(self::$databaseInfo[$key])) { + throw new DatabaseConnectionNotDefinedException('The specified database connection is not defined: ' . $key); + } + + if (!$driver = self::$databaseInfo[$key][$target]['driver']) { + throw new DatabaseDriverNotSpecifiedException('Driver not specified for this database connection: ' . $key); + } + + // We cannot rely on the registry yet, because the registry requires an + // open database connection. + $driver_class = 'DatabaseConnection_' . $driver; + require_once DRUPAL_ROOT . '/core/includes/database/' . $driver . '/database.inc'; + $new_connection = new $driver_class(self::$databaseInfo[$key][$target]); + $new_connection->setTarget($target); + $new_connection->setKey($key); + + // If we have any active logging objects for this connection key, we need + // to associate them with the connection we just opened. + if (!empty(self::$logs[$key])) { + $new_connection->setLogger(self::$logs[$key]); + } + + return $new_connection; + } + + /** + * Closes a connection to the server specified by the given key and target. + * + * @param $target + * The database target name. Defaults to NULL meaning that all target + * connections will be closed. + * @param $key + * The database connection key. Defaults to NULL which means the active key. + */ + public static function closeConnection($target = NULL, $key = NULL) { + // Gets the active connection by default. + if (!isset($key)) { + $key = self::$activeKey; + } + // To close the connection, we need to unset the static variable. + if (isset($target)) { + unset(self::$connections[$key][$target]); + } + else { + unset(self::$connections[$key]); + } + } + + /** + * Instructs the system to temporarily ignore a given key/target. + * + * At times we need to temporarily disable slave queries. To do so, call this + * method with the database key and the target to disable. That database key + * will then always fall back to 'default' for that key, even if it's defined. + * + * @param $key + * The database connection key. + * @param $target + * The target of the specified key to ignore. + */ + public static function ignoreTarget($key, $target) { + self::$ignoreTargets[$key][$target] = TRUE; + } + + /** + * Load a file for the database that might hold a class. + * + * @param $driver + * The name of the driver. + * @param array $files + * The name of the files the driver specific class can be. + */ + public static function loadDriverFile($driver, array $files = array()) { + static $base_path; + + if (empty($base_path)) { + $base_path = dirname(realpath(__FILE__)); + } + + $driver_base_path = "$base_path/$driver"; + foreach ($files as $file) { + // Load the base file first so that classes extending base classes will + // have the base class loaded. + foreach (array("$base_path/$file", "$driver_base_path/$file") as $filename) { + // The OS caches file_exists() and PHP caches require_once(), so + // we'll let both of those take care of performance here. + if (file_exists($filename)) { + require_once $filename; + } + } + } + } +} + +/** + * Exception for when popTransaction() is called with no active transaction. + */ +class DatabaseTransactionNoActiveException extends Exception { } + +/** + * Exception thrown when a savepoint or transaction name occurs twice. + */ +class DatabaseTransactionNameNonUniqueException extends Exception { } + +/** + * Exception thrown when a commit() function fails. + */ +class DatabaseTransactionCommitFailedException extends Exception { } + +/** + * Exception to deny attempts to explicitly manage transactions. + * + * This exception will be thrown when the PDO connection commit() is called. + * Code should never call this method directly. + */ +class DatabaseTransactionExplicitCommitNotAllowedException extends Exception { } + +/** + * Exception thrown for merge queries that do not make semantic sense. + * + * There are many ways that a merge query could be malformed. They should all + * throw this exception and set an appropriately descriptive message. + */ +class InvalidMergeQueryException extends Exception {} + +/** + * Exception thrown if an insert query specifies a field twice. + * + * It is not allowed to specify a field as default and insert field, this + * exception is thrown if that is the case. + */ +class FieldsOverlapException extends Exception {} + +/** + * Exception thrown if an insert query doesn't specify insert or default fields. + */ +class NoFieldsException extends Exception {} + +/** + * Exception thrown if an undefined database connection is requested. + */ +class DatabaseConnectionNotDefinedException extends Exception {} + +/** + * Exception thrown if no driver is specified for a database connection. + */ +class DatabaseDriverNotSpecifiedException extends Exception {} + + +/** + * A wrapper class for creating and managing database transactions. + * + * Not all databases or database configurations support transactions. For + * example, MySQL MyISAM tables do not. It is also easy to begin a transaction + * and then forget to commit it, which can lead to connection errors when + * another transaction is started. + * + * This class acts as a wrapper for transactions. To begin a transaction, + * simply instantiate it. When the object goes out of scope and is destroyed + * it will automatically commit. It also will check to see if the specified + * connection supports transactions. If not, it will simply skip any transaction + * commands, allowing user-space code to proceed normally. The only difference + * is that rollbacks won't actually do anything. + * + * In the vast majority of cases, you should not instantiate this class + * directly. Instead, call ->startTransaction(), from the appropriate connection + * object. + */ +class DatabaseTransaction { + + /** + * The connection object for this transaction. + * + * @var DatabaseConnection + */ + protected $connection; + + /** + * A boolean value to indicate whether this transaction has been rolled back. + * + * @var Boolean + */ + protected $rolledBack = FALSE; + + /** + * The name of the transaction. + * + * This is used to label the transaction savepoint. It will be overridden to + * 'drupal_transaction' if there is no transaction depth. + */ + protected $name; + + public function __construct(DatabaseConnection &$connection, $name = NULL) { + $this->connection = &$connection; + // If there is no transaction depth, then no transaction has started. Name + // the transaction 'drupal_transaction'. + if (!$depth = $connection->transactionDepth()) { + $this->name = 'drupal_transaction'; + } + // Within transactions, savepoints are used. Each savepoint requires a + // name. So if no name is present we need to create one. + elseif (!$name) { + $this->name = 'savepoint_' . $depth; + } + else { + $this->name = $name; + } + $this->connection->pushTransaction($this->name); + } + + public function __destruct() { + // If we rolled back then the transaction would have already been popped. + if ($this->connection->inTransaction() && !$this->rolledBack) { + $this->connection->popTransaction($this->name); + } + } + + /** + * Retrieves the name of the transaction or savepoint. + */ + public function name() { + return $this->name; + } + + /** + * Rolls back the current transaction. + * + * This is just a wrapper method to rollback whatever transaction stack we are + * currently in, which is managed by the connection object itself. Note that + * logging (preferable with watchdog_exception()) needs to happen after a + * transaction has been rolled back or the log messages will be rolled back + * too. + * + * @see DatabaseConnection::rollback() + * @see watchdog_exception() + */ + public function rollback() { + $this->rolledBack = TRUE; + $this->connection->rollback($this->name); + } +} + +/** + * A prepared statement. + * + * Some methods in that class are purposely commented out. Due to a change in + * how PHP defines PDOStatement, we can't define a signature for those methods + * that will work the same way between versions older than 5.2.6 and later + * versions. + * + * Please refer to http://bugs.php.net/bug.php?id=42452 for more details. + * + * Child implementations should either extend PDOStatement: + * @code + * class DatabaseStatement_oracle extends PDOStatement implements DatabaseStatementInterface {} + * @endcode + * or implement their own class, but in that case they will also have to + * implement the Iterator or IteratorArray interfaces before + * DatabaseStatementInterface: + * @code + * class DatabaseStatement_oracle implements Iterator, DatabaseStatementInterface {} + * @endcode + */ +interface DatabaseStatementInterface extends Traversable { + + /** + * Executes a prepared statement + * + * @param $args + * An array of values with as many elements as there are bound parameters in + * the SQL statement being executed. + * @param $options + * An array of options for this query. + * + * @return + * TRUE on success, or FALSE on failure. + */ + public function execute($args = array(), $options = array()); + + /** + * Gets the query string of this statement. + * + * @return + * The query string, in its form with placeholders. + */ + public function getQueryString(); + + /** + * Returns the number of rows affected by the last SQL statement. + * + * @return + * The number of rows affected by the last DELETE, INSERT, or UPDATE + * statement executed. + */ + public function rowCount(); + + /** + * Sets the default fetch mode for this statement. + * + * See http://php.net/manual/en/pdo.constants.php for the definition of the + * constants used. + * + * @param $mode + * One of the PDO::FETCH_* constants. + * @param $a1 + * An option depending of the fetch mode specified by $mode: + * - for PDO::FETCH_COLUMN, the index of the column to fetch + * - for PDO::FETCH_CLASS, the name of the class to create + * - for PDO::FETCH_INTO, the object to add the data to + * @param $a2 + * If $mode is PDO::FETCH_CLASS, the optional arguments to pass to the + * constructor. + */ + // public function setFetchMode($mode, $a1 = NULL, $a2 = array()); + + /** + * Fetches the next row from a result set. + * + * See http://php.net/manual/en/pdo.constants.php for the definition of the + * constants used. + * + * @param $mode + * One of the PDO::FETCH_* constants. + * Default to what was specified by setFetchMode(). + * @param $cursor_orientation + * Not implemented in all database drivers, don't use. + * @param $cursor_offset + * Not implemented in all database drivers, don't use. + * + * @return + * A result, formatted according to $mode. + */ + // public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL); + + /** + * Returns a single field from the next record of a result set. + * + * @param $index + * The numeric index of the field to return. Defaults to the first field. + * + * @return + * A single field from the next record. + */ + public function fetchField($index = 0); + + /** + * Fetches the next row and returns it as an object. + * + * The object will be of the class specified by DatabaseStatementInterface::setFetchMode() + * or stdClass if not specified. + */ + // public function fetchObject(); + + /** + * Fetches the next row and returns it as an associative array. + * + * This method corresponds to PDOStatement::fetchObject(), but for associative + * arrays. For some reason PDOStatement does not have a corresponding array + * helper method, so one is added. + * + * @return + * An associative array. + */ + public function fetchAssoc(); + + /** + * Returns an array containing all of the result set rows. + * + * @param $mode + * One of the PDO::FETCH_* constants. + * @param $column_index + * If $mode is PDO::FETCH_COLUMN, the index of the column to fetch. + * @param $constructor_arguments + * If $mode is PDO::FETCH_CLASS, the arguments to pass to the constructor. + * + * @return + * An array of results. + */ + // function fetchAll($mode = NULL, $column_index = NULL, array $constructor_arguments); + + /** + * Returns an entire single column of a result set as an indexed array. + * + * Note that this method will run the result set to the end. + * + * @param $index + * The index of the column number to fetch. + * + * @return + * An indexed array. + */ + public function fetchCol($index = 0); + + /** + * Returns the entire result set as a single associative array. + * + * This method is only useful for two-column result sets. It will return an + * associative array where the key is one column from the result set and the + * value is another field. In most cases, the default of the first two columns + * is appropriate. + * + * Note that this method will run the result set to the end. + * + * @param $key_index + * The numeric index of the field to use as the array key. + * @param $value_index + * The numeric index of the field to use as the array value. + * + * @return + * An associative array. + */ + public function fetchAllKeyed($key_index = 0, $value_index = 1); + + /** + * Returns the result set as an associative array keyed by the given field. + * + * If the given key appears multiple times, later records will overwrite + * earlier ones. + * + * @param $key + * The name of the field on which to index the array. + * @param $fetch + * The fetchmode to use. If set to PDO::FETCH_ASSOC, PDO::FETCH_NUM, or + * PDO::FETCH_BOTH the returned value with be an array of arrays. For any + * other value it will be an array of objects. By default, the fetch mode + * set for the query will be used. + * + * @return + * An associative array. + */ + public function fetchAllAssoc($key, $fetch = NULL); +} + +/** + * Default implementation of DatabaseStatementInterface. + * + * PDO allows us to extend the PDOStatement class to provide additional + * functionality beyond that offered by default. We do need extra + * functionality. By default, this class is not driver-specific. If a given + * driver needs to set a custom statement class, it may do so in its + * constructor. + * + * @see http://us.php.net/pdostatement + */ +class DatabaseStatementBase extends PDOStatement implements DatabaseStatementInterface { + + /** + * Reference to the database connection object for this statement. + * + * The name $dbh is inherited from PDOStatement. + * + * @var DatabaseConnection + */ + public $dbh; + + protected function __construct($dbh) { + $this->dbh = $dbh; + $this->setFetchMode(PDO::FETCH_OBJ); + } + + public function execute($args = array(), $options = array()) { + if (isset($options['fetch'])) { + if (is_string($options['fetch'])) { + // Default to an object. Note: db fields will be added to the object + // before the constructor is run. If you need to assign fields after + // the constructor is run, see http://drupal.org/node/315092. + $this->setFetchMode(PDO::FETCH_CLASS, $options['fetch']); + } + else { + $this->setFetchMode($options['fetch']); + } + } + + $logger = $this->dbh->getLogger(); + if (!empty($logger)) { + $query_start = microtime(TRUE); + } + + $return = parent::execute($args); + + if (!empty($logger)) { + $query_end = microtime(TRUE); + $logger->log($this, $args, $query_end - $query_start); + } + + return $return; + } + + public function getQueryString() { + return $this->queryString; + } + + public function fetchCol($index = 0) { + return $this->fetchAll(PDO::FETCH_COLUMN, $index); + } + + public function fetchAllAssoc($key, $fetch = NULL) { + $return = array(); + if (isset($fetch)) { + if (is_string($fetch)) { + $this->setFetchMode(PDO::FETCH_CLASS, $fetch); + } + else { + $this->setFetchMode($fetch); + } + } + + foreach ($this as $record) { + $record_key = is_object($record) ? $record->$key : $record[$key]; + $return[$record_key] = $record; + } + + return $return; + } + + public function fetchAllKeyed($key_index = 0, $value_index = 1) { + $return = array(); + $this->setFetchMode(PDO::FETCH_NUM); + foreach ($this as $record) { + $return[$record[$key_index]] = $record[$value_index]; + } + return $return; + } + + public function fetchField($index = 0) { + // Call PDOStatement::fetchColumn to fetch the field. + return $this->fetchColumn($index); + } + + public function fetchAssoc() { + // Call PDOStatement::fetch to fetch the row. + return $this->fetch(PDO::FETCH_ASSOC); + } +} + +/** + * Empty implementation of a database statement. + * + * This class satisfies the requirements of being a database statement/result + * object, but does not actually contain data. It is useful when developers + * need to safely return an "empty" result set without connecting to an actual + * database. Calling code can then treat it the same as if it were an actual + * result set that happens to contain no records. + * + * @see SearchQuery + */ +class DatabaseStatementEmpty implements Iterator, DatabaseStatementInterface { + + public function execute($args = array(), $options = array()) { + return FALSE; + } + + public function getQueryString() { + return ''; + } + + public function rowCount() { + return 0; + } + + public function setFetchMode($mode, $a1 = NULL, $a2 = array()) { + return; + } + + public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL) { + return NULL; + } + + public function fetchField($index = 0) { + return NULL; + } + + public function fetchObject() { + return NULL; + } + + public function fetchAssoc() { + return NULL; + } + + function fetchAll($mode = NULL, $column_index = NULL, array $constructor_arguments = array()) { + return array(); + } + + public function fetchCol($index = 0) { + return array(); + } + + public function fetchAllKeyed($key_index = 0, $value_index = 1) { + return array(); + } + + public function fetchAllAssoc($key, $fetch = NULL) { + return array(); + } + + /* Implementations of Iterator. */ + + public function current() { + return NULL; + } + + public function key() { + return NULL; + } + + public function rewind() { + // Nothing to do: our DatabaseStatement can't be rewound. + } + + public function next() { + // Do nothing, since this is an always-empty implementation. + } + + public function valid() { + return FALSE; + } +} + +/** + * The following utility functions are simply convenience wrappers. + * + * They should never, ever have any database-specific code in them. + */ + +/** + * Executes an arbitrary query string against the active database. + * + * Use this function for SELECT queries if it is just a simple query string. + * If the caller or other modules need to change the query, use db_select() + * instead. + * + * Do not use this function for INSERT, UPDATE, or DELETE queries. Those should + * be handled via db_insert(), db_update() and db_delete() respectively. + * + * @param $query + * The prepared statement query to run. Although it will accept both named and + * unnamed placeholders, named placeholders are strongly preferred as they are + * more self-documenting. + * @param $args + * An array of values to substitute into the query. If the query uses named + * placeholders, this is an associative array in any order. If the query uses + * unnamed placeholders (?), this is an indexed array and the order must match + * the order of placeholders in the query string. + * @param $options + * An array of options to control how the query operates. + * + * @return DatabaseStatementInterface + * A prepared statement object, already executed. + * + * @see DatabaseConnection::defaultOptions() + */ +function db_query($query, array $args = array(), array $options = array()) { + if (empty($options['target'])) { + $options['target'] = 'default'; + } + + return Database::getConnection($options['target'])->query($query, $args, $options); +} + +/** + * Executes a query against the active database, restricted to a range. + * + * @param $query + * The prepared statement query to run. Although it will accept both named and + * unnamed placeholders, named placeholders are strongly preferred as they are + * more self-documenting. + * @param $from + * The first record from the result set to return. + * @param $count + * The number of records to return from the result set. + * @param $args + * An array of values to substitute into the query. If the query uses named + * placeholders, this is an associative array in any order. If the query uses + * unnamed placeholders (?), this is an indexed array and the order must match + * the order of placeholders in the query string. + * @param $options + * An array of options to control how the query operates. + * + * @return DatabaseStatementInterface + * A prepared statement object, already executed. + * + * @see DatabaseConnection::defaultOptions() + */ +function db_query_range($query, $from, $count, array $args = array(), array $options = array()) { + if (empty($options['target'])) { + $options['target'] = 'default'; + } + + return Database::getConnection($options['target'])->queryRange($query, $from, $count, $args, $options); +} + +/** + * Executes a query string and saves the result set to a temporary table. + * + * The execution of the query string happens against the active database. + * + * @param $query + * The prepared statement query to run. Although it will accept both named and + * unnamed placeholders, named placeholders are strongly preferred as they are + * more self-documenting. + * @param $args + * An array of values to substitute into the query. If the query uses named + * placeholders, this is an associative array in any order. If the query uses + * unnamed placeholders (?), this is an indexed array and the order must match + * the order of placeholders in the query string. + * @param $options + * An array of options to control how the query operates. + * + * @return + * The name of the temporary table. + * + * @see DatabaseConnection::defaultOptions() + */ +function db_query_temporary($query, array $args = array(), array $options = array()) { + if (empty($options['target'])) { + $options['target'] = 'default'; + } + + return Database::getConnection($options['target'])->queryTemporary($query, $args, $options); +} + +/** + * Returns a new InsertQuery object for the active database. + * + * @param $table + * The table into which to insert. + * @param $options + * An array of options to control how the query operates. + * + * @return InsertQuery + * A new InsertQuery object for this connection. + */ +function db_insert($table, array $options = array()) { + if (empty($options['target']) || $options['target'] == 'slave') { + $options['target'] = 'default'; + } + return Database::getConnection($options['target'])->insert($table, $options); +} + +/** + * Returns a new MergeQuery object for the active database. + * + * @param $table + * The table into which to merge. + * @param $options + * An array of options to control how the query operates. + * + * @return MergeQuery + * A new MergeQuery object for this connection. + */ +function db_merge($table, array $options = array()) { + if (empty($options['target']) || $options['target'] == 'slave') { + $options['target'] = 'default'; + } + return Database::getConnection($options['target'])->merge($table, $options); +} + +/** + * Returns a new UpdateQuery object for the active database. + * + * @param $table + * The table to update. + * @param $options + * An array of options to control how the query operates. + * + * @return UpdateQuery + * A new UpdateQuery object for this connection. + */ +function db_update($table, array $options = array()) { + if (empty($options['target']) || $options['target'] == 'slave') { + $options['target'] = 'default'; + } + return Database::getConnection($options['target'])->update($table, $options); +} + +/** + * Returns a new DeleteQuery object for the active database. + * + * @param $table + * The table from which to delete. + * @param $options + * An array of options to control how the query operates. + * + * @return DeleteQuery + * A new DeleteQuery object for this connection. + */ +function db_delete($table, array $options = array()) { + if (empty($options['target']) || $options['target'] == 'slave') { + $options['target'] = 'default'; + } + return Database::getConnection($options['target'])->delete($table, $options); +} + +/** + * Returns a new TruncateQuery object for the active database. + * + * @param $table + * The table from which to delete. + * @param $options + * An array of options to control how the query operates. + * + * @return TruncateQuery + * A new TruncateQuery object for this connection. + */ +function db_truncate($table, array $options = array()) { + if (empty($options['target']) || $options['target'] == 'slave') { + $options['target'] = 'default'; + } + return Database::getConnection($options['target'])->truncate($table, $options); +} + +/** + * Returns a new SelectQuery object for the active database. + * + * @param $table + * The base table for this query. May be a string or another SelectQuery + * object. If a query object is passed, it will be used as a subselect. + * @param $alias + * The alias for the base table of this query. + * @param $options + * An array of options to control how the query operates. + * + * @return SelectQuery + * A new SelectQuery object for this connection. + */ +function db_select($table, $alias = NULL, array $options = array()) { + if (empty($options['target'])) { + $options['target'] = 'default'; + } + return Database::getConnection($options['target'])->select($table, $alias, $options); +} + +/** + * Returns a new transaction object for the active database. + * + * @param string $name + * Optional name of the transaction. + * @param array $options + * An array of options to control how the transaction operates: + * - target: The database target name. + * + * @return DatabaseTransaction + * A new DatabaseTransaction object for this connection. + */ +function db_transaction($name = NULL, array $options = array()) { + if (empty($options['target'])) { + $options['target'] = 'default'; + } + return Database::getConnection($options['target'])->startTransaction($name); +} + +/** + * Sets a new active database. + * + * @param $key + * The key in the $databases array to set as the default database. + * + * @return + * The key of the formerly active database. + */ +function db_set_active($key = 'default') { + return Database::setActiveConnection($key); +} + +/** + * Restricts a dynamic table name to safe characters. + * + * Only keeps alphanumeric and underscores. + * + * @param $table + * The table name to escape. + * + * @return + * The escaped table name as a string. + */ +function db_escape_table($table) { + return Database::getConnection()->escapeTable($table); +} + +/** + * Restricts a dynamic column or constraint name to safe characters. + * + * Only keeps alphanumeric and underscores. + * + * @param $field + * The field name to escape. + * + * @return + * The escaped field name as a string. + */ +function db_escape_field($field) { + return Database::getConnection()->escapeField($field); +} + +/** + * Escapes characters that work as wildcard characters in a LIKE pattern. + * + * The wildcard characters "%" and "_" as well as backslash are prefixed with + * a backslash. Use this to do a search for a verbatim string without any + * wildcard behavior. + * + * For example, the following does a case-insensitive query for all rows whose + * name starts with $prefix: + * @code + * $result = db_query( + * 'SELECT * FROM person WHERE name LIKE :pattern', + * array(':pattern' => db_like($prefix) . '%') + * ); + * @endcode + * + * Backslash is defined as escape character for LIKE patterns in + * DatabaseCondition::mapConditionOperator(). + * + * @param $string + * The string to escape. + * + * @return + * The escaped string. + */ +function db_like($string) { + return Database::getConnection()->escapeLike($string); +} + +/** + * Retrieves the name of the currently active database driver. + * + * @return + * The name of the currently active database driver. + */ +function db_driver() { + return Database::getConnection()->driver(); +} + +/** + * Closes the active database connection. + * + * @param $options + * An array of options to control which connection is closed. Only the target + * key has any meaning in this case. + */ +function db_close(array $options = array()) { + if (empty($options['target'])) { + $options['target'] = NULL; + } + Database::closeConnection($options['target']); +} + +/** + * Retrieves a unique id. + * + * Use this function if for some reason you can't use a serial field. Using a + * serial field is preferred, and InsertQuery::execute() returns the value of + * the last ID inserted. + * + * @param $existing_id + * After a database import, it might be that the sequences table is behind, so + * by passing in a minimum ID, it can be assured that we never issue the same + * ID. + * + * @return + * An integer number larger than any number returned before for this sequence. + */ +function db_next_id($existing_id = 0) { + return Database::getConnection()->nextId($existing_id); +} + +/** + * Returns a new DatabaseCondition, set to "OR" all conditions together. + * + * @return DatabaseCondition + */ +function db_or() { + return new DatabaseCondition('OR'); +} + +/** + * Returns a new DatabaseCondition, set to "AND" all conditions together. + * + * @return DatabaseCondition + */ +function db_and() { + return new DatabaseCondition('AND'); +} + +/** + * Returns a new DatabaseCondition, set to "XOR" all conditions together. + * + * @return DatabaseCondition + */ +function db_xor() { + return new DatabaseCondition('XOR'); +} + +/** + * Returns a new DatabaseCondition, set to the specified conjunction. + * + * Internal API function call. The db_and(), db_or(), and db_xor() + * functions are preferred. + * + * @param $conjunction + * The conjunction to use for query conditions (AND, OR or XOR). + * @return DatabaseCondition + */ +function db_condition($conjunction) { + return new DatabaseCondition($conjunction); +} + +/** + * @} End of "defgroup database". + */ + + +/** + * @ingroup schemaapi + * @{ + */ + +/** + * Creates a new table from a Drupal table definition. + * + * @param $name + * The name of the table to create. + * @param $table + * A Schema API table definition array. + */ +function db_create_table($name, $table) { + return Database::getConnection()->schema()->createTable($name, $table); +} + +/** + * Returns an array of field names from an array of key/index column specifiers. + * + * This is usually an identity function but if a key/index uses a column prefix + * specification, this function extracts just the name. + * + * @param $fields + * An array of key/index column specifiers. + * + * @return + * An array of field names. + */ +function db_field_names($fields) { + return Database::getConnection()->schema()->fieldNames($fields); +} + +/** + * Checks if an index exists in the given table. + * + * @param $table + * The name of the table in drupal (no prefixing). + * @param $name + * The name of the index in drupal (no prefixing). + * + * @return + * TRUE if the given index exists, otherwise FALSE. + */ +function db_index_exists($table, $name) { + return Database::getConnection()->schema()->indexExists($table, $name); +} + +/** + * Checks if a table exists. + * + * @param $table + * The name of the table in drupal (no prefixing). + * + * @return + * TRUE if the given table exists, otherwise FALSE. + */ +function db_table_exists($table) { + return Database::getConnection()->schema()->tableExists($table); +} + +/** + * Checks if a column exists in the given table. + * + * @param $table + * The name of the table in drupal (no prefixing). + * @param $field + * The name of the field. + * + * @return + * TRUE if the given column exists, otherwise FALSE. + */ +function db_field_exists($table, $field) { + return Database::getConnection()->schema()->fieldExists($table, $field); +} + +/** + * Finds all tables that are like the specified base table name. + * + * @param $table_expression + * An SQL expression, for example "simpletest%" (without the quotes). + * BEWARE: this is not prefixed, the caller should take care of that. + * + * @return + * Array, both the keys and the values are the matching tables. + */ +function db_find_tables($table_expression) { + return Database::getConnection()->schema()->findTables($table_expression); +} + +function _db_create_keys_sql($spec) { + return Database::getConnection()->schema()->createKeysSql($spec); +} + +/** + * Renames a table. + * + * @param $table + * The table to be renamed. + * @param $new_name + * The new name for the table. + */ +function db_rename_table($table, $new_name) { + return Database::getConnection()->schema()->renameTable($table, $new_name); +} + +/** + * Drops a table. + * + * @param $table + * The table to be dropped. + */ +function db_drop_table($table) { + return Database::getConnection()->schema()->dropTable($table); +} + +/** + * Adds a new field to a table. + * + * @param $table + * Name of the table to be altered. + * @param $field + * Name of the field to be added. + * @param $spec + * The field specification array, as taken from a schema definition. The + * specification may also contain the key 'initial'; the newly-created field + * will be set to the value of the key in all rows. This is most useful for + * creating NOT NULL columns with no default value in existing tables. + * @param $keys_new + * Optional keys and indexes specification to be created on the table along + * with adding the field. The format is the same as a table specification, but + * without the 'fields' element. If you are adding a type 'serial' field, you + * MUST specify at least one key or index including it in this array. See + * db_change_field() for more explanation why. + * + * @see db_change_field() + */ +function db_add_field($table, $field, $spec, $keys_new = array()) { + return Database::getConnection()->schema()->addField($table, $field, $spec, $keys_new); +} + +/** + * Drops a field. + * + * @param $table + * The table to be altered. + * @param $field + * The field to be dropped. + */ +function db_drop_field($table, $field) { + return Database::getConnection()->schema()->dropField($table, $field); +} + +/** + * Sets the default value for a field. + * + * @param $table + * The table to be altered. + * @param $field + * The field to be altered. + * @param $default + * Default value to be set. NULL for 'default NULL'. + */ +function db_field_set_default($table, $field, $default) { + return Database::getConnection()->schema()->fieldSetDefault($table, $field, $default); +} + +/** + * Sets a field to have no default value. + * + * @param $table + * The table to be altered. + * @param $field + * The field to be altered. + */ +function db_field_set_no_default($table, $field) { + return Database::getConnection()->schema()->fieldSetNoDefault($table, $field); +} + +/** + * Adds a primary key to a database table. + * + * @param $table + * Name of the table to be altered. + * @param $fields + * Array of fields for the primary key. + */ +function db_add_primary_key($table, $fields) { + return Database::getConnection()->schema()->addPrimaryKey($table, $fields); +} + +/** + * Drops the primary key of a database table. + * + * @param $table + * Name of the table to be altered. + */ +function db_drop_primary_key($table) { + return Database::getConnection()->schema()->dropPrimaryKey($table); +} + +/** + * Adds a unique key. + * + * @param $table + * The table to be altered. + * @param $name + * The name of the key. + * @param $fields + * An array of field names. + */ +function db_add_unique_key($table, $name, $fields) { + return Database::getConnection()->schema()->addUniqueKey($table, $name, $fields); +} + +/** + * Drops a unique key. + * + * @param $table + * The table to be altered. + * @param $name + * The name of the key. + */ +function db_drop_unique_key($table, $name) { + return Database::getConnection()->schema()->dropUniqueKey($table, $name); +} + +/** + * Adds an index. + * + * @param $table + * The table to be altered. + * @param $name + * The name of the index. + * @param $fields + * An array of field names. + */ +function db_add_index($table, $name, $fields) { + return Database::getConnection()->schema()->addIndex($table, $name, $fields); +} + +/** + * Drops an index. + * + * @param $table + * The table to be altered. + * @param $name + * The name of the index. + */ +function db_drop_index($table, $name) { + return Database::getConnection()->schema()->dropIndex($table, $name); +} + +/** + * Changes a field definition. + * + * IMPORTANT NOTE: To maintain database portability, you have to explicitly + * recreate all indices and primary keys that are using the changed field. + * + * That means that you have to drop all affected keys and indexes with + * db_drop_{primary_key,unique_key,index}() before calling db_change_field(). + * To recreate the keys and indices, pass the key definitions as the optional + * $keys_new argument directly to db_change_field(). + * + * For example, suppose you have: + * @code + * $schema['foo'] = array( + * 'fields' => array( + * 'bar' => array('type' => 'int', 'not null' => TRUE) + * ), + * 'primary key' => array('bar') + * ); + * @endcode + * and you want to change foo.bar to be type serial, leaving it as the primary + * key. The correct sequence is: + * @code + * db_drop_primary_key('foo'); + * db_change_field('foo', 'bar', 'bar', + * array('type' => 'serial', 'not null' => TRUE), + * array('primary key' => array('bar'))); + * @endcode + * + * The reasons for this are due to the different database engines: + * + * On PostgreSQL, changing a field definition involves adding a new field and + * dropping an old one which causes any indices, primary keys and sequences + * (from serial-type fields) that use the changed field to be dropped. + * + * On MySQL, all type 'serial' fields must be part of at least one key or index + * as soon as they are created. You cannot use + * db_add_{primary_key,unique_key,index}() for this purpose because the ALTER + * TABLE command will fail to add the column without a key or index + * specification. The solution is to use the optional $keys_new argument to + * create the key or index at the same time as field. + * + * You could use db_add_{primary_key,unique_key,index}() in all cases unless you + * are converting a field to be type serial. You can use the $keys_new argument + * in all cases. + * + * @param $table + * Name of the table. + * @param $field + * Name of the field to change. + * @param $field_new + * New name for the field (set to the same as $field if you don't want to + * change the name). + * @param $spec + * The field specification for the new field. + * @param $keys_new + * Optional keys and indexes specification to be created on the table along + * with changing the field. The format is the same as a table specification + * but without the 'fields' element. + */ +function db_change_field($table, $field, $field_new, $spec, $keys_new = array()) { + return Database::getConnection()->schema()->changeField($table, $field, $field_new, $spec, $keys_new); +} + +/** + * @} End of "ingroup schemaapi". + */ + +/** + * Sets a session variable specifying the lag time for ignoring a slave server. + */ +function db_ignore_slave() { + $connection_info = Database::getConnectionInfo(); + // Only set ignore_slave_server if there are slave servers being used, which + // is assumed if there are more than one. + if (count($connection_info) > 1) { + // Five minutes is long enough to allow the slave to break and resume + // interrupted replication without causing problems on the Drupal site from + // the old data. + $duration = variable_get('maximum_replication_lag', 300); + // Set session variable with amount of time to delay before using slave. + $_SESSION['ignore_slave_server'] = REQUEST_TIME + $duration; + } +} diff --git a/includes/database/log.inc b/core/includes/database/log.inc similarity index 100% rename from includes/database/log.inc rename to core/includes/database/log.inc diff --git a/includes/database/mysql/database.inc b/core/includes/database/mysql/database.inc similarity index 100% rename from includes/database/mysql/database.inc rename to core/includes/database/mysql/database.inc diff --git a/includes/database/mysql/install.inc b/core/includes/database/mysql/install.inc similarity index 100% rename from includes/database/mysql/install.inc rename to core/includes/database/mysql/install.inc diff --git a/includes/database/mysql/query.inc b/core/includes/database/mysql/query.inc similarity index 100% rename from includes/database/mysql/query.inc rename to core/includes/database/mysql/query.inc diff --git a/includes/database/mysql/schema.inc b/core/includes/database/mysql/schema.inc similarity index 100% rename from includes/database/mysql/schema.inc rename to core/includes/database/mysql/schema.inc diff --git a/includes/database/pgsql/database.inc b/core/includes/database/pgsql/database.inc similarity index 100% rename from includes/database/pgsql/database.inc rename to core/includes/database/pgsql/database.inc diff --git a/includes/database/pgsql/install.inc b/core/includes/database/pgsql/install.inc similarity index 100% rename from includes/database/pgsql/install.inc rename to core/includes/database/pgsql/install.inc diff --git a/includes/database/pgsql/query.inc b/core/includes/database/pgsql/query.inc similarity index 100% rename from includes/database/pgsql/query.inc rename to core/includes/database/pgsql/query.inc diff --git a/includes/database/pgsql/schema.inc b/core/includes/database/pgsql/schema.inc similarity index 100% rename from includes/database/pgsql/schema.inc rename to core/includes/database/pgsql/schema.inc diff --git a/includes/database/pgsql/select.inc b/core/includes/database/pgsql/select.inc similarity index 100% rename from includes/database/pgsql/select.inc rename to core/includes/database/pgsql/select.inc diff --git a/includes/database/prefetch.inc b/core/includes/database/prefetch.inc similarity index 100% rename from includes/database/prefetch.inc rename to core/includes/database/prefetch.inc diff --git a/includes/database/query.inc b/core/includes/database/query.inc similarity index 100% rename from includes/database/query.inc rename to core/includes/database/query.inc diff --git a/core/includes/database/schema.inc b/core/includes/database/schema.inc new file mode 100644 index 0000000..b34713f --- /dev/null +++ b/core/includes/database/schema.inc @@ -0,0 +1,700 @@ + specification) + * that describes the table's database columns. The specification + * is also an array. The following specification parameters are defined: + * - 'description': A string in non-markup plain text describing this field + * and its purpose. References to other tables should be enclosed in + * curly-brackets. For example, the node table vid field + * description might contain "Always holds the largest (most + * recent) {node_revision}.vid value for this nid." + * - 'type': The generic datatype: 'char', 'varchar', 'text', 'blob', 'int', + * 'float', 'numeric', or 'serial'. Most types just map to the according + * database engine specific datatypes. Use 'serial' for auto incrementing + * fields. This will expand to 'INT auto_increment' on MySQL. + * - 'mysql_type', 'pgsql_type', 'sqlite_type', etc.: If you need to + * use a record type not included in the officially supported list + * of types above, you can specify a type for each database + * backend. In this case, you can leave out the type parameter, + * but be advised that your schema will fail to load on backends that + * do not have a type specified. A possible solution can be to + * use the "text" type as a fallback. + * - 'serialize': A boolean indicating whether the field will be stored as + * a serialized string. + * - 'size': The data size: 'tiny', 'small', 'medium', 'normal', + * 'big'. This is a hint about the largest value the field will + * store and determines which of the database engine specific + * datatypes will be used (e.g. on MySQL, TINYINT vs. INT vs. BIGINT). + * 'normal', the default, selects the base type (e.g. on MySQL, + * INT, VARCHAR, BLOB, etc.). + * Not all sizes are available for all data types. See + * DatabaseSchema::getFieldTypeMap() for possible combinations. + * - 'not null': If true, no NULL values will be allowed in this + * database column. Defaults to false. + * - 'default': The field's default value. The PHP type of the + * value matters: '', '0', and 0 are all different. If you + * specify '0' as the default value for a type 'int' field it + * will not work because '0' is a string containing the + * character "zero", not an integer. + * - 'length': The maximal length of a type 'char', 'varchar' or 'text' + * field. Ignored for other field types. + * - 'unsigned': A boolean indicating whether a type 'int', 'float' + * and 'numeric' only is signed or unsigned. Defaults to + * FALSE. Ignored for other field types. + * - 'precision', 'scale': For type 'numeric' fields, indicates + * the precision (total number of significant digits) and scale + * (decimal digits right of the decimal point). Both values are + * mandatory. Ignored for other field types. + * All parameters apart from 'type' are optional except that type + * 'numeric' columns must specify 'precision' and 'scale'. + * - 'primary key': An array of one or more key column specifiers (see below) + * that form the primary key. + * - 'unique keys': An associative array of unique keys ('keyname' => + * specification). Each specification is an array of one or more + * key column specifiers (see below) that form a unique key on the table. + * - 'foreign keys': An associative array of relations ('my_relation' => + * specification). Each specification is an array containing the name of + * the referenced table ('table'), and an array of column mappings + * ('columns'). Column mappings are defined by key pairs ('source_column' => + * 'referenced_column'). + * - 'indexes': An associative array of indexes ('indexname' => + * specification). Each specification is an array of one or more + * key column specifiers (see below) that form an index on the + * table. + * + * A key column specifier is either a string naming a column or an + * array of two elements, column name and length, specifying a prefix + * of the named column. + * + * As an example, here is a SUBSET of the schema definition for + * Drupal's 'node' table. It show four fields (nid, vid, type, and + * title), the primary key on field 'nid', a unique key named 'vid' on + * field 'vid', and two indexes, one named 'nid' on field 'nid' and + * one named 'node_title_type' on the field 'title' and the first four + * bytes of the field 'type': + * + * @code + * $schema['node'] = array( + * 'description' => 'The base table for nodes.', + * 'fields' => array( + * 'nid' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE), + * 'vid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE,'default' => 0), + * 'type' => array('type' => 'varchar','length' => 32,'not null' => TRUE, 'default' => ''), + * 'language' => array('type' => 'varchar','length' => 12,'not null' => TRUE,'default' => ''), + * 'title' => array('type' => 'varchar','length' => 255,'not null' => TRUE, 'default' => ''), + * 'uid' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), + * 'status' => array('type' => 'int', 'not null' => TRUE, 'default' => 1), + * 'created' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), + * 'changed' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), + * 'comment' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), + * 'promote' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), + * 'moderate' => array('type' => 'int', 'not null' => TRUE,'default' => 0), + * 'sticky' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), + * 'tnid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + * 'translate' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), + * ), + * 'indexes' => array( + * 'node_changed' => array('changed'), + * 'node_created' => array('created'), + * 'node_moderate' => array('moderate'), + * 'node_frontpage' => array('promote', 'status', 'sticky', 'created'), + * 'node_status_type' => array('status', 'type', 'nid'), + * 'node_title_type' => array('title', array('type', 4)), + * 'node_type' => array(array('type', 4)), + * 'uid' => array('uid'), + * 'tnid' => array('tnid'), + * 'translate' => array('translate'), + * ), + * 'unique keys' => array( + * 'vid' => array('vid'), + * ), + * 'foreign keys' => array( + * 'node_revision' => array( + * 'table' => 'node_revision', + * 'columns' => array('vid' => 'vid'), + * ), + * 'node_author' => array( + * 'table' => 'users', + * 'columns' => array('uid' => 'uid'), + * ), + * ), + * 'primary key' => array('nid'), + * ); + * @endcode + * + * @see drupal_install_schema() + */ + +abstract class DatabaseSchema implements QueryPlaceholderInterface { + + protected $connection; + + /** + * The placeholder counter. + */ + protected $placeholder = 0; + + /** + * Definition of prefixInfo array structure. + * + * Rather than redefining DatabaseSchema::getPrefixInfo() for each driver, + * by defining the defaultSchema variable only MySQL has to re-write the + * method. + * + * @see DatabaseSchema::getPrefixInfo() + */ + protected $defaultSchema = 'public'; + + public function __construct($connection) { + $this->connection = $connection; + } + + public function nextPlaceholder() { + return $this->placeholder++; + } + + /** + * Get information about the table name and schema from the prefix. + * + * @param + * Name of table to look prefix up for. Defaults to 'default' because thats + * default key for prefix. + * @param $add_prefix + * Boolean that indicates whether the given table name should be prefixed. + * + * @return + * A keyed array with information about the schema, table name and prefix. + */ + protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) { + $info = array( + 'schema' => $this->defaultSchema, + 'prefix' => $this->connection->tablePrefix($table), + ); + if ($add_prefix) { + $table = $info['prefix'] . $table; + } + // If the prefix contains a period in it, then that means the prefix also + // contains a schema reference in which case we will change the schema key + // to the value before the period in the prefix. Everything after the dot + // will be prefixed onto the front of the table. + if (($pos = strpos($table, '.')) !== FALSE) { + // Grab everything before the period. + $info['schema'] = substr($table, 0, $pos); + // Grab everything after the dot. + $info['table'] = substr($table, ++$pos); + } + else { + $info['table'] = $table; + } + return $info; + } + + /** + * Create names for indexes, primary keys and constraints. + * + * This prevents using {} around non-table names like indexes and keys. + */ + function prefixNonTable($table) { + $args = func_get_args(); + $info = $this->getPrefixInfo($table); + $args[0] = $info['table']; + return implode('_', $args); + } + + /** + * Build a condition to match a table name against a standard information_schema. + * + * The information_schema is a SQL standard that provides information about the + * database server and the databases, schemas, tables, columns and users within + * it. This makes information_schema a useful tool to use across the drupal + * database drivers and is used by a few different functions. The function below + * describes the conditions to be meet when querying information_schema.tables + * for drupal tables or information associated with drupal tables. Even though + * this is the standard method, not all databases follow standards and so this + * method should be overwritten by a database driver if the database provider + * uses alternate methods. Because information_schema.tables is used in a few + * different functions, a database driver will only need to override this function + * to make all the others work. For example see + * core/includes/databases/mysql/schema.inc. + * + * @param $table_name + * The name of the table in question. + * @param $operator + * The operator to apply on the 'table' part of the condition. + * @param $add_prefix + * Boolean to indicate whether the table name needs to be prefixed. + * + * @return QueryConditionInterface + * A DatabaseCondition object. + */ + protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) { + $info = $this->connection->getConnectionOptions(); + + // Retrive the table name and schema + $table_info = $this->getPrefixInfo($table_name, $add_prefix); + + $condition = new DatabaseCondition('AND'); + $condition->condition('table_catalog', $info['database']); + $condition->condition('table_schema', $table_info['schema']); + $condition->condition('table_name', $table_info['table'], $operator); + return $condition; + } + + /** + * Check if a table exists. + * + * @param $table + * The name of the table in drupal (no prefixing). + * + * @return + * TRUE if the given table exists, otherwise FALSE. + */ + public function tableExists($table) { + $condition = $this->buildTableNameCondition($table); + $condition->compile($this->connection, $this); + // Normally, we would heartily discourage the use of string + // concatenation for conditionals like this however, we + // couldn't use db_select() here because it would prefix + // information_schema.tables and the query would fail. + // Don't use {} around information_schema.tables table. + return (bool) $this->connection->query("SELECT 1 FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField(); + } + + /** + * Find all tables that are like the specified base table name. + * + * @param $table_expression + * An SQL expression, for example "simpletest%" (without the quotes). + * BEWARE: this is not prefixed, the caller should take care of that. + * + * @return + * Array, both the keys and the values are the matching tables. + */ + public function findTables($table_expression) { + $condition = $this->buildTableNameCondition($table_expression, 'LIKE', FALSE); + + $condition->compile($this->connection, $this); + // Normally, we would heartily discourage the use of string + // concatenation for conditionals like this however, we + // couldn't use db_select() here because it would prefix + // information_schema.tables and the query would fail. + // Don't use {} around information_schema.tables table. + return $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0, 0); + } + + /** + * Check if a column exists in the given table. + * + * @param $table + * The name of the table in drupal (no prefixing). + * @param $name + * The name of the column. + * + * @return + * TRUE if the given column exists, otherwise FALSE. + */ + public function fieldExists($table, $column) { + $condition = $this->buildTableNameCondition($table); + $condition->condition('column_name', $column); + $condition->compile($this->connection, $this); + // Normally, we would heartily discourage the use of string + // concatenation for conditionals like this however, we + // couldn't use db_select() here because it would prefix + // information_schema.tables and the query would fail. + // Don't use {} around information_schema.columns table. + return (bool) $this->connection->query("SELECT 1 FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField(); + } + + /** + * Returns a mapping of Drupal schema field names to DB-native field types. + * + * Because different field types do not map 1:1 between databases, Drupal has + * its own normalized field type names. This function returns a driver-specific + * mapping table from Drupal names to the native names for each database. + * + * @return array + * An array of Schema API field types to driver-specific field types. + */ + abstract public function getFieldTypeMap(); + + /** + * Rename a table. + * + * @param $table + * The table to be renamed. + * @param $new_name + * The new name for the table. + * + * @throws DatabaseSchemaObjectDoesNotExistException + * If the specified table doesn't exist. + * @throws DatabaseSchemaObjectExistsException + * If a table with the specified new name already exists. + */ + abstract public function renameTable($table, $new_name); + + /** + * Drop a table. + * + * @param $table + * The table to be dropped. + * + * @return + * TRUE if the table was successfully dropped, FALSE if there was no table + * by that name to begin with. + */ + abstract public function dropTable($table); + + /** + * Add a new field to a table. + * + * @param $table + * Name of the table to be altered. + * @param $field + * Name of the field to be added. + * @param $spec + * The field specification array, as taken from a schema definition. + * The specification may also contain the key 'initial', the newly + * created field will be set to the value of the key in all rows. + * This is most useful for creating NOT NULL columns with no default + * value in existing tables. + * @param $keys_new + * Optional keys and indexes specification to be created on the + * table along with adding the field. The format is the same as a + * table specification but without the 'fields' element. If you are + * adding a type 'serial' field, you MUST specify at least one key + * or index including it in this array. See db_change_field() for more + * explanation why. + * + * @throws DatabaseSchemaObjectDoesNotExistException + * If the specified table doesn't exist. + * @throws DatabaseSchemaObjectExistsException + * If the specified table already has a field by that name. + */ + abstract public function addField($table, $field, $spec, $keys_new = array()); + + /** + * Drop a field. + * + * @param $table + * The table to be altered. + * @param $field + * The field to be dropped. + * + * @return + * TRUE if the field was successfully dropped, FALSE if there was no field + * by that name to begin with. + */ + abstract public function dropField($table, $field); + + /** + * Set the default value for a field. + * + * @param $table + * The table to be altered. + * @param $field + * The field to be altered. + * @param $default + * Default value to be set. NULL for 'default NULL'. + * + * @throws DatabaseSchemaObjectDoesNotExistException + * If the specified table or field doesn't exist. + */ + abstract public function fieldSetDefault($table, $field, $default); + + /** + * Set a field to have no default value. + * + * @param $table + * The table to be altered. + * @param $field + * The field to be altered. + * + * @throws DatabaseSchemaObjectDoesNotExistException + * If the specified table or field doesn't exist. + */ + abstract public function fieldSetNoDefault($table, $field); + + /** + * Checks if an index exists in the given table. + * + * @param $table + * The name of the table in drupal (no prefixing). + * @param $name + * The name of the index in drupal (no prefixing). + * + * @return + * TRUE if the given index exists, otherwise FALSE. + */ + abstract public function indexExists($table, $name); + + /** + * Add a primary key. + * + * @param $table + * The table to be altered. + * @param $fields + * Fields for the primary key. + * + * @throws DatabaseSchemaObjectDoesNotExistException + * If the specified table doesn't exist. + * @throws DatabaseSchemaObjectExistsException + * If the specified table already has a primary key. + */ + abstract public function addPrimaryKey($table, $fields); + + /** + * Drop the primary key. + * + * @param $table + * The table to be altered. + * + * @return + * TRUE if the primary key was successfully dropped, FALSE if there was no + * primary key on this table to begin with. + */ + abstract public function dropPrimaryKey($table); + + /** + * Add a unique key. + * + * @param $table + * The table to be altered. + * @param $name + * The name of the key. + * @param $fields + * An array of field names. + * + * @throws DatabaseSchemaObjectDoesNotExistException + * If the specified table doesn't exist. + * @throws DatabaseSchemaObjectExistsException + * If the specified table already has a key by that name. + */ + abstract public function addUniqueKey($table, $name, $fields); + + /** + * Drop a unique key. + * + * @param $table + * The table to be altered. + * @param $name + * The name of the key. + * + * @return + * TRUE if the key was successfully dropped, FALSE if there was no key by + * that name to begin with. + */ + abstract public function dropUniqueKey($table, $name); + + /** + * Add an index. + * + * @param $table + * The table to be altered. + * @param $name + * The name of the index. + * @param $fields + * An array of field names. + * + * @throws DatabaseSchemaObjectDoesNotExistException + * If the specified table doesn't exist. + * @throws DatabaseSchemaObjectExistsException + * If the specified table already has an index by that name. + */ + abstract public function addIndex($table, $name, $fields); + + /** + * Drop an index. + * + * @param $table + * The table to be altered. + * @param $name + * The name of the index. + * + * @return + * TRUE if the index was successfully dropped, FALSE if there was no index + * by that name to begin with. + */ + abstract public function dropIndex($table, $name); + + /** + * Change a field definition. + * + * IMPORTANT NOTE: To maintain database portability, you have to explicitly + * recreate all indices and primary keys that are using the changed field. + * + * That means that you have to drop all affected keys and indexes with + * db_drop_{primary_key,unique_key,index}() before calling db_change_field(). + * To recreate the keys and indices, pass the key definitions as the + * optional $keys_new argument directly to db_change_field(). + * + * For example, suppose you have: + * @code + * $schema['foo'] = array( + * 'fields' => array( + * 'bar' => array('type' => 'int', 'not null' => TRUE) + * ), + * 'primary key' => array('bar') + * ); + * @endcode + * and you want to change foo.bar to be type serial, leaving it as the + * primary key. The correct sequence is: + * @code + * db_drop_primary_key('foo'); + * db_change_field('foo', 'bar', 'bar', + * array('type' => 'serial', 'not null' => TRUE), + * array('primary key' => array('bar'))); + * @endcode + * + * The reasons for this are due to the different database engines: + * + * On PostgreSQL, changing a field definition involves adding a new field + * and dropping an old one which* causes any indices, primary keys and + * sequences (from serial-type fields) that use the changed field to be dropped. + * + * On MySQL, all type 'serial' fields must be part of at least one key + * or index as soon as they are created. You cannot use + * db_add_{primary_key,unique_key,index}() for this purpose because + * the ALTER TABLE command will fail to add the column without a key + * or index specification. The solution is to use the optional + * $keys_new argument to create the key or index at the same time as + * field. + * + * You could use db_add_{primary_key,unique_key,index}() in all cases + * unless you are converting a field to be type serial. You can use + * the $keys_new argument in all cases. + * + * @param $table + * Name of the table. + * @param $field + * Name of the field to change. + * @param $field_new + * New name for the field (set to the same as $field if you don't want to change the name). + * @param $spec + * The field specification for the new field. + * @param $keys_new + * Optional keys and indexes specification to be created on the + * table along with changing the field. The format is the same as a + * table specification but without the 'fields' element. + * + * @throws DatabaseSchemaObjectDoesNotExistException + * If the specified table or source field doesn't exist. + * @throws DatabaseSchemaObjectExistsException + * If the specified destination field already exists. + */ + abstract public function changeField($table, $field, $field_new, $spec, $keys_new = array()); + + /** + * Create a new table from a Drupal table definition. + * + * @param $name + * The name of the table to create. + * @param $table + * A Schema API table definition array. + * + * @throws DatabaseSchemaObjectExistsException + * If the specified table already exists. + */ + public function createTable($name, $table) { + if ($this->tableExists($name)) { + throw new DatabaseSchemaObjectExistsException(t('Table %name already exists.', array('%name' => $name))); + } + $statements = $this->createTableSql($name, $table); + foreach ($statements as $statement) { + $this->connection->query($statement); + } + } + + /** + * Return an array of field names from an array of key/index column specifiers. + * + * This is usually an identity function but if a key/index uses a column prefix + * specification, this function extracts just the name. + * + * @param $fields + * An array of key/index column specifiers. + * + * @return + * An array of field names. + */ + public function fieldNames($fields) { + $return = array(); + foreach ($fields as $field) { + if (is_array($field)) { + $return[] = $field[0]; + } + else { + $return[] = $field; + } + } + return $return; + } + + /** + * Prepare a table or column comment for database query. + * + * @param $comment + * The comment string to prepare. + * @param $length + * Optional upper limit on the returned string length. + * + * @return + * The prepared comment. + */ + public function prepareComment($comment, $length = NULL) { + return $this->connection->quote($comment); + } +} + +/** + * Exception thrown if an object being created already exists. + * + * For example, this exception should be thrown whenever there is an attempt to + * create a new database table, field, or index that already exists in the + * database schema. + */ +class DatabaseSchemaObjectExistsException extends Exception {} + +/** + * Exception thrown if an object being modified doesn't exist yet. + * + * For example, this exception should be thrown whenever there is an attempt to + * modify a database table, field, or index that does not currently exist in + * the database schema. + */ +class DatabaseSchemaObjectDoesNotExistException extends Exception {} + +/** + * @} End of "defgroup schemaapi". + */ + diff --git a/includes/database/select.inc b/core/includes/database/select.inc similarity index 100% rename from includes/database/select.inc rename to core/includes/database/select.inc diff --git a/core/includes/database/sqlite/database.inc b/core/includes/database/sqlite/database.inc new file mode 100644 index 0000000..caaabf9 --- /dev/null +++ b/core/includes/database/sqlite/database.inc @@ -0,0 +1,511 @@ +statementClass = NULL; + + // This driver defaults to transaction support, except if explicitly passed FALSE. + $this->transactionSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] !== FALSE; + + $this->connectionOptions = $connection_options; + + parent::__construct('sqlite:' . $connection_options['database'], '', '', array( + // Force column names to lower case. + PDO::ATTR_CASE => PDO::CASE_LOWER, + // Convert numeric values to strings when fetching. + PDO::ATTR_STRINGIFY_FETCHES => TRUE, + )); + + // Attach one database for each registered prefix. + $prefixes = &$this->prefixes; + if (!empty($this->defaultPrefix)) { + // Add in the default prefix, which is also attached. + $prefixes[] = &$this->defaultPrefix; + } + foreach ($this->prefixes as $table => &$prefix) { + // Empty prefix means query the main database -- no need to attach anything. + if (!empty($prefix)) { + // Only attach the database once. + if (!isset($this->attachedDatabases[$prefix])) { + $this->attachedDatabases[$prefix] = $prefix; + $this->query('ATTACH DATABASE :database AS :prefix', array(':database' => $connection_options['database'] . '-' . $prefix, ':prefix' => $prefix)); + } + + // Add a ., so queries become prefix.table, which is proper syntax for + // querying an attached database. + $prefix .= '.'; + } + } + + // Detect support for SAVEPOINT. + $version = $this->query('SELECT sqlite_version()')->fetchField(); + $this->savepointSupport = (version_compare($version, '3.6.8') >= 0); + + // Create functions needed by SQLite. + $this->sqliteCreateFunction('if', array($this, 'sqlFunctionIf')); + $this->sqliteCreateFunction('greatest', array($this, 'sqlFunctionGreatest')); + $this->sqliteCreateFunction('pow', 'pow', 2); + $this->sqliteCreateFunction('length', 'strlen', 1); + $this->sqliteCreateFunction('md5', 'md5', 1); + $this->sqliteCreateFunction('concat', array($this, 'sqlFunctionConcat')); + $this->sqliteCreateFunction('substring', array($this, 'sqlFunctionSubstring'), 3); + $this->sqliteCreateFunction('substring_index', array($this, 'sqlFunctionSubstringIndex'), 3); + $this->sqliteCreateFunction('rand', array($this, 'sqlFunctionRand')); + } + + /** + * Destructor for the SQLite connection. + * + * We prune empty databases on destruct, but only if tables have been + * dropped. This is especially needed when running the test suite, which + * creates and destroy databases several times in a row. + */ + public function __destruct() { + if ($this->tableDropped && !empty($this->attachedDatabases)) { + foreach ($this->attachedDatabases as $prefix) { + // Check if the database is now empty, ignore the internal SQLite tables. + try { + $count = $this->query('SELECT COUNT(*) FROM ' . $prefix . '.sqlite_master WHERE type = :type AND name NOT LIKE :pattern', array(':type' => 'table', ':pattern' => 'sqlite_%'))->fetchField(); + + // We can prune the database file if it doens't have any tables. + if ($count == 0) { + // Detach the database. + $this->query('DETACH DATABASE :schema', array(':schema' => $prefix)); + // Destroy the database file. + unlink($this->connectionOptions['database'] . '-' . $prefix); + } + } + catch (Exception $e) { + // Ignore the exception and continue. There is nothing we can do here + // to report the error or fail safe. + } + } + } + } + + /** + * SQLite compatibility implementation for the IF() SQL function. + */ + public function sqlFunctionIf($condition, $expr1, $expr2 = NULL) { + return $condition ? $expr1 : $expr2; + } + + /** + * SQLite compatibility implementation for the GREATEST() SQL function. + */ + public function sqlFunctionGreatest() { + $args = func_get_args(); + foreach ($args as $k => $v) { + if (!isset($v)) { + unset($args); + } + } + if (count($args)) { + return max($args); + } + else { + return NULL; + } + } + + /** + * SQLite compatibility implementation for the CONCAT() SQL function. + */ + public function sqlFunctionConcat() { + $args = func_get_args(); + return implode('', $args); + } + + /** + * SQLite compatibility implementation for the SUBSTRING() SQL function. + */ + public function sqlFunctionSubstring($string, $from, $length) { + return substr($string, $from - 1, $length); + } + + /** + * SQLite compatibility implementation for the SUBSTRING_INDEX() SQL function. + */ + public function sqlFunctionSubstringIndex($string, $delimiter, $count) { + // If string is empty, simply return an empty string. + if (empty($string)) { + return ''; + } + $end = 0; + for ($i = 0; $i < $count; $i++) { + $end = strpos($string, $delimiter, $end + 1); + if ($end === FALSE) { + $end = strlen($string); + } + } + return substr($string, 0, $end); + } + + /** + * SQLite compatibility implementation for the RAND() SQL function. + */ + public function sqlFunctionRand($seed = NULL) { + if (isset($seed)) { + mt_srand($seed); + } + return mt_rand() / mt_getrandmax(); + } + + /** + * SQLite-specific implementation of DatabaseConnection::prepare(). + * + * We don't use prepared statements at all at this stage. We just create + * a DatabaseStatement_sqlite object, that will create a PDOStatement + * using the semi-private PDOPrepare() method below. + */ + public function prepare($query, $options = array()) { + return new DatabaseStatement_sqlite($this, $query, $options); + } + + /** + * NEVER CALL THIS FUNCTION: YOU MIGHT DEADLOCK YOUR PHP PROCESS. + * + * This is a wrapper around the parent PDO::prepare method. However, as + * the PDO SQLite driver only closes SELECT statements when the PDOStatement + * destructor is called and SQLite does not allow data change (INSERT, + * UPDATE etc) on a table which has open SELECT statements, you should never + * call this function and keep a PDOStatement object alive as that can lead + * to a deadlock. This really, really should be private, but as + * DatabaseStatement_sqlite needs to call it, we have no other choice but to + * expose this function to the world. + */ + public function PDOPrepare($query, array $options = array()) { + return parent::prepare($query, $options); + } + + public function queryRange($query, $from, $count, array $args = array(), array $options = array()) { + return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options); + } + + public function queryTemporary($query, array $args = array(), array $options = array()) { + // Generate a new temporary table name and protect it from prefixing. + // SQLite requires that temporary tables to be non-qualified. + $tablename = $this->generateTemporaryTableName(); + $this->prefixes[$tablename] = ''; + + $this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE ' . $tablename . ' AS SELECT', $query), $args, $options); + return $tablename; + } + + public function driver() { + return 'sqlite'; + } + + public function databaseType() { + return 'sqlite'; + } + + public function mapConditionOperator($operator) { + // We don't want to override any of the defaults. + static $specials = array( + 'LIKE' => array('postfix' => " ESCAPE '\\'"), + 'NOT LIKE' => array('postfix' => " ESCAPE '\\'"), + ); + return isset($specials[$operator]) ? $specials[$operator] : NULL; + } + + public function prepareQuery($query) { + return $this->prepare($this->prefixTables($query)); + } + + public function nextId($existing_id = 0) { + $transaction = $this->startTransaction(); + // We can safely use literal queries here instead of the slower query + // builder because if a given database breaks here then it can simply + // override nextId. However, this is unlikely as we deal with short strings + // and integers and no known databases require special handling for those + // simple cases. If another transaction wants to write the same row, it will + // wait until this transaction commits. + $stmt = $this->query('UPDATE {sequences} SET value = GREATEST(value, :existing_id) + 1', array( + ':existing_id' => $existing_id, + )); + if (!$stmt->rowCount()) { + $this->query('INSERT INTO {sequences} (value) VALUES (:existing_id + 1)', array( + ':existing_id' => $existing_id, + )); + } + // The transaction gets committed when the transaction object gets destroyed + // because it gets out of scope. + return $this->query('SELECT value FROM {sequences}')->fetchField(); + } + + public function rollback($savepoint_name = 'drupal_transaction') { + if ($this->savepointSupport) { + return parent::rollBack($savepoint_name); + } + + if (!$this->inTransaction()) { + throw new DatabaseTransactionNoActiveException(); + } + // A previous rollback to an earlier savepoint may mean that the savepoint + // in question has already been rolled back. + if (!in_array($savepoint_name, $this->transactionLayers)) { + return; + } + + // We need to find the point we're rolling back to, all other savepoints + // before are no longer needed. + while ($savepoint = array_pop($this->transactionLayers)) { + if ($savepoint == $savepoint_name) { + // Mark whole stack of transactions as needed roll back. + $this->willRollback = TRUE; + // If it is the last the transaction in the stack, then it is not a + // savepoint, it is the transaction itself so we will need to roll back + // the transaction rather than a savepoint. + if (empty($this->transactionLayers)) { + break; + } + return; + } + } + if ($this->supportsTransactions()) { + PDO::rollBack(); + } + } + + public function pushTransaction($name) { + if ($this->savepointSupport) { + return parent::pushTransaction($name); + } + if (!$this->supportsTransactions()) { + return; + } + if (isset($this->transactionLayers[$name])) { + throw new DatabaseTransactionNameNonUniqueException($name . " is already in use."); + } + if (!$this->inTransaction()) { + PDO::beginTransaction(); + } + $this->transactionLayers[$name] = $name; + } + + public function popTransaction($name) { + if ($this->savepointSupport) { + return parent::popTransaction($name); + } + if (!$this->supportsTransactions()) { + return; + } + if (!$this->inTransaction()) { + throw new DatabaseTransactionNoActiveException(); + } + + // Commit everything since SAVEPOINT $name. + while($savepoint = array_pop($this->transactionLayers)) { + if ($savepoint != $name) continue; + + // If there are no more layers left then we should commit or rollback. + if (empty($this->transactionLayers)) { + // If there was any rollback() we should roll back whole transaction. + if ($this->willRollback) { + $this->willRollback = FALSE; + PDO::rollBack(); + } + elseif (!PDO::commit()) { + throw new DatabaseTransactionCommitFailedException(); + } + } + else { + break; + } + } + } + +} + +/** + * Specific SQLite implementation of DatabaseConnection. + * + * See DatabaseConnection_sqlite::PDOPrepare() for reasons why we must prefetch + * the data instead of using PDOStatement. + * + * @see DatabaseConnection_sqlite::PDOPrepare() + */ +class DatabaseStatement_sqlite extends DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface { + + /** + * SQLite specific implementation of getStatement(). + * + * The PDO SQLite layer doesn't replace numeric placeholders in queries + * correctly, and this makes numeric expressions (such as COUNT(*) >= :count) + * fail. We replace numeric placeholders in the query ourselves to work + * around this bug. + * + * See http://bugs.php.net/bug.php?id=45259 for more details. + */ + protected function getStatement($query, &$args = array()) { + if (count($args)) { + // Check if $args is a simple numeric array. + if (range(0, count($args) - 1) === array_keys($args)) { + // In that case, we have unnamed placeholders. + $count = 0; + $new_args = array(); + foreach ($args as $value) { + if (is_float($value) || is_int($value)) { + if (is_float($value)) { + // Force the conversion to float so as not to loose precision + // in the automatic cast. + $value = sprintf('%F', $value); + } + $query = substr_replace($query, $value, strpos($query, '?'), 1); + } + else { + $placeholder = ':db_statement_placeholder_' . $count++; + $query = substr_replace($query, $placeholder, strpos($query, '?'), 1); + $new_args[$placeholder] = $value; + } + } + $args = $new_args; + } + else { + // Else, this is using named placeholders. + foreach ($args as $placeholder => $value) { + if (is_float($value) || is_int($value)) { + if (is_float($value)) { + // Force the conversion to float so as not to loose precision + // in the automatic cast. + $value = sprintf('%F', $value); + } + + // We will remove this placeholder from the query as PDO throws an + // exception if the number of placeholders in the query and the + // arguments does not match. + unset($args[$placeholder]); + // PDO allows placeholders to not be prefixed by a colon. See + // http://marc.info/?l=php-internals&m=111234321827149&w=2 for + // more. + if ($placeholder[0] != ':') { + $placeholder = ":$placeholder"; + } + // When replacing the placeholders, make sure we search for the + // exact placeholder. For example, if searching for + // ':db_placeholder_1', do not replace ':db_placeholder_11'. + $query = preg_replace('/' . preg_quote($placeholder) . '\b/', $value, $query); + } + } + } + } + + return $this->dbh->PDOPrepare($query); + } + + public function execute($args = array(), $options = array()) { + try { + $return = parent::execute($args, $options); + } + catch (PDOException $e) { + if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) { + // The schema has changed. SQLite specifies that we must resend the query. + $return = parent::execute($args, $options); + } + else { + // Rethrow the exception. + throw $e; + } + } + + // In some weird cases, SQLite will prefix some column names by the name + // of the table. We post-process the data, by renaming the column names + // using the same convention as MySQL and PostgreSQL. + $rename_columns = array(); + foreach ($this->columnNames as $k => $column) { + // In some SQLite versions, SELECT DISTINCT(field) will return "(field)" + // instead of "field". + if (preg_match("/^\((.*)\)$/", $column, $matches)) { + $rename_columns[$column] = $matches[1]; + $this->columnNames[$k] = $matches[1]; + $column = $matches[1]; + } + + // Remove "table." prefixes. + if (preg_match("/^.*\.(.*)$/", $column, $matches)) { + $rename_columns[$column] = $matches[1]; + $this->columnNames[$k] = $matches[1]; + } + } + if ($rename_columns) { + // DatabaseStatementPrefetch already extracted the first row, + // put it back into the result set. + if (isset($this->currentRow)) { + $this->data[0] = &$this->currentRow; + } + + // Then rename all the columns across the result set. + foreach ($this->data as $k => $row) { + foreach ($rename_columns as $old_column => $new_column) { + $this->data[$k][$new_column] = $this->data[$k][$old_column]; + unset($this->data[$k][$old_column]); + } + } + + // Finally, extract the first row again. + $this->currentRow = $this->data[0]; + unset($this->data[0]); + } + + return $return; + } +} + +/** + * @} End of "ingroup database". + */ diff --git a/includes/database/sqlite/install.inc b/core/includes/database/sqlite/install.inc similarity index 100% rename from includes/database/sqlite/install.inc rename to core/includes/database/sqlite/install.inc diff --git a/includes/database/sqlite/query.inc b/core/includes/database/sqlite/query.inc similarity index 100% rename from includes/database/sqlite/query.inc rename to core/includes/database/sqlite/query.inc diff --git a/includes/database/sqlite/schema.inc b/core/includes/database/sqlite/schema.inc similarity index 100% rename from includes/database/sqlite/schema.inc rename to core/includes/database/sqlite/schema.inc diff --git a/includes/database/sqlite/select.inc b/core/includes/database/sqlite/select.inc similarity index 100% rename from includes/database/sqlite/select.inc rename to core/includes/database/sqlite/select.inc diff --git a/includes/date.inc b/core/includes/date.inc similarity index 100% rename from includes/date.inc rename to core/includes/date.inc diff --git a/includes/entity.inc b/core/includes/entity.inc similarity index 100% rename from includes/entity.inc rename to core/includes/entity.inc diff --git a/core/includes/errors.inc b/core/includes/errors.inc new file mode 100644 index 0000000..a8148dc --- /dev/null +++ b/core/includes/errors.inc @@ -0,0 +1,291 @@ + array('Error', LOG_ERR), + E_WARNING => array('Warning', LOG_WARNING), + E_PARSE => array('Parse error', LOG_ERR), + E_NOTICE => array('Notice', LOG_NOTICE), + E_CORE_ERROR => array('Core error', LOG_ERR), + E_CORE_WARNING => array('Core warning', LOG_WARNING), + E_COMPILE_ERROR => array('Compile error', LOG_ERR), + E_COMPILE_WARNING => array('Compile warning', LOG_WARNING), + E_USER_ERROR => array('User error', LOG_ERR), + E_USER_WARNING => array('User warning', LOG_WARNING), + E_USER_NOTICE => array('User notice', LOG_NOTICE), + E_STRICT => array('Strict warning', LOG_DEBUG), + E_RECOVERABLE_ERROR => array('Recoverable fatal error', LOG_ERR), + E_DEPRECATED => array('Deprecated function', LOG_DEBUG), + E_USER_DEPRECATED => array('User deprecated function', LOG_DEBUG), + ); + + return $types; +} + +/** + * Custom PHP error handler. + * + * @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. + * @param $context + * An array that points to the active symbol table at the point the error occurred. + */ +function _drupal_error_handler_real($error_level, $message, $filename, $line, $context) { + if ($error_level & error_reporting()) { + $types = drupal_error_levels(); + list($severity_msg, $severity_level) = $types[$error_level]; + $caller = _drupal_get_last_caller(debug_backtrace()); + + if (!function_exists('filter_xss_admin')) { + require_once DRUPAL_ROOT . '/core/includes/common.inc'; + } + + // We treat recoverable errors as fatal. + _drupal_log_error(array( + '%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error', + // The standard PHP error handler considers that the error messages + // are HTML. We mimick this behavior here. + '!message' => filter_xss_admin($message), + '%function' => $caller['function'], + '%file' => $caller['file'], + '%line' => $caller['line'], + 'severity_level' => $severity_level, + ), $error_level == E_RECOVERABLE_ERROR); + } +} + +/** + * Decode an exception, especially to retrive the correct caller. + * + * @param $exception + * The exception object that was thrown. + * @return + * An error in the format expected by _drupal_log_error(). + */ +function _drupal_decode_exception($exception) { + $message = $exception->getMessage(); + + $backtrace = $exception->getTrace(); + // Add the line throwing the exception to the backtrace. + array_unshift($backtrace, array('line' => $exception->getLine(), 'file' => $exception->getFile())); + + // For PDOException errors, we try to return the initial caller, + // skipping internal functions of the database layer. + if ($exception instanceof PDOException) { + // The first element in the stack is the call, the second element gives us the caller. + // We skip calls that occurred in one of the classes of the database layer + // or in one of its global functions. + $db_functions = array('db_query', 'db_query_range'); + while (!empty($backtrace[1]) && ($caller = $backtrace[1]) && + ((isset($caller['class']) && (strpos($caller['class'], 'Query') !== FALSE || strpos($caller['class'], 'Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE)) || + in_array($caller['function'], $db_functions))) { + // We remove that call. + array_shift($backtrace); + } + if (isset($exception->query_string, $exception->args)) { + $message .= ": " . $exception->query_string . "; " . print_r($exception->args, TRUE); + } + } + $caller = _drupal_get_last_caller($backtrace); + + return array( + '%type' => get_class($exception), + // The standard PHP exception handler considers that the exception message + // is plain-text. We mimick this behavior here. + '!message' => check_plain($message), + '%function' => $caller['function'], + '%file' => $caller['file'], + '%line' => $caller['line'], + 'severity_level' => LOG_ERR, + ); +} + +/** + * Render an error message for an exception without any possibility of a further exception occuring. + * + * @param $exception + * The exception object that was thrown. + * @return + * An error message. + */ +function _drupal_render_exception_safe($exception) { + return check_plain(strtr('%type: !message in %function (line %line of %file).', _drupal_decode_exception($exception))); +} + +/** + * Determines whether an error should be displayed. + * + * When in maintenance mode or when error_level is ERROR_REPORTING_DISPLAY_ALL, + * all errors should be displayed. For ERROR_REPORTING_DISPLAY_SOME, $error + * will be examined to determine if it should be displayed. + * + * @param $error + * Optional error to examine for ERROR_REPORTING_DISPLAY_SOME. + * + * @return + * TRUE if an error should be displayed. + */ +function error_displayable($error = NULL) { + $error_level = variable_get('error_level', ERROR_REPORTING_DISPLAY_ALL); + $updating = (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update'); + $all_errors_displayed = ($error_level == ERROR_REPORTING_DISPLAY_ALL); + $error_needs_display = ($error_level == ERROR_REPORTING_DISPLAY_SOME && + isset($error) && $error['%type'] != 'Notice' && $error['%type'] != 'Strict warning'); + + return ($updating || $all_errors_displayed || $error_needs_display); +} + +/** + * Log a PHP error or exception, display an error page in fatal cases. + * + * @param $error + * An array with the following keys: %type, !message, %function, %file, %line + * and severity_level. All the parameters are plain-text, with the exception of + * !message, which needs to be a safe HTML string. + * @param $fatal + * TRUE if the error is fatal. + */ +function _drupal_log_error($error, $fatal = FALSE) { + // Initialize a maintenance theme if the boostrap was not complete. + // Do it early because drupal_set_message() triggers a drupal_theme_initialize(). + if ($fatal && (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL)) { + unset($GLOBALS['theme']); + if (!defined('MAINTENANCE_MODE')) { + define('MAINTENANCE_MODE', 'error'); + } + drupal_maintenance_theme(); + } + + // When running inside the testing framework, we relay the errors + // to the tested site by the way of HTTP headers. + $test_info = &$GLOBALS['drupal_test_info']; + if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) { + // $number does not use drupal_static as it should not be reset + // as it uniquely identifies each PHP error. + static $number = 0; + $assertion = array( + $error['!message'], + $error['%type'], + array( + 'function' => $error['%function'], + 'file' => $error['%file'], + 'line' => $error['%line'], + ), + ); + header('X-Drupal-Assertion-' . $number . ': ' . rawurlencode(serialize($assertion))); + $number++; + } + + watchdog('php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']); + + if ($fatal) { + drupal_add_http_header('Status', '500 Service unavailable (with message)'); + } + + if (drupal_is_cli()) { + if ($fatal) { + // When called from CLI, simply output a plain text message. + print html_entity_decode(strip_tags(t('%type: !message in %function (line %line of %file).', $error))). "\n"; + exit; + } + } + + if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') { + if ($fatal) { + // When called from JavaScript, simply output the error message. + print t('%type: !message in %function (line %line of %file).', $error); + exit; + } + } + else { + // Display the message if the current error reporting level allows this type + // of message to be displayed, and unconditionnaly in update.php. + if (error_displayable($error)) { + $class = 'error'; + + // If error type is 'User notice' then treat it as debug information + // instead of an error message, see dd(). + if ($error['%type'] == 'User notice') { + $error['%type'] = 'Debug'; + $class = 'status'; + } + + drupal_set_message(t('%type: !message in %function (line %line of %file).', $error), $class); + } + + if ($fatal) { + drupal_set_title(t('Error')); + // We fallback to a maintenance page at this point, because the page generation + // itself can generate errors. + print theme('maintenance_page', array('content' => t('The website encountered an unexpected error. Please try again later.'))); + exit; + } + } +} + +/** + * Gets the last caller from a backtrace. + * + * @param $backtrace + * A standard PHP backtrace. + * @return + * An associative array with keys 'file', 'line' and 'function'. + */ +function _drupal_get_last_caller($backtrace) { + // Errors that occur inside PHP internal functions do not generate + // information about file and line. Ignore black listed functions. + $blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler'); + while (($backtrace && !isset($backtrace[0]['line'])) || + (isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], $blacklist))) { + array_shift($backtrace); + } + + // The first trace is the call itself. + // It gives us the line and the file of the last call. + $call = $backtrace[0]; + + // The second call give us the function where the call originated. + if (isset($backtrace[1])) { + if (isset($backtrace[1]['class'])) { + $call['function'] = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()'; + } + else { + $call['function'] = $backtrace[1]['function'] . '()'; + } + } + else { + $call['function'] = 'main()'; + } + return $call; +} diff --git a/core/includes/file.inc b/core/includes/file.inc new file mode 100644 index 0000000..63b61ed --- /dev/null +++ b/core/includes/file.inc @@ -0,0 +1,2390 @@ + $info) { + // Add defaults. + $wrappers[$scheme] += array('type' => STREAM_WRAPPERS_NORMAL); + } + drupal_alter('stream_wrappers', $wrappers); + $existing = stream_get_wrappers(); + foreach ($wrappers as $scheme => $info) { + // We only register classes that implement our interface. + if (in_array('DrupalStreamWrapperInterface', class_implements($info['class']), TRUE)) { + // Record whether we are overriding an existing scheme. + if (in_array($scheme, $existing, TRUE)) { + $wrappers[$scheme]['override'] = TRUE; + stream_wrapper_unregister($scheme); + } + else { + $wrappers[$scheme]['override'] = FALSE; + } + if (($info['type'] & STREAM_WRAPPERS_LOCAL) == STREAM_WRAPPERS_LOCAL) { + stream_wrapper_register($scheme, $info['class']); + } + else { + stream_wrapper_register($scheme, $info['class'], STREAM_IS_URL); + } + } + // Pre-populate the static cache with the filters most typically used. + $wrappers_storage[STREAM_WRAPPERS_ALL][$scheme] = $wrappers[$scheme]; + if (($info['type'] & STREAM_WRAPPERS_WRITE_VISIBLE) == STREAM_WRAPPERS_WRITE_VISIBLE) { + $wrappers_storage[STREAM_WRAPPERS_WRITE_VISIBLE][$scheme] = $wrappers[$scheme]; + } + } + } + + if (!isset($wrappers_storage[$filter])) { + $wrappers_storage[$filter] = array(); + foreach ($wrappers_storage[STREAM_WRAPPERS_ALL] as $scheme => $info) { + // Bit-wise filter. + if (($info['type'] & $filter) == $filter) { + $wrappers_storage[$filter][$scheme] = $info; + } + } + } + + return $wrappers_storage[$filter]; +} + +/** + * Returns the stream wrapper class name for a given scheme. + * + * @param $scheme + * Stream scheme. + * + * @return + * Return string if a scheme has a registered handler, or FALSE. + */ +function file_stream_wrapper_get_class($scheme) { + $wrappers = file_get_stream_wrappers(); + return empty($wrappers[$scheme]) ? FALSE : $wrappers[$scheme]['class']; +} + +/** + * Returns the scheme of a URI (e.g. a stream). + * + * @param $uri + * A stream, referenced as "scheme://target". + * + * @return + * A string containing the name of the scheme, or FALSE if none. For example, + * the URI "public://example.txt" would return "public". + * + * @see file_uri_target() + */ +function file_uri_scheme($uri) { + $position = strpos($uri, '://'); + return $position ? substr($uri, 0, $position) : FALSE; +} + +/** + * Check that the scheme of a stream URI is valid. + * + * Confirms that there is a registered stream handler for the provided scheme + * and that it is callable. This is useful if you want to confirm a valid + * scheme without creating a new instance of the registered handler. + * + * @param $scheme + * A URI scheme, a stream is referenced as "scheme://target". + * + * @return + * Returns TRUE if the string is the name of a validated stream, + * or FALSE if the scheme does not have a registered handler. + */ +function file_stream_wrapper_valid_scheme($scheme) { + // Does the scheme have a registered handler that is callable? + $class = file_stream_wrapper_get_class($scheme); + if (class_exists($class)) { + return TRUE; + } + else { + return FALSE; + } +} + + +/** + * Returns the part of an URI after the schema. + * + * @param $uri + * A stream, referenced as "scheme://target". + * + * @return + * A string containing the target (path), or FALSE if none. + * For example, the URI "public://sample/test.txt" would return + * "sample/test.txt". + * + * @see file_uri_scheme() + */ +function file_uri_target($uri) { + $data = explode('://', $uri, 2); + + // Remove erroneous leading or trailing, forward-slashes and backslashes. + return count($data) == 2 ? trim($data[1], '\/') : FALSE; +} + +/** + * Get the default file stream implementation. + * + * @return + * 'public', 'private' or any other file scheme defined as the default. + */ +function file_default_scheme() { + return variable_get('file_default_scheme', 'public'); +} + +/** + * Normalizes a URI by making it syntactically correct. + * + * A stream is referenced as "scheme://target". + * + * The following actions are taken: + * - Remove trailing slashes from target + * - Trim erroneous leading slashes from target. e.g. ":///" becomes "://". + * + * @param $uri + * String reference containing the URI to normalize. + * + * @return + * The normalized URI. + */ +function file_stream_wrapper_uri_normalize($uri) { + $scheme = file_uri_scheme($uri); + + if ($scheme && file_stream_wrapper_valid_scheme($scheme)) { + $target = file_uri_target($uri); + + if ($target !== FALSE) { + $uri = $scheme . '://' . $target; + } + } + else { + // The default scheme is file:// + $url = 'file://' . $uri; + } + return $uri; +} + +/** + * Returns a reference to the stream wrapper class responsible for a given URI. + * + * The scheme determines the stream wrapper class that should be + * used by consulting the stream wrapper registry. + * + * @param $uri + * A stream, referenced as "scheme://target". + * + * @return + * Returns a new stream wrapper object appropriate for the given URI or FALSE + * if no registered handler could be found. For example, a URI of + * "private://example.txt" would return a new private stream wrapper object + * (DrupalPrivateStreamWrapper). + */ +function file_stream_wrapper_get_instance_by_uri($uri) { + $scheme = file_uri_scheme($uri); + $class = file_stream_wrapper_get_class($scheme); + if (class_exists($class)) { + $instance = new $class(); + $instance->setUri($uri); + return $instance; + } + else { + return FALSE; + } +} + +/** + * Returns a reference to the stream wrapper class responsible for a given scheme. + * + * This helper method returns a stream instance using a scheme. That is, the + * passed string does not contain a "://". For example, "public" is a scheme + * but "public://" is a URI (stream). This is because the later contains both + * a scheme and target despite target being empty. + * + * Note: the instance URI will be initialized to "scheme://" so that you can + * make the customary method calls as if you had retrieved an instance by URI. + * + * @param $scheme + * If the stream was "public://target", "public" would be the scheme. + * + * @return + * Returns a new stream wrapper object appropriate for the given $scheme. + * For example, for the public scheme a stream wrapper object + * (DrupalPublicStreamWrapper). + * FALSE is returned if no registered handler could be found. + */ +function file_stream_wrapper_get_instance_by_scheme($scheme) { + $class = file_stream_wrapper_get_class($scheme); + if (class_exists($class)) { + $instance = new $class(); + $instance->setUri($scheme . '://'); + return $instance; + } + else { + return FALSE; + } +} + +/** + * Creates a web-accessible URL for a stream to an external or local file. + * + * Compatibility: normal paths and stream wrappers. + * @see http://drupal.org/node/515192 + * + * There are two kinds of local files: + * - "managed files", i.e. those stored by a Drupal-compatible stream wrapper. + * These are files that have either been uploaded by users or were generated + * automatically (for example through CSS aggregation). + * - "shipped files", i.e. those outside of the files directory, which ship as + * part of Drupal core or contributed modules or themes. + * + * @param $uri + * The URI to a file for which we need an external URL, or the path to a + * shipped file. + * + * @return + * A string containing a URL that may be used to access the file. + * If the provided string already contains a preceding 'http', 'https', or + * '/', nothing is done and the same string is returned. If a stream wrapper + * could not be found to generate an external URL, then FALSE is returned. + */ +function file_create_url($uri) { + // Allow the URI to be altered, e.g. to serve a file from a CDN or static + // file server. + drupal_alter('file_url', $uri); + + $scheme = file_uri_scheme($uri); + + if (!$scheme) { + // Allow for: + // - root-relative URIs (e.g. /foo.jpg in http://example.com/foo.jpg) + // - protocol-relative URIs (e.g. //bar.jpg, which is expanded to + // http://example.com/bar.jpg by the browser when viewing a page over + // HTTP and to https://example.com/bar.jpg when viewing a HTTPS page) + // Both types of relative URIs are characterized by a leading slash, hence + // we can use a single check. + if (drupal_substr($uri, 0, 1) == '/') { + return $uri; + } + else { + // If this is not a properly formatted stream, then it is a shipped file. + // Therefore, return the urlencoded URI with the base URL prepended. + return $GLOBALS['base_url'] . '/' . drupal_encode_path($uri); + } + } + elseif ($scheme == 'http' || $scheme == 'https') { + // Check for http so that we don't have to implement getExternalUrl() for + // the http wrapper. + return $uri; + } + else { + // Attempt to return an external URL using the appropriate wrapper. + if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) { + return $wrapper->getExternalUrl(); + } + else { + return FALSE; + } + } +} + +/** + * Check that the directory exists and is writable. + * + * Directories need to have execute permissions to be considered a directory by + * FTP servers, etc. + * + * @param $directory + * A string reference containing the name of a directory path or URI. A + * trailing slash will be trimmed from a path. + * @param $options + * A bitmask to indicate if the directory should be created if it does + * not exist (FILE_CREATE_DIRECTORY) or made writable if it is read-only + * (FILE_MODIFY_PERMISSIONS). + * + * @return + * TRUE if the directory exists (or was created) and is writable. FALSE + * otherwise. + */ +function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) { + if (!file_stream_wrapper_valid_scheme(file_uri_scheme($directory))) { + // Only trim if we're not dealing with a stream. + $directory = rtrim($directory, '/\\'); + } + + // Check if directory exists. + if (!is_dir($directory)) { + // Let mkdir() recursively create directories and use the default directory + // permissions. + if (($options & FILE_CREATE_DIRECTORY) && @drupal_mkdir($directory, NULL, TRUE)) { + return drupal_chmod($directory); + } + return FALSE; + } + // The directory exists, so check to see if it is writable. + $writable = is_writable($directory); + if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) { + return drupal_chmod($directory); + } + + return $writable; +} + +/** + * If missing, create a .htaccess file in each Drupal files directory. + */ +function file_ensure_htaccess() { + file_create_htaccess('public://', FALSE); + if (variable_get('file_private_path', FALSE)) { + file_create_htaccess('private://', TRUE); + } + file_create_htaccess('temporary://', TRUE); +} + +/** + * Creates an .htaccess file in the given directory. + * + * @param $directory + * The directory. + * @param $private + * FALSE indicates that $directory should be an open and public directory. + * The default is TRUE which indicates a private and protected directory. + */ +function file_create_htaccess($directory, $private = TRUE) { + if (file_uri_scheme($directory)) { + $directory = file_stream_wrapper_uri_normalize($directory); + } + else { + $directory = rtrim($directory, '/\\'); + } + $htaccess_path = $directory . '/.htaccess'; + + if (file_exists($htaccess_path)) { + // Short circuit if the .htaccess file already exists. + return; + } + + if ($private) { + // Private .htaccess file. + $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nDeny from all\nOptions None\nOptions +FollowSymLinks"; + } + else { + // Public .htaccess file. + $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks"; + } + + // Write the .htaccess file. + if (file_put_contents($htaccess_path, $htaccess_lines)) { + drupal_chmod($htaccess_path, 0444); + } + else { + $variables = array('%directory' => $directory, '!htaccess' => '
' . nl2br(check_plain($htaccess_lines))); + watchdog('security', "Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: !htaccess", $variables, LOG_ERR); + } +} + +/** + * Loads file objects from the database. + * + * @param $fids + * An array of file IDs. + * @param $conditions + * (deprecated) An associative array of conditions on the {file_managed} + * table, where the keys are the database fields and the values are the + * values those fields must have. Instead, it is preferable to use + * EntityFieldQuery to retrieve a list of entity IDs loadable by + * this function. + * + * @return + * An array of file objects, indexed by fid. + * + * @see hook_file_load() + * @see file_load() + * @see entity_load() + * @see EntityFieldQuery + * + * @todo Remove $conditions in Drupal 8. + */ +function file_load_multiple($fids = array(), $conditions = array()) { + return entity_load('file', $fids, $conditions); +} + +/** + * Load a file object from the database. + * + * @param $fid + * A file ID. + * + * @return + * A file object. + * + * @see hook_file_load() + * @see file_load_multiple() + */ +function file_load($fid) { + $files = file_load_multiple(array($fid), array()); + return reset($files); +} + +/** + * Save a file object to the database. + * + * If the $file->fid is not set a new record will be added. + * + * @param $file + * A file object returned by file_load(). + * + * @return + * The updated file object. + * + * @see hook_file_insert() + * @see hook_file_update() + */ +function file_save(stdClass $file) { + $file->timestamp = REQUEST_TIME; + $file->filesize = filesize($file->uri); + + // Load the stored entity, if any. + if (!empty($file->fid) && !isset($file->original)) { + $file->original = entity_load_unchanged('file', $file->fid); + } + + module_invoke_all('file_presave', $file); + module_invoke_all('entity_presave', $file, 'file'); + + if (empty($file->fid)) { + drupal_write_record('file_managed', $file); + // Inform modules about the newly added file. + module_invoke_all('file_insert', $file); + module_invoke_all('entity_insert', $file, 'file'); + } + else { + drupal_write_record('file_managed', $file, 'fid'); + // Inform modules that the file has been updated. + module_invoke_all('file_update', $file); + module_invoke_all('entity_update', $file, 'file'); + } + + unset($file->original); + return $file; +} + +/** + * Determines where a file is used. + * + * @param $file + * A file object. + * + * @return + * A nested array with usage data. The first level is keyed by module name, + * the second by object type, the third has 'id' and 'count' keys. + * + * @see file_usage_add() + * @see file_usage_delete() + */ +function file_usage_list(stdClass $file) { + $result = db_select('file_usage', 'f') + ->fields('f', array('module', 'type', 'id', 'count')) + ->condition('fid', $file->fid) + ->condition('count', 0, '>') + ->execute(); + $references = array(); + foreach ($result as $usage) { + $references[$usage->module][$usage->type] = array('id' => $usage->id, 'count' => $usage->count); + } + return $references; +} + +/** + * Records that a module is using a file. + * + * This usage information will be queried during file_delete() to ensure that + * a file is not in use before it is physically removed from disk. + * + * Examples: + * - A module that associates files with nodes, so $type would be + * 'node' and $id would be the node's nid. Files for all revisions are stored + * within a single nid. + * - The User module associates an image with a user, so $type would be 'user' + * and the $id would be the user's uid. + * + * @param $file + * A file object. + * @param $module + * The name of the module using the file. + * @param $type + * The type of the object that contains the referenced file. + * @param $id + * The unique, numeric ID of the object containing the referenced file. + * @param $count + * (optional) The number of references to add to the object. Defaults to 1. + * + * @see file_usage_list() + * @see file_usage_delete() + */ +function file_usage_add(stdClass $file, $module, $type, $id, $count = 1) { + db_merge('file_usage') + ->key(array( + 'fid' => $file->fid, + 'module' => $module, + 'type' => $type, + 'id' => $id, + )) + ->fields(array('count' => $count)) + ->expression('count', 'count + :count', array(':count' => $count)) + ->execute(); +} + +/** + * Removes a record to indicate that a module is no longer using a file. + * + * The file_delete() function is typically called after removing a file usage + * to remove the record from the file_managed table and delete the file itself. + * + * @param $file + * A file object. + * @param $module + * The name of the module using the file. + * @param $type + * (optional) The type of the object that contains the referenced file. May + * be omitted if all module references to a file are being deleted. + * @param $id + * (optional) The unique, numeric ID of the object containing the referenced + * file. May be omitted if all module references to a file are being deleted. + * @param $count + * (optional) The number of references to delete from the object. Defaults to + * 1. 0 may be specified to delete all references to the file within a + * specific object. + * + * @see file_usage_add() + * @see file_usage_list() + * @see file_delete() + */ +function file_usage_delete(stdClass $file, $module, $type = NULL, $id = NULL, $count = 1) { + // Delete rows that have a exact or less value to prevent empty rows. + $query = db_delete('file_usage') + ->condition('module', $module) + ->condition('fid', $file->fid); + if ($type && $id) { + $query + ->condition('type', $type) + ->condition('id', $id); + } + if ($count) { + $query->condition('count', $count, '<='); + } + $result = $query->execute(); + + // If the row has more than the specified count decrement it by that number. + if (!$result && $count > 0) { + $query = db_update('file_usage') + ->condition('module', $module) + ->condition('fid', $file->fid); + if ($type && $id) { + $query + ->condition('type', $type) + ->condition('id', $id); + } + $query->expression('count', 'count - :count', array(':count' => $count)); + $query->execute(); + } +} + +/** + * Copies a file to a new location and adds a file record to the database. + * + * This function should be used when manipulating files that have records + * stored in the database. This is a powerful function that in many ways + * performs like an advanced version of copy(). + * - Checks if $source and $destination are valid and readable/writable. + * - Checks that $source is not equal to $destination; if they are an error + * is reported. + * - If file already exists in $destination either the call will error out, + * replace the file or rename the file based on the $replace parameter. + * - Adds the new file to the files database. If the source file is a + * temporary file, the resulting file will also be a temporary file. See + * file_save_upload() for details on temporary files. + * + * @param $source + * A file object. + * @param $destination + * A string containing the destination that $source should be copied to. + * This must be a stream wrapper URI. If this value is omitted, Drupal's + * default files scheme will be used, usually "public://". + * @param $replace + * Replace behavior when the destination file already exists: + * - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with + * the destination name exists then its database entry will be updated. If + * no database entry is found then a new one will be created. + * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is + * unique. + * - FILE_EXISTS_ERROR - Do nothing and return FALSE. + * + * @return + * File object if the copy is successful, or FALSE in the event of an error. + * + * @see file_unmanaged_copy() + * @see hook_file_copy() + */ +function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) { + if (!file_valid_uri($destination)) { + watchdog('file', 'File %file (%realpath) could not be copied, because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', array('%file' => $source->uri, '%realpath' => drupal_realpath($source->uri), '%destination' => $destination)); + drupal_set_message(t('The specified file %file could not be copied, because the destination is invalid. More information is available in the system log.', array('%file' => $source->uri)), 'error'); + return FALSE; + } + + if ($uri = file_unmanaged_copy($source->uri, $destination, $replace)) { + $file = clone $source; + $file->fid = NULL; + $file->uri = $uri; + $file->filename = basename($uri); + // If we are replacing an existing file re-use its database record. + if ($replace == FILE_EXISTS_REPLACE) { + $existing_files = file_load_multiple(array(), array('uri' => $uri)); + if (count($existing_files)) { + $existing = reset($existing_files); + $file->fid = $existing->fid; + $file->filename = $existing->filename; + } + } + // If we are renaming around an existing file (rather than a directory), + // use its basename for the filename. + elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) { + $file->filename = basename($destination); + } + + $file = file_save($file); + + // Inform modules that the file has been copied. + module_invoke_all('file_copy', $file, $source); + + return $file; + } + return FALSE; +} + +/** + * Determine whether the URI has a valid scheme for file API operations. + * + * There must be a scheme and it must be a Drupal-provided scheme like + * 'public', 'private', 'temporary', or an extension provided with + * hook_stream_wrappers(). + * + * @param $uri + * The URI to be tested. + * + * @return + * TRUE if the URI is allowed. + */ +function file_valid_uri($uri) { + // Assert that the URI has an allowed scheme. Barepaths are not allowed. + $uri_scheme = file_uri_scheme($uri); + if (empty($uri_scheme) || !file_stream_wrapper_valid_scheme($uri_scheme)) { + return FALSE; + } + return TRUE; +} + +/** + * Copies a file to a new location without invoking the file API. + * + * This is a powerful function that in many ways performs like an advanced + * version of copy(). + * - Checks if $source and $destination are valid and readable/writable. + * - Checks that $source is not equal to $destination; if they are an error + * is reported. + * - If file already exists in $destination either the call will error out, + * replace the file or rename the file based on the $replace parameter. + * + * @param $source + * A string specifying the filepath or URI of the source file. + * @param $destination + * A URI containing the destination that $source should be copied to. The + * URI may be a bare filepath (without a scheme) and in that case the default + * scheme (file://) will be used. If this value is omitted, Drupal's default + * files scheme will be used, usually "public://". + * @param $replace + * Replace behavior when the destination file already exists: + * - FILE_EXISTS_REPLACE - Replace the existing file. + * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is + * unique. + * - FILE_EXISTS_ERROR - Do nothing and return FALSE. + * + * @return + * The path to the new file, or FALSE in the event of an error. + * + * @see file_copy() + */ +function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) { + $original_source = $source; + $original_destination = $destination; + + // Assert that the source file actually exists. + $source = drupal_realpath($source); + if (!file_exists($source)) { + // @todo Replace drupal_set_message() calls with exceptions instead. + drupal_set_message(t('The specified file %file could not be copied, because no file by that name exists. Please check that you supplied the correct filename.', array('%file' => $original_source)), 'error'); + watchdog('file', 'File %file (%realpath) could not be copied because it does not exist.', array('%file' => $original_source, '%realpath' => drupal_realpath($original_source))); + return FALSE; + } + + // Build a destination URI if necessary. + if (!isset($destination)) { + $destination = file_build_uri(basename($source)); + } + + + // Prepare the destination directory. + if (file_prepare_directory($destination)) { + // The destination is already a directory, so append the source basename. + $destination = file_stream_wrapper_uri_normalize($destination . '/' . basename($source)); + } + else { + // Perhaps $destination is a dir/file? + $dirname = drupal_dirname($destination); + if (!file_prepare_directory($dirname)) { + // The destination is not valid. + watchdog('file', 'File %file could not be copied, because the destination directory %destination is not configured correctly.', array('%file' => $original_source, '%destination' => drupal_realpath($dirname))); + drupal_set_message(t('The specified file %file could not be copied, because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $original_source)), 'error'); + return FALSE; + } + } + + // Determine whether we can perform this operation based on overwrite rules. + $destination = file_destination($destination, $replace); + if ($destination === FALSE) { + drupal_set_message(t('The file %file could not be copied because a file by that name already exists in the destination directory.', array('%file' => $original_source)), 'error'); + watchdog('file', 'File %file could not be copied because a file by that name already exists in the destination directory (%directory)', array('%file' => $original_source, '%destination' => drupal_realpath($destination))); + return FALSE; + } + + // Assert that the source and destination filenames are not the same. + if (drupal_realpath($source) == drupal_realpath($destination)) { + drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error'); + watchdog('file', 'File %file could not be copied because it would overwrite itself.', array('%file' => $source)); + return FALSE; + } + // Make sure the .htaccess files are present. + file_ensure_htaccess(); + // Perform the copy operation. + if (!@copy($source, $destination)) { + watchdog('file', 'The specified file %file could not be copied to %destination.', array('%file' => $source, '%destination' => drupal_realpath($destination)), LOG_ERR); + return FALSE; + } + + // Set the permissions on the new file. + drupal_chmod($destination); + + return $destination; +} + +/** + * Given a relative path, construct a URI into Drupal's default files location. + */ +function file_build_uri($path) { + $uri = file_default_scheme() . '://' . $path; + return file_stream_wrapper_uri_normalize($uri); +} + +/** + * Determines the destination path for a file depending on how replacement of + * existing files should be handled. + * + * @param $destination + * A string specifying the desired final URI or filepath. + * @param $replace + * Replace behavior when the destination file already exists. + * - FILE_EXISTS_REPLACE - Replace the existing file. + * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is + * unique. + * - FILE_EXISTS_ERROR - Do nothing and return FALSE. + * + * @return + * The destination filepath, or FALSE if the file already exists + * and FILE_EXISTS_ERROR is specified. + */ +function file_destination($destination, $replace) { + if (file_exists($destination)) { + switch ($replace) { + case FILE_EXISTS_REPLACE: + // Do nothing here, we want to overwrite the existing file. + break; + + case FILE_EXISTS_RENAME: + $basename = basename($destination); + $directory = drupal_dirname($destination); + $destination = file_create_filename($basename, $directory); + break; + + case FILE_EXISTS_ERROR: + // Error reporting handled by calling function. + return FALSE; + } + } + return $destination; +} + +/** + * Move a file to a new location and update the file's database entry. + * + * Moving a file is performed by copying the file to the new location and then + * deleting the original. + * - Checks if $source and $destination are valid and readable/writable. + * - Performs a file move if $source is not equal to $destination. + * - If file already exists in $destination either the call will error out, + * replace the file or rename the file based on the $replace parameter. + * - Adds the new file to the files database. + * + * @param $source + * A file object. + * @param $destination + * A string containing the destination that $source should be moved to. + * This must be a stream wrapper URI. If this value is omitted, Drupal's + * default files scheme will be used, usually "public://". + * @param $replace + * Replace behavior when the destination file already exists: + * - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with + * the destination name exists then its database entry will be updated and + * file_delete() called on the source file after hook_file_move is called. + * If no database entry is found then the source files record will be + * updated. + * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is + * unique. + * - FILE_EXISTS_ERROR - Do nothing and return FALSE. + * + * @return + * Resulting file object for success, or FALSE in the event of an error. + * + * @see file_unmanaged_move() + * @see hook_file_move() + */ +function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) { + if (!file_valid_uri($destination)) { + watchdog('file', 'File %file (%realpath) could not be moved, because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', array('%file' => $source->uri, '%realpath' => drupal_realpath($source->uri), '%destination' => $destination)); + drupal_set_message(t('The specified file %file could not be moved, because the destination is invalid. More information is available in the system log.', array('%file' => $source->uri)), 'error'); + return FALSE; + } + + if ($uri = file_unmanaged_move($source->uri, $destination, $replace)) { + $delete_source = FALSE; + + $file = clone $source; + $file->uri = $uri; + // If we are replacing an existing file re-use its database record. + if ($replace == FILE_EXISTS_REPLACE) { + $existing_files = file_load_multiple(array(), array('uri' => $uri)); + if (count($existing_files)) { + $existing = reset($existing_files); + $delete_source = TRUE; + $file->fid = $existing->fid; + } + } + // If we are renaming around an existing file (rather than a directory), + // use its basename for the filename. + elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) { + $file->filename = basename($destination); + } + + $file = file_save($file); + + // Inform modules that the file has been moved. + module_invoke_all('file_move', $file, $source); + + if ($delete_source) { + // Try a soft delete to remove original if it's not in use elsewhere. + file_delete($source); + } + + return $file; + } + return FALSE; +} + +/** + * Move a file to a new location without calling any hooks or making any + * changes to the database. + * + * @param $source + * A string specifying the filepath or URI of the original file. + * @param $destination + * A string containing the destination that $source should be moved to. + * This must be a stream wrapper URI. If this value is omitted, Drupal's + * default files scheme will be used, usually "public://". + * @param $replace + * Replace behavior when the destination file already exists: + * - FILE_EXISTS_REPLACE - Replace the existing file. + * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is + * unique. + * - FILE_EXISTS_ERROR - Do nothing and return FALSE. + * + * @return + * The URI of the moved file, or FALSE in the event of an error. + * + * @see file_move() + */ +function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) { + $filepath = file_unmanaged_copy($source, $destination, $replace); + if ($filepath == FALSE || file_unmanaged_delete($source) == FALSE) { + return FALSE; + } + return $filepath; +} + +/** + * Modify a filename as needed for security purposes. + * + * Munging a file name prevents unknown file extensions from masking exploit + * files. When web servers such as Apache decide how to process a URL request, + * they use the file extension. If the extension is not recognized, Apache + * skips that extension and uses the previous file extension. For example, if + * the file being requested is exploit.php.pps, and Apache does not recognize + * the '.pps' extension, it treats the file as PHP and executes it. To make + * this file name safe for Apache and prevent it from executing as PHP, the + * .php extension is "munged" into .php_, making the safe file name + * exploit.php_.pps. + * + * Specifically, this function adds an underscore to all extensions that are + * between 2 and 5 characters in length, internal to the file name, and not + * included in $extensions. + * + * Function behavior is also controlled by the Drupal variable + * 'allow_insecure_uploads'. If 'allow_insecure_uploads' evaluates to TRUE, no + * alterations will be made, if it evaluates to FALSE, the filename is 'munged'. + * + * @param $filename + * File name to modify. + * @param $extensions + * A space-separated list of extensions that should not be altered. + * @param $alerts + * If TRUE, drupal_set_message() will be called to display a message if the + * file name was changed. + * + * @return + * The potentially modified $filename. + */ +function file_munge_filename($filename, $extensions, $alerts = TRUE) { + $original = $filename; + + // Allow potentially insecure uploads for very savvy users and admin + if (!variable_get('allow_insecure_uploads', 0)) { + $whitelist = array_unique(explode(' ', trim($extensions))); + + // Split the filename up by periods. The first part becomes the basename + // the last part the final extension. + $filename_parts = explode('.', $filename); + $new_filename = array_shift($filename_parts); // Remove file basename. + $final_extension = array_pop($filename_parts); // Remove final extension. + + // Loop through the middle parts of the name and add an underscore to the + // end of each section that could be a file extension but isn't in the list + // of allowed extensions. + foreach ($filename_parts as $filename_part) { + $new_filename .= '.' . $filename_part; + if (!in_array($filename_part, $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) { + $new_filename .= '_'; + } + } + $filename = $new_filename . '.' . $final_extension; + + if ($alerts && $original != $filename) { + drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $filename))); + } + } + + return $filename; +} + +/** + * Undo the effect of upload_munge_filename(). + * + * @param $filename + * String with the filename to be unmunged. + * + * @return + * An unmunged filename string. + */ +function file_unmunge_filename($filename) { + return str_replace('_.', '.', $filename); +} + +/** + * Create a full file path from a directory and filename. + * + * If a file with the specified name already exists, an alternative will be + * used. + * + * @param $basename + * String filename + * @param $directory + * String containing the directory or parent URI. + * + * @return + * File path consisting of $directory and a unique filename based off + * of $basename. + */ +function file_create_filename($basename, $directory) { + // Strip control characters (ASCII value < 32). Though these are allowed in + // some filesystems, not many applications handle them well. + $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename); + if (substr(PHP_OS, 0, 3) == 'WIN') { + // These characters are not allowed in Windows filenames + $basename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $basename); + } + + // A URI or path may already have a trailing slash or look like "public://". + if (substr($directory, -1) == '/') { + $separator = ''; + } + else { + $separator = '/'; + } + + $destination = $directory . $separator . $basename; + + if (file_exists($destination)) { + // Destination file already exists, generate an alternative. + $pos = strrpos($basename, '.'); + if ($pos !== FALSE) { + $name = substr($basename, 0, $pos); + $ext = substr($basename, $pos); + } + else { + $name = $basename; + $ext = ''; + } + + $counter = 0; + do { + $destination = $directory . $separator . $name . '_' . $counter++ . $ext; + } while (file_exists($destination)); + } + + return $destination; +} + +/** + * Delete a file and its database record. + * + * If the $force parameter is not TRUE, file_usage_list() will be called to + * determine if the file is being used by any modules. If the file is being + * used the delete will be canceled. + * + * @param $file + * A file object. + * @param $force + * Boolean indicating that the file should be deleted even if the file is + * reported as in use by the file_usage table. + * + * @return mixed + * TRUE for success, FALSE in the event of an error, or an array if the file + * is being used by any modules. + * + * @see file_unmanaged_delete() + * @see file_usage_list() + * @see file_usage_delete() + * @see hook_file_delete() + */ +function file_delete(stdClass $file, $force = FALSE) { + if (!file_valid_uri($file->uri)) { + watchdog('file', 'File %file (%realpath) could not be deleted because it is not a valid URI. This may be caused by improper use of file_delete() or a missing stream wrapper.', array('%file' => $file->uri, '%realpath' => drupal_realpath($file->uri))); + drupal_set_message(t('The specified file %file could not be deleted, because it is not a valid URI. More information is available in the system log.', array('%file' => $file->uri)), 'error'); + return FALSE; + } + + // If any module still has a usage entry in the file_usage table, the file + // will not be deleted, but file_delete() will return a populated array + // that tests as TRUE. + if (!$force && ($references = file_usage_list($file))) { + return $references; + } + + // Let other modules clean up any references to the deleted file. + module_invoke_all('file_delete', $file); + module_invoke_all('entity_delete', $file, 'file'); + + // Make sure the file is deleted before removing its row from the + // database, so UIs can still find the file in the database. + if (file_unmanaged_delete($file->uri)) { + db_delete('file_managed')->condition('fid', $file->fid)->execute(); + db_delete('file_usage')->condition('fid', $file->fid)->execute(); + return TRUE; + } + return FALSE; +} + +/** + * Delete a file without calling any hooks or making any changes to the + * database. + * + * This function should be used when the file to be deleted does not have an + * entry recorded in the files table. + * + * @param $path + * A string containing a file path or (streamwrapper) URI. + * + * @return + * TRUE for success or path does not exist, or FALSE in the event of an + * error. + * + * @see file_delete() + * @see file_unmanaged_delete_recursive() + */ +function file_unmanaged_delete($path) { + // Resolve streamwrapper URI to local path. + $path = drupal_realpath($path); + if (is_dir($path)) { + watchdog('file', '%path is a directory and cannot be removed using file_unmanaged_delete().', array('%path' => $path), LOG_ERR); + return FALSE; + } + if (is_file($path)) { + return drupal_unlink($path); + } + // Return TRUE for non-existent file, but log that nothing was actually + // deleted, as the current state is the intended result. + if (!file_exists($path)) { + watchdog('file', 'The file %path was not deleted, because it does not exist.', array('%path' => $path), LOG_NOTICE); + return TRUE; + } + // We cannot handle anything other than files and directories. Log an error + // for everything else (sockets, symbolic links, etc). + watchdog('file', 'The file %path is not of a recognized type so it was not deleted.', array('%path' => $path), LOG_ERR); + return FALSE; +} + +/** + * Recursively delete all files and directories in the specified filepath. + * + * If the specified path is a directory then the function will call itself + * recursively to process the contents. Once the contents have been removed the + * directory will also be removed. + * + * If the specified path is a file then it will be passed to + * file_unmanaged_delete(). + * + * Note that this only deletes visible files with write permission. + * + * @param $path + * A string containing either an URI or a file or directory path. + * + * @return + * TRUE for success or if path does not exist, FALSE in the event of an + * error. + * + * @see file_unmanaged_delete() + */ +function file_unmanaged_delete_recursive($path) { + // Resolve streamwrapper URI to local path. + $path = drupal_realpath($path); + if (is_dir($path)) { + $dir = dir($path); + while (($entry = $dir->read()) !== FALSE) { + if ($entry == '.' || $entry == '..') { + continue; + } + $entry_path = $path . '/' . $entry; + file_unmanaged_delete_recursive($entry_path); + } + $dir->close(); + + return drupal_rmdir($path); + } + return file_unmanaged_delete($path); +} + +/** + * Determine total disk space used by a single user or the whole filesystem. + * + * @param $uid + * Optional. A user id, specifying NULL returns the total space used by all + * non-temporary files. + * @param $status + * Optional. The file status to consider. The default is to only + * consider files in status FILE_STATUS_PERMANENT. + * + * @return + * An integer containing the number of bytes used. + */ +function file_space_used($uid = NULL, $status = FILE_STATUS_PERMANENT) { + $query = db_select('file_managed', 'f'); + $query->condition('f.status', $status); + $query->addExpression('SUM(f.filesize)', 'filesize'); + if (isset($uid)) { + $query->condition('f.uid', $uid); + } + return $query->execute()->fetchField(); +} + +/** + * Saves a file upload to a new location. + * + * The file will be added to the {file_managed} table as a temporary file. + * Temporary files are periodically cleaned. To make the file a permanent file, + * assign the status and use file_save() to save the changes. + * + * @param $source + * A string specifying the filepath or URI of the uploaded file to save. + * @param $validators + * An optional, associative array of callback functions used to validate the + * file. See file_validate() for a full discussion of the array format. + * If no extension validator is provided it will default to a limited safe + * list of extensions which is as follows: "jpg jpeg gif png txt + * doc xls pdf ppt pps odt ods odp". To allow all extensions you must + * explicitly set the 'file_validate_extensions' validator to an empty array + * (Beware: this is not safe and should only be allowed for trusted users, if + * at all). + * @param $destination + * A string containing the URI $source should be copied to. + * This must be a stream wrapper URI. If this value is omitted, Drupal's + * temporary files scheme will be used ("temporary://"). + * @param $replace + * Replace behavior when the destination file already exists: + * - FILE_EXISTS_REPLACE: Replace the existing file. + * - FILE_EXISTS_RENAME: Append _{incrementing number} until the filename is + * unique. + * - FILE_EXISTS_ERROR: Do nothing and return FALSE. + * + * @return + * An object containing the file information if the upload succeeded, FALSE + * in the event of an error, or NULL if no file was uploaded. The + * documentation for the "File interface" group, which you can find under + * Related topics, or the header at the top of this file, documents the + * components of a file object. In addition to the standard components, + * this function adds: + * - source: Path to the file before it is moved. + * - destination: Path to the file after it is moved (same as 'uri'). + */ +function file_save_upload($source, $validators = array(), $destination = FALSE, $replace = FILE_EXISTS_RENAME) { + global $user; + static $upload_cache; + + // Return cached objects without processing since the file will have + // already been processed and the paths in _FILES will be invalid. + if (isset($upload_cache[$source])) { + return $upload_cache[$source]; + } + + // Make sure there's an upload to process. + if (empty($_FILES['files']['name'][$source])) { + return NULL; + } + + // Check for file upload errors and return FALSE if a lower level system + // error occurred. For a complete list of errors: + // See http://php.net/manual/en/features.file-upload.errors.php. + switch ($_FILES['files']['error'][$source]) { + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + drupal_set_message(t('The file %file could not be saved, because it exceeds %maxsize, the maximum allowed size for uploads.', array('%file' => $_FILES['files']['name'][$source], '%maxsize' => format_size(file_upload_max_size()))), 'error'); + return FALSE; + + case UPLOAD_ERR_PARTIAL: + case UPLOAD_ERR_NO_FILE: + drupal_set_message(t('The file %file could not be saved, because the upload did not complete.', array('%file' => $_FILES['files']['name'][$source])), 'error'); + return FALSE; + + case UPLOAD_ERR_OK: + // Final check that this is a valid upload, if it isn't, use the + // default error handler. + if (is_uploaded_file($_FILES['files']['tmp_name'][$source])) { + break; + } + + // Unknown error + default: + drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $_FILES['files']['name'][$source])), 'error'); + return FALSE; + } + + // Begin building file object. + $file = new stdClass(); + $file->uid = $user->uid; + $file->status = 0; + $file->filename = trim(basename($_FILES['files']['name'][$source]), '.'); + $file->uri = $_FILES['files']['tmp_name'][$source]; + $file->filemime = file_get_mimetype($file->filename); + $file->filesize = $_FILES['files']['size'][$source]; + + $extensions = ''; + if (isset($validators['file_validate_extensions'])) { + if (isset($validators['file_validate_extensions'][0])) { + // Build the list of non-munged extensions if the caller provided them. + $extensions = $validators['file_validate_extensions'][0]; + } + else { + // If 'file_validate_extensions' is set and the list is empty then the + // caller wants to allow any extension. In this case we have to remove the + // validator or else it will reject all extensions. + unset($validators['file_validate_extensions']); + } + } + else { + // No validator was provided, so add one using the default list. + // Build a default non-munged safe list for file_munge_filename(). + $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp'; + $validators['file_validate_extensions'] = array(); + $validators['file_validate_extensions'][0] = $extensions; + } + + if (!empty($extensions)) { + // Munge the filename to protect against possible malicious extension hiding + // within an unknown file type (ie: filename.html.foo). + $file->filename = file_munge_filename($file->filename, $extensions); + } + + // Rename potentially executable files, to help prevent exploits (i.e. will + // rename filename.php.foo and filename.php to filename.php.foo.txt and + // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads' + // evaluates to TRUE. + if (!variable_get('allow_insecure_uploads', 0) && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $file->filename) && (substr($file->filename, -4) != '.txt')) { + $file->filemime = 'text/plain'; + $file->uri .= '.txt'; + $file->filename .= '.txt'; + // The .txt extension may not be in the allowed list of extensions. We have + // to add it here or else the file upload will fail. + if (!empty($extensions)) { + $validators['file_validate_extensions'][0] .= ' txt'; + drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $file->filename))); + } + } + + // If the destination is not provided, use the temporary directory. + if (empty($destination)) { + $destination = 'temporary://'; + } + + // Assert that the destination contains a valid stream. + $destination_scheme = file_uri_scheme($destination); + if (!$destination_scheme || !file_stream_wrapper_valid_scheme($destination_scheme)) { + drupal_set_message(t('The file could not be uploaded, because the destination %destination is invalid.', array('%destination' => $destination)), 'error'); + return FALSE; + } + + $file->source = $source; + // A URI may already have a trailing slash or look like "public://". + if (substr($destination, -1) != '/') { + $destination .= '/'; + } + $file->destination = file_destination($destination . $file->filename, $replace); + // If file_destination() returns FALSE then $replace == FILE_EXISTS_ERROR and + // there's an existing file so we need to bail. + if ($file->destination === FALSE) { + drupal_set_message(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', array('%source' => $source, '%directory' => $destination)), 'error'); + return FALSE; + } + + // Add in our check of the the file name length. + $validators['file_validate_name_length'] = array(); + + // Call the validation functions specified by this function's caller. + $errors = file_validate($file, $validators); + + // Check for errors. + if (!empty($errors)) { + $message = t('The specified file %name could not be uploaded.', array('%name' => $file->filename)); + if (count($errors) > 1) { + $message .= theme('item_list', array('items' => $errors)); + } + else { + $message .= ' ' . array_pop($errors); + } + form_set_error($source, $message); + return FALSE; + } + + // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary + // directory. This overcomes open_basedir restrictions for future file + // operations. + $file->uri = $file->destination; + if (!move_uploaded_file($_FILES['files']['tmp_name'][$source], $file->uri)) { + form_set_error($source, t('File upload error. Could not move uploaded file.')); + watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->uri)); + return FALSE; + } + + // Set the permissions on the new file. + drupal_chmod($file->uri); + + // If we are replacing an existing file re-use its database record. + if ($replace == FILE_EXISTS_REPLACE) { + $existing_files = file_load_multiple(array(), array('uri' => $file->uri)); + if (count($existing_files)) { + $existing = reset($existing_files); + $file->fid = $existing->fid; + } + } + + // If we made it this far it's safe to record this file in the database. + if ($file = file_save($file)) { + // Add file to the cache. + $upload_cache[$source] = $file; + return $file; + } + return FALSE; +} + + +/** + * Check that a file meets the criteria specified by the validators. + * + * After executing the validator callbacks specified hook_file_validate() will + * also be called to allow other modules to report errors about the file. + * + * @param $file + * A Drupal file object. + * @param $validators + * An optional, associative array of callback functions used to validate the + * file. The keys are function names and the values arrays of callback + * parameters which will be passed in after the file object. The + * functions should return an array of error messages; an empty array + * indicates that the file passed validation. The functions will be called in + * the order specified. + * + * @return + * An array containing validation error messages. + * + * @see hook_file_validate() + */ +function file_validate(stdClass &$file, $validators = array()) { + // Call the validation functions specified by this function's caller. + $errors = array(); + foreach ($validators as $function => $args) { + if (function_exists($function)) { + array_unshift($args, $file); + $errors = array_merge($errors, call_user_func_array($function, $args)); + } + } + + // Let other modules perform validation on the new file. + return array_merge($errors, module_invoke_all('file_validate', $file)); +} + +/** + * Check for files with names longer than we can store in the database. + * + * @param $file + * A Drupal file object. + * @return + * An array. If the file name is too long, it will contain an error message. + */ +function file_validate_name_length(stdClass $file) { + $errors = array(); + + if (empty($file->filename)) { + $errors[] = t("The file's name is empty. Please give a name to the file."); + } + if (strlen($file->filename) > 240) { + $errors[] = t("The file's name exceeds the 240 characters limit. Please rename the file and try again."); + } + return $errors; +} + +/** + * Check that the filename ends with an allowed extension. + * + * @param $file + * A Drupal file object. + * @param $extensions + * A string with a space separated list of allowed extensions. + * + * @return + * An array. If the file extension is not allowed, it will contain an error + * message. + * + * @see hook_file_validate() + */ +function file_validate_extensions(stdClass $file, $extensions) { + $errors = array(); + + $regex = '/\.(' . preg_replace('/ +/', '|', preg_quote($extensions)) . ')$/i'; + if (!preg_match($regex, $file->filename)) { + $errors[] = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => $extensions)); + } + return $errors; +} + +/** + * Check that the file's size is below certain limits. + * + * This check is not enforced for the user #1. + * + * @param $file + * A Drupal file object. + * @param $file_limit + * An integer specifying the maximum file size in bytes. Zero indicates that + * no limit should be enforced. + * @param $user_limit + * An integer specifying the maximum number of bytes the user is allowed. + * Zero indicates that no limit should be enforced. + * + * @return + * An array. If the file size exceeds limits, it will contain an error + * message. + * + * @see hook_file_validate() + */ +function file_validate_size(stdClass $file, $file_limit = 0, $user_limit = 0) { + global $user; + + $errors = array(); + + // Bypass validation for uid = 1. + if ($user->uid != 1) { + if ($file_limit && $file->filesize > $file_limit) { + $errors[] = t('The file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size($file->filesize), '%maxsize' => format_size($file_limit))); + } + + // Save a query by only calling file_space_used() when a limit is provided. + if ($user_limit && (file_space_used($user->uid) + $file->filesize) > $user_limit) { + $errors[] = t('The file is %filesize which would exceed your disk quota of %quota.', array('%filesize' => format_size($file->filesize), '%quota' => format_size($user_limit))); + } + } + return $errors; +} + +/** + * Check that the file is recognized by image_get_info() as an image. + * + * @param $file + * A Drupal file object. + * + * @return + * An array. If the file is not an image, it will contain an error message. + * + * @see hook_file_validate() + */ +function file_validate_is_image(stdClass $file) { + $errors = array(); + + $info = image_get_info($file->uri); + if (!$info || empty($info['extension'])) { + $errors[] = t('Only JPEG, PNG and GIF images are allowed.'); + } + + return $errors; +} + +/** + * Verify that image dimensions are within the specified maximum and minimum. + * + * Non-image files will be ignored. If a image toolkit is available the image + * will be scaled to fit within the desired maximum dimensions. + * + * @param $file + * A Drupal file object. This function may resize the file affecting its + * size. + * @param $maximum_dimensions + * An optional string in the form WIDTHxHEIGHT e.g. '640x480' or '85x85'. If + * an image toolkit is installed the image will be resized down to these + * dimensions. A value of 0 indicates no restriction on size, so resizing + * will be attempted. + * @param $minimum_dimensions + * An optional string in the form WIDTHxHEIGHT. This will check that the + * image meets a minimum size. A value of 0 indicates no restriction. + * + * @return + * An array. If the file is an image and did not meet the requirements, it + * will contain an error message. + * + * @see hook_file_validate() + */ +function file_validate_image_resolution(stdClass $file, $maximum_dimensions = 0, $minimum_dimensions = 0) { + $errors = array(); + + // Check first that the file is an image. + if ($info = image_get_info($file->uri)) { + if ($maximum_dimensions) { + // Check that it is smaller than the given dimensions. + list($width, $height) = explode('x', $maximum_dimensions); + if ($info['width'] > $width || $info['height'] > $height) { + // Try to resize the image to fit the dimensions. + if ($image = image_load($file->uri)) { + image_scale($image, $width, $height); + image_save($image); + $file->filesize = $image->info['file_size']; + drupal_set_message(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', array('%dimensions' => $maximum_dimensions))); + } + else { + $errors[] = t('The image is too large; the maximum dimensions are %dimensions pixels.', array('%dimensions' => $maximum_dimensions)); + } + } + } + + if ($minimum_dimensions) { + // Check that it is larger than the given dimensions. + list($width, $height) = explode('x', $minimum_dimensions); + if ($info['width'] < $width || $info['height'] < $height) { + $errors[] = t('The image is too small; the minimum dimensions are %dimensions pixels.', array('%dimensions' => $minimum_dimensions)); + } + } + } + + return $errors; +} + +/** + * Save a string to the specified destination and create a database file entry. + * + * @param $data + * A string containing the contents of the file. + * @param $destination + * A string containing the destination URI. + * This must be a stream wrapper URI. If this value is omitted, Drupal's + * default files scheme will be used, usually "public://". + * @param $replace + * Replace behavior when the destination file already exists: + * - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with + * the destination name exists then its database entry will be updated. If + * no database entry is found then a new one will be created. + * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is + * unique. + * - FILE_EXISTS_ERROR - Do nothing and return FALSE. + * + * @return + * A file object, or FALSE on error. + * + * @see file_unmanaged_save_data() + */ +function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) { + global $user; + + if (empty($destination)) { + $destination = file_default_scheme() . '://'; + } + if (!file_valid_uri($destination)) { + watchdog('file', 'The data could not be saved because the destination %destination is invalid. This may be caused by improper use of file_save_data() or a missing stream wrapper.', array('%destination' => $destination)); + drupal_set_message(t('The data could not be saved, because the destination is invalid. More information is available in the system log.'), 'error'); + return FALSE; + } + + if ($uri = file_unmanaged_save_data($data, $destination, $replace)) { + // Create a file object. + $file = new stdClass(); + $file->fid = NULL; + $file->uri = $uri; + $file->filename = basename($uri); + $file->filemime = file_get_mimetype($file->uri); + $file->uid = $user->uid; + $file->status = FILE_STATUS_PERMANENT; + // If we are replacing an existing file re-use its database record. + if ($replace == FILE_EXISTS_REPLACE) { + $existing_files = file_load_multiple(array(), array('uri' => $uri)); + if (count($existing_files)) { + $existing = reset($existing_files); + $file->fid = $existing->fid; + $file->filename = $existing->filename; + } + } + // If we are renaming around an existing file (rather than a directory), + // use its basename for the filename. + elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) { + $file->filename = basename($destination); + } + + return file_save($file); + } + return FALSE; +} + +/** + * Save a string to the specified destination without invoking file API. + * + * This function is identical to file_save_data() except the file will not be + * saved to the {file_managed} table and none of the file_* hooks will be + * called. + * + * @param $data + * A string containing the contents of the file. + * @param $destination + * A string containing the destination location. + * This must be a stream wrapper URI. If no value is provided, a + * randomized name will be generated and the file is saved using Drupal's + * default files scheme, usually "public://". + * @param $replace + * Replace behavior when the destination file already exists: + * - FILE_EXISTS_REPLACE - Replace the existing file. + * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is + * unique. + * - FILE_EXISTS_ERROR - Do nothing and return FALSE. + * + * @return + * A string with the path of the resulting file, or FALSE on error. + * + * @see file_save_data() + */ +function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) { + // Write the data to a temporary file. + $temp_name = drupal_tempnam('temporary://', 'file'); + if (file_put_contents($temp_name, $data) === FALSE) { + drupal_set_message(t('The file could not be created.'), 'error'); + return FALSE; + } + + // Move the file to its final destination. + return file_unmanaged_move($temp_name, $destination, $replace); +} + +/** + * Transfer file using HTTP to client. + * + * Pipes a file through Drupal to the client. + * + * @param $uri + * String specifying the file URI to transfer. + * @param $headers + * An array of HTTP headers to send along with file. + */ +function file_transfer($uri, $headers) { + if (ob_get_level()) { + ob_end_clean(); + } + + foreach ($headers as $name => $value) { + drupal_add_http_header($name, $value); + } + drupal_send_headers(); + $scheme = file_uri_scheme($uri); + // Transfer file in 1024 byte chunks to save memory usage. + if ($scheme && file_stream_wrapper_valid_scheme($scheme) && $fd = fopen($uri, 'rb')) { + while (!feof($fd)) { + print fread($fd, 1024); + } + fclose($fd); + } + else { + drupal_not_found(); + } + drupal_exit(); +} + +/** + * Menu handler for private file transfers. + * + * Call modules that implement hook_file_download() to find out if a file is + * accessible and what headers it should be transferred with. If a module + * returns -1 drupal_access_denied() will be returned. If one or more modules + * returned headers the download will start with the returned headers. If no + * modules respond drupal_not_found() will be returned. + * + * @see hook_file_download() + */ +function file_download() { + // Merge remainder of arguments from GET['q'], into relative file path. + $args = func_get_args(); + $scheme = array_shift($args); + $target = implode('/', $args); + $uri = $scheme . '://' . $target; + if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) { + // Let other modules provide headers and controls access to the file. + // module_invoke_all() uses array_merge_recursive() which merges header + // values into a new array. To avoid that and allow modules to override + // headers instead, use array_merge() to merge the returned arrays. + $headers = array(); + foreach (module_implements('file_download') as $module) { + $function = $module . '_file_download'; + $result = $function($uri); + if ($result == -1) { + return drupal_access_denied(); + } + if (isset($result) && is_array($result)) { + $headers = array_merge($headers, $result); + } + } + if (count($headers)) { + file_transfer($uri, $headers); + } + } + return drupal_not_found(); +} + + +/** + * Finds all files that match a given mask in a given directory. + * + * Directories and files beginning with a period are excluded; this + * prevents hidden files and directories (such as SVN working directories) + * from being scanned. + * + * @param $dir + * The base directory or URI to scan, without trailing slash. + * @param $mask + * The preg_match() regular expression of the files to find. + * @param $options + * An associative array of additional options, with the following elements: + * - 'nomask': The preg_match() regular expression of the files to ignore. + * Defaults to '/(\.\.?|CVS)$/'. + * - 'callback': The callback function to call for each match. There is no + * default callback. + * - 'recurse': When TRUE, the directory scan will recurse the entire tree + * starting at the provided directory. Defaults to TRUE. + * - 'key': The key to be used for the returned associative array of files. + * Possible values are 'uri', for the file's URI; 'filename', for the + * basename of the file; and 'name' for the name of the file without the + * extension. Defaults to 'uri'. + * - 'min_depth': Minimum depth of directories to return files from. Defaults + * to 0. + * @param $depth + * Current depth of recursion. This parameter is only used internally and + * should not be passed in. + * + * @return + * An associative array (keyed on the chosen key) of objects with 'uri', + * 'filename', and 'name' members corresponding to the matching files. + */ +function file_scan_directory($dir, $mask, $options = array(), $depth = 0) { + // Merge in defaults. + $options += array( + 'nomask' => '/(\.\.?|CVS)$/', + 'callback' => 0, + 'recurse' => TRUE, + 'key' => 'uri', + 'min_depth' => 0, + ); + + $options['key'] = in_array($options['key'], array('uri', 'filename', 'name')) ? $options['key'] : 'uri'; + $files = array(); + if (is_dir($dir) && $handle = opendir($dir)) { + while (FALSE !== ($filename = readdir($handle))) { + if (!preg_match($options['nomask'], $filename) && $filename[0] != '.') { + $uri = "$dir/$filename"; + $uri = file_stream_wrapper_uri_normalize($uri); + if (is_dir($uri) && $options['recurse']) { + // Give priority to files in this folder by merging them in after any subdirectory files. + $files = array_merge(file_scan_directory($uri, $mask, $options, $depth + 1), $files); + } + elseif ($depth >= $options['min_depth'] && preg_match($mask, $filename)) { + // Always use this match over anything already set in $files with the + // same $$options['key']. + $file = new stdClass(); + $file->uri = $uri; + $file->filename = $filename; + $file->name = pathinfo($filename, PATHINFO_FILENAME); + $key = $options['key']; + $files[$file->$key] = $file; + if ($options['callback']) { + $options['callback']($uri); + } + } + } + } + + closedir($handle); + } + + return $files; +} + +/** + * Determine the maximum file upload size by querying the PHP settings. + * + * @return + * A file size limit in bytes based on the PHP upload_max_filesize and + * post_max_size + */ +function file_upload_max_size() { + static $max_size = -1; + + if ($max_size < 0) { + // Start with post_max_size. + $max_size = parse_size(ini_get('post_max_size')); + + // If upload_max_size is less, then reduce. Except if upload_max_size is + // zero, which indicates no limit. + $upload_max = parse_size(ini_get('upload_max_filesize')); + if ($upload_max > 0 && $upload_max < $max_size) { + $max_size = $upload_max; + } + } + return $max_size; +} + +/** + * Determine an Internet Media Type, or MIME type from a filename. + * + * @param $uri + * A string containing the URI, path, or filename. + * @param $mapping + * An optional map of extensions to their mimetypes, in the form: + * - 'mimetypes': a list of mimetypes, keyed by an identifier, + * - 'extensions': the mapping itself, an associative array in which + * the key is the extension (lowercase) and the value is the mimetype + * identifier. If $mapping is NULL file_mimetype_mapping() is called. + * + * @return + * The internet media type registered for the extension or + * application/octet-stream for unknown extensions. + * + * @see file_default_mimetype_mapping() + */ +function file_get_mimetype($uri, $mapping = NULL) { + if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) { + return $wrapper->getMimeType($uri, $mapping); + } + else { + // getMimeType() is not implementation specific, so we can directly + // call it without an instance. + return DrupalLocalStreamWrapper::getMimeType($uri, $mapping); + } +} + +/** + * Set the permissions on a file or directory. + * + * This function will use the 'file_chmod_directory' and 'file_chmod_file' + * variables for the default modes for directories and uploaded/generated + * files. By default these will give everyone read access so that users + * accessing the files with a user account without the webserver group (e.g. + * via FTP) can read these files, and give group write permissions so webserver + * group members (e.g. a vhost account) can alter files uploaded and owned by + * the webserver. + * + * PHP's chmod does not support stream wrappers so we use our wrapper + * implementation which interfaces with chmod() by default. Contrib wrappers + * may override this behavior in their implementations as needed. + * + * @param $uri + * A string containing a URI file, or directory path. + * @param $mode + * Integer value for the permissions. Consult PHP chmod() documentation for + * more information. + * + * @return + * TRUE for success, FALSE in the event of an error. + * + * @ingroup php_wrappers + */ +function drupal_chmod($uri, $mode = NULL) { + if (!isset($mode)) { + if (is_dir($uri)) { + $mode = variable_get('file_chmod_directory', 0775); + } + else { + $mode = variable_get('file_chmod_file', 0664); + } + } + + // If this URI is a stream, pass it off to the appropriate stream wrapper. + // Otherwise, attempt PHP's chmod. This allows use of drupal_chmod even + // for unmanaged files outside of the stream wrapper interface. + if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) { + if ($wrapper->chmod($mode)) { + return TRUE; + } + } + else { + if (@chmod($uri, $mode)) { + return TRUE; + } + } + + watchdog('file', 'The file permissions could not be set on %uri.', array('%uri' => $uri), LOG_ERR); + return FALSE; +} + +/** + * Deletes a file. + * + * PHP's unlink() is broken on Windows, as it can fail to remove a file + * when it has a read-only flag set. + * + * @param $uri + * A URI or pathname. + * @param $context + * Refer to http://php.net/manual/en/ref.stream.php + * + * @return + * Boolean TRUE on success, or FALSE on failure. + * + * @see unlink() + * @ingroup php_wrappers + */ +function drupal_unlink($uri, $context = NULL) { + $scheme = file_uri_scheme($uri); + if ((!$scheme || !file_stream_wrapper_valid_scheme($scheme)) && (substr(PHP_OS, 0, 3) == 'WIN')) { + chmod($uri, 0600); + } + if ($context) { + return unlink($uri, $context); + } + else { + return unlink($uri); + } +} + +/** + * Returns the absolute path of a file or directory + * + * PHP's realpath() does not properly support streams, so this function + * fills that gap. If a stream wrapped URI is provided, it will be passed + * to the registered wrapper for handling. If the URI does not contain a + * scheme or the wrapper implementation does not implement realpath, then + * FALSE will be returned. + * + * @see http://php.net/manual/en/function.realpath.php + * + * Compatibility: normal paths and stream wrappers. + * @see http://drupal.org/node/515192 + * + * @param $uri + * A string containing the URI to verify. If this value is omitted, + * Drupal's public files directory will be used [public://]. + * + * @return + * The absolute pathname, or FALSE on failure. + * + * @see realpath() + * @ingroup php_wrappers + */ +function drupal_realpath($uri) { + // If this URI is a stream, pass it off to the appropriate stream wrapper. + // Otherwise, attempt PHP's realpath. This allows use of drupal_realpath even + // for unmanaged files outside of the stream wrapper interface. + if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) { + return $wrapper->realpath(); + } + + return realpath($uri); +} + +/** + * Gets the name of the directory from a given path. + * + * PHP's dirname() does not properly pass streams, so this function fills + * that gap. It is backwards compatible with normal paths and will use + * PHP's dirname() as a fallback. + * + * Compatibility: normal paths and stream wrappers. + * @see http://drupal.org/node/515192 + * + * @param $uri + * A URI or path. + * + * @return + * A string containing the directory name. + * + * @see dirname() + * @ingroup php_wrappers + */ +function drupal_dirname($uri) { + $scheme = file_uri_scheme($uri); + + if ($scheme && file_stream_wrapper_valid_scheme($scheme)) { + return file_stream_wrapper_get_instance_by_scheme($scheme)->dirname($uri); + } + else { + return dirname($uri); + } +} + +/** + * Creates a directory using Drupal's default mode. + * + * PHP's mkdir() does not respect Drupal's default permissions mode. If a mode + * is not provided, this function will make sure that Drupal's is used. + * + * Compatibility: normal paths and stream wrappers. + * @see http://drupal.org/node/515192 + * + * @param $uri + * A URI or pathname. + * @param $mode + * By default the Drupal mode is used. + * @param $recursive + * Default to FALSE. + * @param $context + * Refer to http://php.net/manual/en/ref.stream.php + * + * @return + * Boolean TRUE on success, or FALSE on failure. + * + * @see mkdir() + * @ingroup php_wrappers + */ +function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) { + if (!isset($mode)) { + $mode = variable_get('file_chmod_directory', 0775); + } + + if (!isset($context)) { + return mkdir($uri, $mode, $recursive); + } + else { + return mkdir($uri, $mode, $recursive, $context); + } +} + +/** + * Remove a directory. + * + * PHP's rmdir() is broken on Windows, as it can fail to remove a directory + * when it has a read-only flag set. + * + * @param $uri + * A URI or pathname. + * @param $context + * Refer to http://php.net/manual/en/ref.stream.php + * + * @return + * Boolean TRUE on success, or FALSE on failure. + * + * @see rmdir() + * @ingroup php_wrappers + */ +function drupal_rmdir($uri, $context = NULL) { + $scheme = file_uri_scheme($uri); + if ((!$scheme || !file_stream_wrapper_valid_scheme($scheme)) && (substr(PHP_OS, 0, 3) == 'WIN')) { + chmod($uri, 0700); + } + if ($context) { + return rmdir($uri, $context); + } + else { + return rmdir($uri); + } +} + +/** + * Creates a file with a unique filename in the specified directory. + * + * PHP's tempnam() does not return a URI like we want. This function + * will return a URI if given a URI, or it will return a filepath if + * given a filepath. + * + * Compatibility: normal paths and stream wrappers. + * @see http://drupal.org/node/515192 + * + * @param $directory + * The directory where the temporary filename will be created. + * @param $prefix + * The prefix of the generated temporary filename. + * Note: Windows uses only the first three characters of prefix. + * + * @return + * The new temporary filename, or FALSE on failure. + * + * @see tempnam() + * @ingroup php_wrappers + */ +function drupal_tempnam($directory, $prefix) { + $scheme = file_uri_scheme($directory); + + if ($scheme && file_stream_wrapper_valid_scheme($scheme)) { + $wrapper = file_stream_wrapper_get_instance_by_scheme($scheme); + + if ($filename = tempnam($wrapper->getDirectoryPath(), $prefix)) { + return $scheme . '://' . basename($filename); + } + else { + return FALSE; + } + } + else { + // Handle as a normal tempnam() call. + return tempnam($directory, $prefix); + } +} + +/** + * Get the path of system-appropriate temporary directory. + */ +function file_directory_temp() { + $temporary_directory = variable_get('file_temporary_path', NULL); + + if (empty($temporary_directory)) { + $directories = array(); + + // Has PHP been set with an upload_tmp_dir? + if (ini_get('upload_tmp_dir')) { + $directories[] = ini_get('upload_tmp_dir'); + } + + // Operating system specific dirs. + if (substr(PHP_OS, 0, 3) == 'WIN') { + $directories[] = 'c:\\windows\\temp'; + $directories[] = 'c:\\winnt\\temp'; + $path_delimiter = '\\'; + } + else { + $directories[] = '/tmp'; + $path_delimiter = '/'; + } + // PHP may be able to find an alternative tmp directory. + $directories[] = sys_get_temp_dir(); + + foreach ($directories as $directory) { + if (is_dir($directory) && is_writable($directory)) { + $temporary_directory = $directory; + break; + } + } + + if (empty($temporary_directory)) { + // If no directory has been found default to 'files/tmp' or 'files\\tmp'. + $temporary_directory = variable_get('file_public_path', conf_path() . '/files') . $path_delimiter . 'tmp'; + } + // Save the path of the discovered directory. + variable_set('file_temporary_path', $temporary_directory); + } + + return $temporary_directory; +} + +/** + * Examines a file object and returns appropriate content headers for download. + * + * @param $file + * A file object. + * @return + * An associative array of headers, as expected by file_transfer(). + */ +function file_get_content_headers($file) { + $name = mime_header_encode($file->filename); + $type = mime_header_encode($file->filemime); + // Serve images, text, and flash content for display rather than download. + $inline_types = variable_get('file_inline_types', array('^text/', '^image/', 'flash$')); + $disposition = 'attachment'; + foreach ($inline_types as $inline_type) { + // Exclamation marks are used as delimiters to avoid escaping slashes. + if (preg_match('!' . $inline_type . '!', $file->filemime)) { + $disposition = 'inline'; + } + } + + return array( + 'Content-Type' => $type . '; name="' . $name . '"', + 'Content-Length' => $file->filesize, + 'Content-Disposition' => $disposition . '; filename="' . $name . '"', + 'Cache-Control' => 'private', + ); +} + +/** + * @} End of "defgroup file". + */ diff --git a/includes/file.mimetypes.inc b/core/includes/file.mimetypes.inc similarity index 100% rename from includes/file.mimetypes.inc rename to core/includes/file.mimetypes.inc diff --git a/includes/filetransfer/filetransfer.inc b/core/includes/filetransfer/filetransfer.inc similarity index 100% rename from includes/filetransfer/filetransfer.inc rename to core/includes/filetransfer/filetransfer.inc diff --git a/includes/filetransfer/ftp.inc b/core/includes/filetransfer/ftp.inc similarity index 100% rename from includes/filetransfer/ftp.inc rename to core/includes/filetransfer/ftp.inc diff --git a/includes/filetransfer/local.inc b/core/includes/filetransfer/local.inc similarity index 100% rename from includes/filetransfer/local.inc rename to core/includes/filetransfer/local.inc diff --git a/includes/filetransfer/ssh.inc b/core/includes/filetransfer/ssh.inc similarity index 100% rename from includes/filetransfer/ssh.inc rename to core/includes/filetransfer/ssh.inc diff --git a/core/includes/form.inc b/core/includes/form.inc new file mode 100644 index 0000000..1f45e69 --- /dev/null +++ b/core/includes/form.inc @@ -0,0 +1,4374 @@ + 'submit', + * '#value' => t('Submit'), + * ); + * return $form; + * } + * function my_module_example_form_validate($form, &$form_state) { + * // Validation logic. + * } + * function my_module_example_form_submit($form, &$form_state) { + * // Submission logic. + * } + * @endcode + * + * Or with any number of additional arguments: + * @code + * $extra = "extra"; + * $form = drupal_get_form('my_module_example_form', $extra); + * ... + * function my_module_example_form($form, &$form_state, $extra) { + * $form['submit'] = array( + * '#type' => 'submit', + * '#value' => $extra, + * ); + * return $form; + * } + * @endcode + * + * The $form argument to form-related functions is a structured array containing + * the elements and properties of the form. For information on the array + * components and format, and more detailed explanations of the Form API + * workflow, see the + * @link http://api.drupal.org/api/drupal/developer--topics--forms_api_reference.html Form API reference @endlink + * and the + * @link http://drupal.org/node/37775 Form API section of the handbook. @endlink + * In addition, there is a set of Form API tutorials in + * @link form_example_tutorial.inc the Form Example Tutorial @endlink which + * provide basics all the way up through multistep forms. + * + * In the form builder, validation, submission, and other form functions, + * $form_state is the primary influence on the processing of the form and is + * passed by reference to most functions, so they use it to communicate with + * the form system and each other. + * + * The $form_state keys are: + * - build_info: Do not change; internal information stored by Form API to be + * able to build and rebuild the form: + * - args: A list of arguments used to rebuild the form from cache. + * - files: A list of include files to be loaded to rebuild the form. See + * form_load_include(). + * - 'values': An associative array of values submitted to the form. The + * validation functions and submit functions use this array for nearly all + * their decision making. (Note that + * @link http://api.drupal.org/api/drupal/developer--topics--forms_api_reference.html/7#tree #tree @endlink + * determines whether the values are a flat array or an array whose structure + * parallels the $form array.) + * - 'rebuild': If the submit function sets $form_state['rebuild'] to TRUE, + * submission is not completed and instead the form is rebuilt using any + * information that the submit function has made available to the form builder + * function via $form_state. This is commonly used for wizard-style + * multi-step forms, add-more buttons, and the like. For further information + * see drupal_build_form(). + * - 'redirect': a URL that will be used to redirect the form on submission. + * See drupal_redirect_form() for complete information. + * - 'storage': $form_state['storage'] is not a special key, and no specific + * support is provided for it in the Form API, but by tradition it was + * the location where application-specific data was stored for communication + * between the submit, validation, and form builder functions, especially + * in a multi-step-style form. Form implementations may use any key(s) within + * $form_state (other than the keys listed here and other reserved ones used + * by Form API internals) for this kind of storage. The recommended way to + * ensure that the chosen key doesn't conflict with ones used by the Form API + * or other modules is to use the module name as the key name or a prefix for + * the key name. For example, the Node module uses $form_state['node'] in node + * editing forms to store information about the node being edited, and this + * information stays available across successive clicks of the "Preview" + * button as well as when the "Save" button is finally clicked. + * - 'temporary': Since values for all non-reserved keys in $form_state persist + * throughout a multistep form sequence, the Form API provides the 'temporary' + * key for modules to use for communicating information across form-related + * functions during a single page request only. There is no use-case for this + * functionality in core. + * - 'triggering_element': (read-only) The form element that triggered + * submission. This is the same as the deprecated + * $form_state['clicked_button']. It is the element that caused submission, + * which may or may not be a button (in the case of Ajax forms.) This is + * often used to distinguish between various buttons in a submit handler, + * and is also used in Ajax handlers. + * - 'cache': The typical form workflow involves two page requests. During the + * first page request, a form is built and returned for the user to fill in. + * Then the user fills the form in and submits it, triggering a second page + * request in which the form must be built and processed. By default, $form + * and $form_state are built from scratch during each of these page requests. + * In some special use-cases, it is necessary or desired to persist the $form + * and $form_state variables from the initial page request to the one that + * processes the submission. A form builder function can set 'cache' to TRUE + * to do this. One example where this is needed is to handle Ajax submissions, + * so ajax_process_form() sets this for all forms that include an element with + * a #ajax property. (In Ajax, the handler has no way to build the form + * itself, so must rely on the cached version created on each page load, so + * it's a classic example of this use case.) Note that the persistence of + * $form and $form_state across successive submissions of a multi-step form + * happens automatically regardless of the value for 'cache'. + * - 'input': The array of values as they were submitted by the user. These are + * raw and unvalidated, so should not be used without a thorough understanding + * of security implications. In almost all cases, code should use the data in + * the 'values' array exclusively. The most common use of this key is for + * multi-step forms that need to clear some of the user input when setting + * 'rebuild'. + */ + +/** + * Wrapper for drupal_build_form() for use when $form_state is not needed. + * + * @param $form_id + * The unique string identifying the desired form. If a function with that + * name exists, it is called to build the form array. Modules that need to + * generate the same form (or very similar forms) using different $form_ids + * can implement hook_forms(), which maps different $form_id values to the + * proper form constructor function. Examples may be found in node_forms(), + * search_forms(), and user_forms(). + * @param ... + * Any additional arguments are passed on to the functions called by + * drupal_get_form(), including the unique form constructor function. For + * example, the node_edit form requires that a node object is passed in here + * when it is called. These are available to implementations of + * hook_form_alter() and hook_form_FORM_ID_alter() as the array + * $form_state['build_info']['args']. + * + * @return + * The form array. + * + * @see drupal_build_form() + */ +function drupal_get_form($form_id) { + $form_state = array(); + + $args = func_get_args(); + // Remove $form_id from the arguments. + array_shift($args); + $form_state['build_info']['args'] = $args; + + return drupal_build_form($form_id, $form_state); +} + +/** + * Build and process a form based on a form id. + * + * The form may also be retrieved from the cache if the form was built in a + * previous page-load. The form is then passed on for processing, validation + * and submission if there is proper input. + * + * @param $form_id + * The unique string identifying the desired form. If a function with that + * name exists, it is called to build the form array. Modules that need to + * generate the same form (or very similar forms) using different $form_ids + * can implement hook_forms(), which maps different $form_id values to the + * proper form constructor function. Examples may be found in node_forms(), + * search_forms(), and user_forms(). + * @param $form_state + * An array which stores information about the form. This is passed as a + * reference so that the caller can use it to examine what in the form changed + * when the form submission process is complete. Furthermore, it may be used + * to store information related to the processed data in the form, which will + * persist across page requests when the 'cache' or 'rebuild' flag is set. + * The following parameters may be set in $form_state to affect how the form + * is rendered: + * - build_info: A keyed array of build information that is necessary to + * rebuild the form from cache when the original context may no longer be + * available: + * - args: An array of arguments to pass to the form builder. + * - files: An optional array defining include files that need to be loaded + * for building the form. Each array entry may be the path to a file or + * another array containing values for the parameters 'type', 'module' and + * 'name' as needed by module_load_include(). The files listed here are + * automatically loaded by form_get_cache(). By default the current menu + * router item's 'file' definition is added, if existent. + * - rebuild: Normally, after the entire form processing is completed and + * submit handlers ran, a form is considered to be done and + * drupal_redirect_form() will redirect the user to a new page using a GET + * request (so a browser refresh does not re-submit the form). However, if + * 'rebuild' has been set to TRUE, then a new copy of the form is + * immediately built and sent to the browser; instead of a redirect. This is + * used for multi-step forms, such as wizards and confirmation forms. + * Normally, $form_state['rebuild'] is set by a submit handler, since it is + * usually logic within a submit handler that determines whether a form is + * done or requires another step. However, a validation handler may already + * set $form_state['rebuild'] to cause the form processing to bypass submit + * handlers and rebuild the form instead, even if there are no validation + * errors. + * - input: An array of input that corresponds to $_POST or $_GET, depending + * on the 'method' chosen (see below). + * - method: The HTTP form method to use for finding the input for this form. + * May be 'post' or 'get'. Defaults to 'post'. Note that 'get' method + * forms do not use form ids so are always considered to be submitted, which + * can have unexpected effects. The 'get' method should only be used on + * forms that do not change data, as that is exclusively the domain of post. + * - no_redirect: If set to TRUE the form will NOT perform a drupal_goto(), + * even if 'redirect' is set. + * - cache: If set to TRUE the original, unprocessed form structure will be + * cached, which allows to rebuild the entire form from cache. + * - no_cache: If set to TRUE the form will NOT be cached, even if 'cache' is + * set. + * - always_process: If TRUE and the method is GET, a form_id is not + * necessary. This should only be used on RESTful GET forms that do NOT + * write data, as this could lead to security issues. It is useful so that + * searches do not need to have a form_id in their query arguments to + * trigger the search. + * - must_validate: Ordinarily, a form is only validated once but there are + * times when a form is resubmitted internally and should be validated + * again. Setting this to TRUE will force that to happen. This is most + * likely to occur during AHAH or Ajax operations. + * - temporary: An array holding temporary data accessible during the current + * page request only. It may be used to temporary save any data that doesn't + * need to or shouldn't be cached during the whole form workflow, e.g. data + * that needs to be accessed during the current form build process only. + * - wrapper_callback: Modules that wish to pre-populate certain forms with + * common elements, such as back/next/save buttons in multi-step form + * wizards, may define a form builder function name that returns a form + * structure, which is passed on to the actual form builder function. + * Such implementations may either define the 'wrapper_callback' via + * hook_forms() or have to invoke drupal_build_form() (instead of + * drupal_get_form()) on their own in a custom menu callback to prepare + * $form_state accordingly. + * Further $form_state properties controlling the redirection behavior after + * form submission may be found in drupal_redirect_form(). + * + * @return + * The rendered form or NULL, depending upon the $form_state flags that were set. + * + * @see drupal_redirect_form() + */ +function drupal_build_form($form_id, &$form_state) { + // Ensure some defaults; if already set they will not be overridden. + $form_state += form_state_defaults(); + + if (!isset($form_state['input'])) { + $form_state['input'] = $form_state['method'] == 'get' ? $_GET : $_POST; + } + + if (isset($_SESSION['batch_form_state'])) { + // We've been redirected here after a batch processing. The form has + // already been processed, but needs to be rebuilt. See _batch_finished(). + $form_state = $_SESSION['batch_form_state']; + unset($_SESSION['batch_form_state']); + return drupal_rebuild_form($form_id, $form_state); + } + + // If the incoming input contains a form_build_id, we'll check the cache for a + // copy of the form in question. If it's there, we don't have to rebuild the + // form to proceed. In addition, if there is stored form_state data from a + // previous step, we'll retrieve it so it can be passed on to the form + // processing code. + $check_cache = isset($form_state['input']['form_id']) && $form_state['input']['form_id'] == $form_id && !empty($form_state['input']['form_build_id']); + if ($check_cache) { + $form = form_get_cache($form_state['input']['form_build_id'], $form_state); + } + + // If the previous bit of code didn't result in a populated $form object, we + // are hitting the form for the first time and we need to build it from + // scratch. + if (!isset($form)) { + // If we attempted to serve the form from cache, uncacheable $form_state + // keys need to be removed after retrieving and preparing the form, except + // any that were already set prior to retrieving the form. + if ($check_cache) { + $form_state_before_retrieval = $form_state; + } + + $form = drupal_retrieve_form($form_id, $form_state); + drupal_prepare_form($form_id, $form, $form_state); + + // form_set_cache() removes uncacheable $form_state keys defined in + // form_state_keys_no_cache() in order for multi-step forms to work + // properly. This means that form processing logic for single-step forms + // using $form_state['cache'] may depend on data stored in those keys + // during drupal_retrieve_form()/drupal_prepare_form(), but form + // processing should not depend on whether the form is cached or not, so + // $form_state is adjusted to match what it would be after a + // form_set_cache()/form_get_cache() sequence. These exceptions are + // allowed to survive here: + // - always_process: Does not make sense in conjunction with form caching + // in the first place, since passing form_build_id as a GET parameter is + // not desired. + // - temporary: Any assigned data is expected to survives within the same + // page request. + if ($check_cache) { + $uncacheable_keys = array_flip(array_diff(form_state_keys_no_cache(), array('always_process', 'temporary'))); + $form_state = array_diff_key($form_state, $uncacheable_keys); + $form_state += $form_state_before_retrieval; + } + } + + // Now that we have a constructed form, process it. This is where: + // - Element #process functions get called to further refine $form. + // - User input, if any, gets incorporated in the #value property of the + // corresponding elements and into $form_state['values']. + // - Validation and submission handlers are called. + // - If this submission is part of a multistep workflow, the form is rebuilt + // to contain the information of the next step. + // - If necessary, the form and form state are cached or re-cached, so that + // appropriate information persists to the next page request. + // All of the handlers in the pipeline receive $form_state by reference and + // can use it to know or update information about the state of the form. + drupal_process_form($form_id, $form, $form_state); + + // If this was a successful submission of a single-step form or the last step + // of a multi-step form, then drupal_process_form() issued a redirect to + // another page, or back to this page, but as a new request. Therefore, if + // we're here, it means that this is either a form being viewed initially + // before any user input, or there was a validation error requiring the form + // to be re-displayed, or we're in a multi-step workflow and need to display + // the form's next step. In any case, we have what we need in $form, and can + // return it for rendering. + return $form; +} + +/** + * Retrieve default values for the $form_state array. + */ +function form_state_defaults() { + return array( + 'rebuild' => FALSE, + 'rebuild_info' => array(), + 'redirect' => NULL, + // @todo 'args' is usually set, so no other default 'build_info' keys are + // appended via += form_state_defaults(). + 'build_info' => array( + 'args' => array(), + 'files' => array(), + ), + 'temporary' => array(), + 'submitted' => FALSE, + 'executed' => FALSE, + 'programmed' => FALSE, + 'cache'=> FALSE, + 'method' => 'post', + 'groups' => array(), + 'buttons' => array(), + ); +} + +/** + * Constructs a new $form from the information in $form_state. + * + * This is the key function for making multi-step forms advance from step to + * step. It is called by drupal_process_form() when all user input processing, + * including calling validation and submission handlers, for the request is + * finished. If a validate or submit handler set $form_state['rebuild'] to TRUE, + * and if other conditions don't preempt a rebuild from happening, then this + * function is called to generate a new $form, the next step in the form + * workflow, to be returned for rendering. + * + * Ajax form submissions are almost always multi-step workflows, so that is one + * common use-case during which form rebuilding occurs. See ajax_form_callback() + * for more information about creating Ajax-enabled forms. + * + * @param $form_id + * The unique string identifying the desired form. If a function + * with that name exists, it is called to build the form array. + * Modules that need to generate the same form (or very similar forms) + * using different $form_ids can implement hook_forms(), which maps + * different $form_id values to the proper form constructor function. Examples + * may be found in node_forms(), search_forms(), and user_forms(). + * @param $form_state + * A keyed array containing the current state of the form. + * @param $old_form + * (optional) A previously built $form. Used to retain the #build_id and + * #action properties in Ajax callbacks and similar partial form rebuilds. The + * only properties copied from $old_form are the ones which both exist in + * $old_form and for which $form_state['rebuild_info']['copy'][PROPERTY] is + * TRUE. If $old_form is not passed, the entire $form is rebuilt freshly. + * 'rebuild_info' needs to be a separate top-level property next to + * 'build_info', since the contained data must not be cached. + * + * @return + * The newly built form. + * + * @see drupal_process_form() + * @see ajax_form_callback() + */ +function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) { + $form = drupal_retrieve_form($form_id, $form_state); + + // If only parts of the form will be returned to the browser (e.g., Ajax or + // RIA clients), re-use the old #build_id to not require client-side code to + // manually update the hidden 'build_id' input element. + // Otherwise, a new #build_id is generated, to not clobber the previous + // build's data in the form cache; also allowing the user to go back to an + // earlier build, make changes, and re-submit. + // @see drupal_prepare_form() + if (isset($old_form['#build_id']) && !empty($form_state['rebuild_info']['copy']['#build_id'])) { + $form['#build_id'] = $old_form['#build_id']; + } + else { + $form['#build_id'] = 'form-' . drupal_hash_base64(uniqid(mt_rand(), TRUE) . mt_rand()); + } + + // #action defaults to request_uri(), but in case of Ajax and other partial + // rebuilds, the form is submitted to an alternate URL, and the original + // #action needs to be retained. + if (isset($old_form['#action']) && !empty($form_state['rebuild_info']['copy']['#action'])) { + $form['#action'] = $old_form['#action']; + } + + drupal_prepare_form($form_id, $form, $form_state); + + // Caching is normally done in drupal_process_form(), but what needs to be + // cached is the $form structure before it passes through form_builder(), + // so we need to do it here. + // @todo For Drupal 8, find a way to avoid this code duplication. + if (empty($form_state['no_cache'])) { + form_set_cache($form['#build_id'], $form, $form_state); + } + + // Clear out all group associations as these might be different when + // re-rendering the form. + $form_state['groups'] = array(); + + // Return a fully built form that is ready for rendering. + return form_builder($form_id, $form, $form_state); +} + +/** + * Fetch a form from cache. + */ +function form_get_cache($form_build_id, &$form_state) { + if ($cached = cache_get('form_' . $form_build_id, 'cache_form')) { + $form = $cached->data; + + global $user; + if ((isset($form['#cache_token']) && drupal_valid_token($form['#cache_token'])) || (!isset($form['#cache_token']) && !$user->uid)) { + if ($cached = cache_get('form_state_' . $form_build_id, 'cache_form')) { + // Re-populate $form_state for subsequent rebuilds. + $form_state = $cached->data + $form_state; + + // If the original form is contained in include files, load the files. + // @see form_load_include() + $form_state['build_info'] += array('files' => array()); + foreach ($form_state['build_info']['files'] as $file) { + if (is_array($file)) { + $file += array('type' => 'inc', 'name' => $file['module']); + module_load_include($file['type'], $file['module'], $file['name']); + } + elseif (file_exists($file)) { + require_once DRUPAL_ROOT . '/' . $file; + } + } + } + return $form; + } + } +} + +/** + * Store a form in the cache. + */ +function form_set_cache($form_build_id, $form, $form_state) { + // 6 hours cache life time for forms should be plenty. + $expire = 21600; + + // Cache form structure. + if (isset($form)) { + if ($GLOBALS['user']->uid) { + $form['#cache_token'] = drupal_get_token(); + } + cache_set('form_' . $form_build_id, $form, 'cache_form', REQUEST_TIME + $expire); + } + + // Cache form state. + if ($data = array_diff_key($form_state, array_flip(form_state_keys_no_cache()))) { + cache_set('form_state_' . $form_build_id, $data, 'cache_form', REQUEST_TIME + $expire); + } +} + +/** + * Returns an array of $form_state keys that shouldn't be cached. + */ +function form_state_keys_no_cache() { + return array( + // Public properties defined by form constructors and form handlers. + 'always_process', + 'must_validate', + 'rebuild', + 'rebuild_info', + 'redirect', + 'no_redirect', + 'temporary', + // Internal properties defined by form processing. + 'buttons', + 'triggering_element', + 'clicked_button', + 'complete form', + 'groups', + 'input', + 'method', + 'submit_handlers', + 'submitted', + 'executed', + 'validate_handlers', + 'values', + ); +} + +/** + * Loads an include file and makes sure it is loaded whenever the form is processed. + * + * Example: + * @code + * // Load node.admin.inc from Node module. + * form_load_include($form_state, 'inc', 'node', 'node.admin'); + * @endcode + * + * Use this function instead of module_load_include() from inside a form + * constructor or any form processing logic as it ensures that the include file + * is loaded whenever the form is processed. In contrast to using + * module_load_include() directly, form_load_include() makes sure the include + * file is correctly loaded also if the form is cached. + * + * @param $form_state + * The current state of the form. + * @param $type + * The include file's type (file extension). + * @param $module + * The module to which the include file belongs. + * @param $name + * (optional) The base file name (without the $type extension). If omitted, + * $module is used; i.e., resulting in "$module.$type" by default. + * + * @return + * The filepath of the loaded include file, or FALSE if the include file was + * not found or has been loaded already. + * + * @see module_load_include() + */ +function form_load_include(&$form_state, $type, $module, $name = NULL) { + if (!isset($name)) { + $name = $module; + } + if (!isset($form_state['build_info']['files']["$module:$name.$type"])) { + // Only add successfully included files to the form state. + if ($result = module_load_include($type, $module, $name)) { + $form_state['build_info']['files']["$module:$name.$type"] = array( + 'type' => $type, + 'module' => $module, + 'name' => $name, + ); + return $result; + } + } + return FALSE; +} + +/** + * Retrieves, populates, and processes a form. + * + * This function allows you to supply values for form elements and submit a + * form for processing. Compare to drupal_get_form(), which also builds and + * processes a form, but does not allow you to supply values. + * + * There is no return value, but you can check to see if there are errors + * by calling form_get_errors(). + * + * @param $form_id + * The unique string identifying the desired form. If a function + * with that name exists, it is called to build the form array. + * Modules that need to generate the same form (or very similar forms) + * using different $form_ids can implement hook_forms(), which maps + * different $form_id values to the proper form constructor function. Examples + * may be found in node_forms(), search_forms(), and user_forms(). + * @param $form_state + * A keyed array containing the current state of the form. Most important is + * the $form_state['values'] collection, a tree of data used to simulate the + * incoming $_POST information from a user's form submission. If a key is not + * filled in $form_state['values'], then the default value of the respective + * element is used. To submit an unchecked checkbox or other control that + * browsers submit by not having a $_POST entry, include the key, but set the + * value to NULL. + * @param ... + * Any additional arguments are passed on to the functions called by + * drupal_form_submit(), including the unique form constructor function. + * For example, the node_edit form requires that a node object be passed + * in here when it is called. Arguments that need to be passed by reference + * should not be included here, but rather placed directly in the $form_state + * build info array so that the reference can be preserved. For example, a + * form builder function with the following signature: + * @code + * function mymodule_form($form, &$form_state, &$object) { + * } + * @endcode + * would be called via drupal_form_submit() as follows: + * @code + * $form_state['values'] = $my_form_values; + * $form_state['build_info']['args'] = array(&$object); + * drupal_form_submit('mymodule_form', $form_state); + * @endcode + * For example: + * @code + * // register a new user + * $form_state = array(); + * $form_state['values']['name'] = 'robo-user'; + * $form_state['values']['mail'] = 'robouser@example.com'; + * $form_state['values']['pass']['pass1'] = 'password'; + * $form_state['values']['pass']['pass2'] = 'password'; + * $form_state['values']['op'] = t('Create new account'); + * drupal_form_submit('user_register_form', $form_state); + * @endcode + */ +function drupal_form_submit($form_id, &$form_state) { + if (!isset($form_state['build_info']['args'])) { + $args = func_get_args(); + array_shift($args); + array_shift($args); + $form_state['build_info']['args'] = $args; + } + // Merge in default values. + $form_state += form_state_defaults(); + + // Populate $form_state['input'] with the submitted values before retrieving + // the form, to be consistent with what drupal_build_form() does for + // non-programmatic submissions (form builder functions may expect it to be + // there). + $form_state['input'] = $form_state['values']; + + $form_state['programmed'] = TRUE; + $form = drupal_retrieve_form($form_id, $form_state); + // Programmed forms are always submitted. + $form_state['submitted'] = TRUE; + + // Reset form validation. + $form_state['must_validate'] = TRUE; + form_clear_error(); + + drupal_prepare_form($form_id, $form, $form_state); + drupal_process_form($form_id, $form, $form_state); +} + +/** + * Retrieves the structured array that defines a given form. + * + * @param $form_id + * The unique string identifying the desired form. If a function + * with that name exists, it is called to build the form array. + * Modules that need to generate the same form (or very similar forms) + * using different $form_ids can implement hook_forms(), which maps + * different $form_id values to the proper form constructor function. + * @param $form_state + * A keyed array containing the current state of the form, including the + * additional arguments to drupal_get_form() or drupal_form_submit() in the + * 'args' component of the array. + */ +function drupal_retrieve_form($form_id, &$form_state) { + $forms = &drupal_static(__FUNCTION__); + + // Record the filepath of the include file containing the original form, so + // the form builder callbacks can be loaded when the form is being rebuilt + // from cache on a different path (such as 'system/ajax'). See + // form_get_cache(). + // $menu_get_item() is not available at installation time. + if (!isset($form_state['build_info']['files']['menu']) && !defined('MAINTENANCE_MODE')) { + $item = menu_get_item(); + if (!empty($item['include_file'])) { + // Do not use form_load_include() here, as the file is already loaded. + // Anyway, form_get_cache() is able to handle filepaths too. + $form_state['build_info']['files']['menu'] = $item['include_file']; + } + } + + // We save two copies of the incoming arguments: one for modules to use + // when mapping form ids to constructor functions, and another to pass to + // the constructor function itself. + $args = $form_state['build_info']['args']; + + // We first check to see if there's a function named after the $form_id. + // If there is, we simply pass the arguments on to it to get the form. + if (!function_exists($form_id)) { + // In cases where many form_ids need to share a central constructor function, + // such as the node editing form, modules can implement hook_forms(). It + // maps one or more form_ids to the correct constructor functions. + // + // We cache the results of that hook to save time, but that only works + // for modules that know all their form_ids in advance. (A module that + // adds a small 'rate this comment' form to each comment in a list + // would need a unique form_id for each one, for example.) + // + // So, we call the hook if $forms isn't yet populated, OR if it doesn't + // yet have an entry for the requested form_id. + if (!isset($forms) || !isset($forms[$form_id])) { + $forms = module_invoke_all('forms', $form_id, $args); + } + $form_definition = $forms[$form_id]; + if (isset($form_definition['callback arguments'])) { + $args = array_merge($form_definition['callback arguments'], $args); + } + if (isset($form_definition['callback'])) { + $callback = $form_definition['callback']; + $form_state['build_info']['base_form_id'] = $callback; + } + // In case $form_state['wrapper_callback'] is not defined already, we also + // allow hook_forms() to define one. + if (!isset($form_state['wrapper_callback']) && isset($form_definition['wrapper_callback'])) { + $form_state['wrapper_callback'] = $form_definition['wrapper_callback']; + } + } + + $form = array(); + // We need to pass $form_state by reference in order for forms to modify it, + // since call_user_func_array() requires that referenced variables are passed + // explicitly. + $args = array_merge(array($form, &$form_state), $args); + + // When the passed $form_state (not using drupal_get_form()) defines a + // 'wrapper_callback', then it requests to invoke a separate (wrapping) form + // builder function to pre-populate the $form array with form elements, which + // the actual form builder function ($callback) expects. This allows for + // pre-populating a form with common elements for certain forms, such as + // back/next/save buttons in multi-step form wizards. See drupal_build_form(). + if (isset($form_state['wrapper_callback']) && function_exists($form_state['wrapper_callback'])) { + $form = call_user_func_array($form_state['wrapper_callback'], $args); + // Put the prepopulated $form into $args. + $args[0] = $form; + } + + // If $callback was returned by a hook_forms() implementation, call it. + // Otherwise, call the function named after the form id. + $form = call_user_func_array(isset($callback) ? $callback : $form_id, $args); + $form['#form_id'] = $form_id; + + return $form; +} + +/** + * Processes a form submission. + * + * This function is the heart of form API. The form gets built, validated and in + * appropriate cases, submitted and rebuilt. + * + * @param $form_id + * The unique string identifying the current form. + * @param $form + * An associative array containing the structure of the form. + * @param $form_state + * A keyed array containing the current state of the form. This + * includes the current persistent storage data for the form, and + * any data passed along by earlier steps when displaying a + * multi-step form. Additional information, like the sanitized $_POST + * data, is also accumulated here. + */ +function drupal_process_form($form_id, &$form, &$form_state) { + $form_state['values'] = array(); + + // With $_GET, these forms are always submitted if requested. + if ($form_state['method'] == 'get' && !empty($form_state['always_process'])) { + if (!isset($form_state['input']['form_build_id'])) { + $form_state['input']['form_build_id'] = $form['#build_id']; + } + if (!isset($form_state['input']['form_id'])) { + $form_state['input']['form_id'] = $form_id; + } + if (!isset($form_state['input']['form_token']) && isset($form['#token'])) { + $form_state['input']['form_token'] = drupal_get_token($form['#token']); + } + } + + // form_builder() finishes building the form by calling element #process + // functions and mapping user input, if any, to #value properties, and also + // storing the values in $form_state['values']. We need to retain the + // unprocessed $form in case it needs to be cached. + $unprocessed_form = $form; + $form = form_builder($form_id, $form, $form_state); + + // Only process the input if we have a correct form submission. + if ($form_state['process_input']) { + drupal_validate_form($form_id, $form, $form_state); + + // drupal_html_id() maintains a cache of element IDs it has seen, + // so it can prevent duplicates. We want to be sure we reset that + // cache when a form is processed, so scenarios that result in + // the form being built behind the scenes and again for the + // browser don't increment all the element IDs needlessly. + drupal_static_reset('drupal_html_id'); + + if ($form_state['submitted'] && !form_get_errors() && !$form_state['rebuild']) { + // Execute form submit handlers. + form_execute_handlers('submit', $form, $form_state); + + // We'll clear out the cached copies of the form and its stored data + // here, as we've finished with them. The in-memory copies are still + // here, though. + if (!variable_get('cache', 0) && !empty($form_state['values']['form_build_id'])) { + cache_clear_all('form_' . $form_state['values']['form_build_id'], 'cache_form'); + cache_clear_all('form_state_' . $form_state['values']['form_build_id'], 'cache_form'); + } + + // If batches were set in the submit handlers, we process them now, + // possibly ending execution. We make sure we do not react to the batch + // that is already being processed (if a batch operation performs a + // drupal_form_submit). + if ($batch =& batch_get() && !isset($batch['current_set'])) { + // Store $form_state information in the batch definition. + // We need the full $form_state when either: + // - Some submit handlers were saved to be called during batch + // processing. See form_execute_handlers(). + // - The form is multistep. + // In other cases, we only need the information expected by + // drupal_redirect_form(). + if ($batch['has_form_submits'] || !empty($form_state['rebuild'])) { + $batch['form_state'] = $form_state; + } + else { + $batch['form_state'] = array_intersect_key($form_state, array_flip(array('programmed', 'rebuild', 'storage', 'no_redirect', 'redirect'))); + } + + $batch['progressive'] = !$form_state['programmed']; + batch_process(); + + // Execution continues only for programmatic forms. + // For 'regular' forms, we get redirected to the batch processing + // page. Form redirection will be handled in _batch_finished(), + // after the batch is processed. + } + + // Set a flag to indicate the the form has been processed and executed. + $form_state['executed'] = TRUE; + + // Redirect the form based on values in $form_state. + drupal_redirect_form($form_state); + } + + // Don't rebuild or cache form submissions invoked via drupal_form_submit(). + if (!empty($form_state['programmed'])) { + return; + } + + // If $form_state['rebuild'] has been set and input has been processed + // without validation errors, we are in a multi-step workflow that is not + // yet complete. A new $form needs to be constructed based on the changes + // made to $form_state during this request. Normally, a submit handler sets + // $form_state['rebuild'] if a fully executed form requires another step. + // However, for forms that have not been fully executed (e.g., Ajax + // submissions triggered by non-buttons), there is no submit handler to set + // $form_state['rebuild']. It would not make sense to redisplay the + // identical form without an error for the user to correct, so we also + // rebuild error-free non-executed forms, regardless of + // $form_state['rebuild']. + // @todo D8: Simplify this logic; considering Ajax and non-HTML front-ends, + // along with element-level #submit properties, it makes no sense to have + // divergent form execution based on whether the triggering element has + // #executes_submit_callback set to TRUE. + if (($form_state['rebuild'] || !$form_state['executed']) && !form_get_errors()) { + // Form building functions (e.g., _form_builder_handle_input_element()) + // may use $form_state['rebuild'] to determine if they are running in the + // context of a rebuild, so ensure it is set. + $form_state['rebuild'] = TRUE; + $form = drupal_rebuild_form($form_id, $form_state, $form); + } + } + + // After processing the form, the form builder or a #process callback may + // have set $form_state['cache'] to indicate that the form and form state + // shall be cached. But the form may only be cached if the 'no_cache' property + // is not set to TRUE. Only cache $form as it was prior to form_builder(), + // because form_builder() must run for each request to accomodate new user + // input. Rebuilt forms are not cached here, because drupal_rebuild_form() + // already takes care of that. + if (!$form_state['rebuild'] && $form_state['cache'] && empty($form_state['no_cache'])) { + form_set_cache($form['#build_id'], $unprocessed_form, $form_state); + } +} + +/** + * Prepares a structured form array by adding required elements, + * executing any hook_form_alter functions, and optionally inserting + * a validation token to prevent tampering. + * + * @param $form_id + * A unique string identifying the form for validation, submission, + * theming, and hook_form_alter functions. + * @param $form + * An associative array containing the structure of the form. + * @param $form_state + * A keyed array containing the current state of the form. Passed + * in here so that hook_form_alter() calls can use it, as well. + */ +function drupal_prepare_form($form_id, &$form, &$form_state) { + global $user; + + $form['#type'] = 'form'; + $form_state['programmed'] = isset($form_state['programmed']) ? $form_state['programmed'] : FALSE; + + // Fix the form method, if it is 'get' in $form_state, but not in $form. + if ($form_state['method'] == 'get' && !isset($form['#method'])) { + $form['#method'] = 'get'; + } + + // Generate a new #build_id for this form, if none has been set already. The + // form_build_id is used as key to cache a particular build of the form. For + // multi-step forms, this allows the user to go back to an earlier build, make + // changes, and re-submit. + // @see drupal_build_form() + // @see drupal_rebuild_form() + if (!isset($form['#build_id'])) { + $form['#build_id'] = 'form-' . drupal_hash_base64(uniqid(mt_rand(), TRUE) . mt_rand()); + } + $form['form_build_id'] = array( + '#type' => 'hidden', + '#value' => $form['#build_id'], + '#id' => $form['#build_id'], + '#name' => 'form_build_id', + ); + + // Add a token, based on either #token or form_id, to any form displayed to + // authenticated users. This ensures that any submitted form was actually + // requested previously by the user and protects against cross site request + // forgeries. + // This does not apply to programmatically submitted forms. Furthermore, since + // tokens are session-bound and forms displayed to anonymous users are very + // likely cached, we cannot assign a token for them. + // During installation, there is no $user yet. + if (!empty($user->uid) && !$form_state['programmed']) { + // Form constructors may explicitly set #token to FALSE when cross site + // request forgery is irrelevant to the form, such as search forms. + if (isset($form['#token']) && $form['#token'] === FALSE) { + unset($form['#token']); + } + // Otherwise, generate a public token based on the form id. + else { + $form['#token'] = $form_id; + $form['form_token'] = array( + '#id' => drupal_html_id('edit-' . $form_id . '-form-token'), + '#type' => 'token', + '#default_value' => drupal_get_token($form['#token']), + ); + } + } + + if (isset($form_id)) { + $form['form_id'] = array( + '#type' => 'hidden', + '#value' => $form_id, + '#id' => drupal_html_id("edit-$form_id"), + ); + } + if (!isset($form['#id'])) { + $form['#id'] = drupal_html_id($form_id); + } + + $form += element_info('form'); + $form += array('#tree' => FALSE, '#parents' => array()); + + if (!isset($form['#validate'])) { + // Check for a handler specific to $form_id. + if (function_exists($form_id . '_validate')) { + $form['#validate'][] = $form_id . '_validate'; + } + // Otherwise check whether this is a shared form and whether there is a + // handler for the shared $form_id. + elseif (isset($form_state['build_info']['base_form_id']) && function_exists($form_state['build_info']['base_form_id'] . '_validate')) { + $form['#validate'][] = $form_state['build_info']['base_form_id'] . '_validate'; + } + } + + if (!isset($form['#submit'])) { + // Check for a handler specific to $form_id. + if (function_exists($form_id . '_submit')) { + $form['#submit'][] = $form_id . '_submit'; + } + // Otherwise check whether this is a shared form and whether there is a + // handler for the shared $form_id. + elseif (isset($form_state['build_info']['base_form_id']) && function_exists($form_state['build_info']['base_form_id'] . '_submit')) { + $form['#submit'][] = $form_state['build_info']['base_form_id'] . '_submit'; + } + } + + // If no #theme has been set, automatically apply theme suggestions. + // theme_form() itself is in #theme_wrappers and not #theme. Therefore, the + // #theme function only has to care for rendering the inner form elements, + // not the form itself. + if (!isset($form['#theme'])) { + $form['#theme'] = array($form_id); + if (isset($form_state['build_info']['base_form_id'])) { + $form['#theme'][] = $form_state['build_info']['base_form_id']; + } + } + + // Invoke hook_form_alter(), hook_form_BASE_FORM_ID_alter(), and + // hook_form_FORM_ID_alter() implementations. + $hooks = array('form'); + if (isset($form_state['build_info']['base_form_id'])) { + $hooks[] = 'form_' . $form_state['build_info']['base_form_id']; + } + $hooks[] = 'form_' . $form_id; + drupal_alter($hooks, $form, $form_state, $form_id); +} + + +/** + * Validates user-submitted form data from the $form_state using + * the validate functions defined in a structured form array. + * + * @param $form_id + * A unique string identifying the form for validation, submission, + * theming, and hook_form_alter functions. + * @param $form + * An associative array containing the structure of the form, which is passed + * by reference. Form validation handlers are able to alter the form structure + * (like #process and #after_build callbacks during form building) in case of + * a validation error. If a validation handler alters the form structure, it + * is responsible for validating the values of changed form elements in + * $form_state['values'] to prevent form submit handlers from receiving + * unvalidated values. + * @param $form_state + * A keyed array containing the current state of the form. The current + * user-submitted data is stored in $form_state['values'], though + * form validation functions are passed an explicit copy of the + * values for the sake of simplicity. Validation handlers can also + * $form_state to pass information on to submit handlers. For example: + * $form_state['data_for_submission'] = $data; + * This technique is useful when validation requires file parsing, + * web service requests, or other expensive requests that should + * not be repeated in the submission step. + */ +function drupal_validate_form($form_id, &$form, &$form_state) { + $validated_forms = &drupal_static(__FUNCTION__, array()); + + if (isset($validated_forms[$form_id]) && empty($form_state['must_validate'])) { + return; + } + + // If the session token was set by drupal_prepare_form(), ensure that it + // matches the current user's session. + if (isset($form['#token'])) { + if (!drupal_valid_token($form_state['values']['form_token'], $form['#token'])) { + // Setting this error will cause the form to fail validation. + form_set_error('form_token', t('This form is outdated. Reload the page and try again. Contact the site administrator if the problem persists.')); + } + } + + _form_validate($form, $form_state, $form_id); + $validated_forms[$form_id] = TRUE; + + // If validation errors are limited then remove any non validated form values, + // so that only values that passed validation are left for submit callbacks. + if (isset($form_state['triggering_element']['#limit_validation_errors']) && $form_state['triggering_element']['#limit_validation_errors'] !== FALSE) { + $values = array(); + foreach ($form_state['triggering_element']['#limit_validation_errors'] as $section) { + // If the section exists within $form_state['values'], even if the value + // is NULL, copy it to $values. + $section_exists = NULL; + $value = drupal_array_get_nested_value($form_state['values'], $section, $section_exists); + if ($section_exists) { + drupal_array_set_nested_value($values, $section, $value); + } + } + // A button's #value does not require validation, so for convenience we + // allow the value of the clicked button to be retained in its normal + // $form_state['values'] locations, even if these locations are not included + // in #limit_validation_errors. + if (isset($form_state['triggering_element']['#button_type'])) { + $button_value = $form_state['triggering_element']['#value']; + + // Like all input controls, the button value may be in the location + // dictated by #parents. If it is, copy it to $values, but do not override + // what may already be in $values. + $parents = $form_state['triggering_element']['#parents']; + if (!drupal_array_nested_key_exists($values, $parents) && drupal_array_get_nested_value($form_state['values'], $parents) === $button_value) { + drupal_array_set_nested_value($values, $parents, $button_value); + } + + // Additionally, form_builder() places the button value in + // $form_state['values'][BUTTON_NAME]. If it's still there, after + // validation handlers have run, copy it to $values, but do not override + // what may already be in $values. + $name = $form_state['triggering_element']['#name']; + if (!isset($values[$name]) && isset($form_state['values'][$name]) && $form_state['values'][$name] === $button_value) { + $values[$name] = $button_value; + } + } + $form_state['values'] = $values; + } +} + +/** + * Redirects the user to a URL after a form has been processed. + * + * After a form was executed, the data in $form_state controls whether the form + * is redirected. By default, we redirect to a new destination page. The path of + * the destination page can be set in $form_state['redirect']. If that is not + * set, the user is redirected to the current page to display a fresh, + * unpopulated copy of the form. + * + * There are several triggers that may prevent a redirection though: + * - If $form_state['redirect'] is FALSE, a form builder function or form + * validation/submit handler does not want a user to be redirected, which + * means that drupal_goto() is not invoked. For most forms, the redirection + * logic will be the same regardless of whether $form_state['redirect'] is + * undefined or FALSE. However, in case it was not defined and the current + * request contains a 'destination' query string, drupal_goto() will redirect + * to that given destination instead. Only setting $form_state['redirect'] to + * FALSE will prevent any redirection. + * - If $form_state['no_redirect'] is TRUE, then the callback that originally + * built the form explicitly disallows any redirection, regardless of the + * redirection value in $form_state['redirect']. For example, ajax_get_form() + * defines $form_state['no_redirect'] when building a form in an Ajax + * callback to prevent any redirection. $form_state['no_redirect'] should NOT + * be altered by form builder functions or form validation/submit handlers. + * - If $form_state['programmed'] is TRUE, the form submission was usually + * invoked via drupal_form_submit(), so any redirection would break the script + * that invoked drupal_form_submit(). + * - If $form_state['rebuild'] is TRUE, the form needs to be rebuilt without + * redirection. + * + * @param $form_state + * A keyed array containing the current state of the form. + * + * @see drupal_process_form() + * @see drupal_build_form() + */ +function drupal_redirect_form($form_state) { + // Skip redirection for form submissions invoked via drupal_form_submit(). + if (!empty($form_state['programmed'])) { + return; + } + // Skip redirection if rebuild is activated. + if (!empty($form_state['rebuild'])) { + return; + } + // Skip redirection if it was explicitly disallowed. + if (!empty($form_state['no_redirect'])) { + return; + } + // Only invoke drupal_goto() if redirect value was not set to FALSE. + if (!isset($form_state['redirect']) || $form_state['redirect'] !== FALSE) { + if (isset($form_state['redirect'])) { + if (is_array($form_state['redirect'])) { + call_user_func_array('drupal_goto', $form_state['redirect']); + } + else { + // This function can be called from the installer, which guarantees + // that $redirect will always be a string, so catch that case here + // and use the appropriate redirect function. + $function = drupal_installation_attempted() ? 'install_goto' : 'drupal_goto'; + $function($form_state['redirect']); + } + } + drupal_goto($_GET['q']); + } +} + +/** + * Performs validation on form elements. First ensures required fields are + * completed, #maxlength is not exceeded, and selected options were in the + * list of options given to the user. Then calls user-defined validators. + * + * @param $elements + * An associative array containing the structure of the form. + * @param $form_state + * A keyed array containing the current state of the form. The current + * user-submitted data is stored in $form_state['values'], though + * form validation functions are passed an explicit copy of the + * values for the sake of simplicity. Validation handlers can also + * $form_state to pass information on to submit handlers. For example: + * $form_state['data_for_submission'] = $data; + * This technique is useful when validation requires file parsing, + * web service requests, or other expensive requests that should + * not be repeated in the submission step. + * @param $form_id + * A unique string identifying the form for validation, submission, + * theming, and hook_form_alter functions. + */ +function _form_validate(&$elements, &$form_state, $form_id = NULL) { + // Also used in the installer, pre-database setup. + $t = get_t(); + + // Recurse through all children. + foreach (element_children($elements) as $key) { + if (isset($elements[$key]) && $elements[$key]) { + _form_validate($elements[$key], $form_state); + } + } + + // Validate the current input. + if (!isset($elements['#validated']) || !$elements['#validated']) { + // The following errors are always shown. + if (isset($elements['#needs_validation'])) { + // Verify that the value is not longer than #maxlength. + if (isset($elements['#maxlength']) && drupal_strlen($elements['#value']) > $elements['#maxlength']) { + form_error($elements, $t('!name cannot be longer than %max characters but is currently %length characters long.', array('!name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title'], '%max' => $elements['#maxlength'], '%length' => drupal_strlen($elements['#value'])))); + } + + if (isset($elements['#options']) && isset($elements['#value'])) { + if ($elements['#type'] == 'select') { + $options = form_options_flatten($elements['#options']); + } + else { + $options = $elements['#options']; + } + if (is_array($elements['#value'])) { + $value = in_array($elements['#type'], array('checkboxes', 'tableselect')) ? array_keys($elements['#value']) : $elements['#value']; + foreach ($value as $v) { + if (!isset($options[$v])) { + form_error($elements, $t('An illegal choice has been detected. Please contact the site administrator.')); + watchdog('form', 'Illegal choice %choice in !name element.', array('%choice' => $v, '!name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']), LOG_ERR); + } + } + } + // Non-multiple select fields always have a value in HTML. If the user + // does not change the form, it will be the value of the first option. + // Because of this, form validation for the field will almost always + // pass, even if the user did not select anything. To work around this + // browser behavior, required select fields without a #default_value get + // an additional, first empty option. In case the submitted value is + // identical to the empty option's value, we reset the element's value + // to NULL to trigger the regular #required handling below. + // @see form_process_select() + elseif ($elements['#type'] == 'select' && !$elements['#multiple'] && $elements['#required'] && !isset($elements['#default_value']) && $elements['#value'] === $elements['#empty_value']) { + $elements['#value'] = NULL; + form_set_value($elements, NULL, $form_state); + } + elseif (!isset($options[$elements['#value']])) { + form_error($elements, $t('An illegal choice has been detected. Please contact the site administrator.')); + watchdog('form', 'Illegal choice %choice in %name element.', array('%choice' => $elements['#value'], '%name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']), LOG_ERR); + } + } + } + + // While this element is being validated, it may be desired that some calls + // to form_set_error() be suppressed and not result in a form error, so + // that a button that implements low-risk functionality (such as "Previous" + // or "Add more") that doesn't require all user input to be valid can still + // have its submit handlers triggered. The triggering element's + // #limit_validation_errors property contains the information for which + // errors are needed, and all other errors are to be suppressed. The + // #limit_validation_errors property is ignored if submit handlers will run, + // but the element doesn't have a #submit property, because it's too large a + // security risk to have any invalid user input when executing form-level + // submit handlers. + if (isset($form_state['triggering_element']['#limit_validation_errors']) && ($form_state['triggering_element']['#limit_validation_errors'] !== FALSE) && !($form_state['submitted'] && !isset($form_state['triggering_element']['#submit']))) { + form_set_error(NULL, '', $form_state['triggering_element']['#limit_validation_errors']); + } + // If submit handlers won't run (due to the submission having been triggered + // by an element whose #executes_submit_callback property isn't TRUE), then + // it's safe to suppress all validation errors, and we do so by default, + // which is particularly useful during an Ajax submission triggered by a + // non-button. An element can override this default by setting the + // #limit_validation_errors property. For button element types, + // #limit_validation_errors defaults to FALSE (via system_element_info()), + // so that full validation is their default behavior. + elseif (isset($form_state['triggering_element']) && !isset($form_state['triggering_element']['#limit_validation_errors']) && !$form_state['submitted']) { + form_set_error(NULL, '', array()); + } + // As an extra security measure, explicitly turn off error suppression if + // one of the above conditions wasn't met. Since this is also done at the + // end of this function, doing it here is only to handle the rare edge case + // where a validate handler invokes form processing of another form. + else { + drupal_static_reset('form_set_error:limit_validation_errors'); + } + + // Make sure a value is passed when the field is required. + if (isset($elements['#needs_validation']) && $elements['#required']) { + // A simple call to empty() will not cut it here as some fields, like + // checkboxes, can return a valid value of '0'. Instead, check the + // length if it's a string, and the item count if it's an array. + // An unchecked checkbox has a #value of integer 0, different than string + // '0', which could be a valid value. + $is_empty_multiple = (!count($elements['#value'])); + $is_empty_string = (is_string($elements['#value']) && drupal_strlen(trim($elements['#value'])) == 0); + $is_empty_value = ($elements['#value'] === 0); + if ($is_empty_multiple || $is_empty_string || $is_empty_value) { + // Although discouraged, a #title is not mandatory for form elements. In + // case there is no #title, we cannot set a form error message. + // Instead of setting no #title, form constructors are encouraged to set + // #title_display to 'invisible' to improve accessibility. + if (isset($elements['#title'])) { + form_error($elements, $t('!name field is required.', array('!name' => $elements['#title']))); + } + else { + form_error($elements); + } + } + } + + // Call user-defined form level validators. + if (isset($form_id)) { + form_execute_handlers('validate', $elements, $form_state); + } + // Call any element-specific validators. These must act on the element + // #value data. + elseif (isset($elements['#element_validate'])) { + foreach ($elements['#element_validate'] as $function) { + $function($elements, $form_state, $form_state['complete form']); + } + } + $elements['#validated'] = TRUE; + } + + // Done validating this element, so turn off error suppression. + // _form_validate() turns it on again when starting on the next element, if + // it's still appropriate to do so. + drupal_static_reset('form_set_error:limit_validation_errors'); +} + +/** + * A helper function used to execute custom validation and submission + * handlers for a given form. Button-specific handlers are checked + * first. If none exist, the function falls back to form-level handlers. + * + * @param $type + * The type of handler to execute. 'validate' or 'submit' are the + * defaults used by Form API. + * @param $form + * An associative array containing the structure of the form. + * @param $form_state + * A keyed array containing the current state of the form. If the user + * submitted the form by clicking a button with custom handler functions + * defined, those handlers will be stored here. + */ +function form_execute_handlers($type, &$form, &$form_state) { + $return = FALSE; + // If there was a button pressed, use its handlers. + if (isset($form_state[$type . '_handlers'])) { + $handlers = $form_state[$type . '_handlers']; + } + // Otherwise, check for a form-level handler. + elseif (isset($form['#' . $type])) { + $handlers = $form['#' . $type]; + } + else { + $handlers = array(); + } + + foreach ($handlers as $function) { + // Check if a previous _submit handler has set a batch, but make sure we + // do not react to a batch that is already being processed (for instance + // if a batch operation performs a drupal_form_submit()). + if ($type == 'submit' && ($batch =& batch_get()) && !isset($batch['id'])) { + // Some previous submit handler has set a batch. To ensure correct + // execution order, store the call in a special 'control' batch set. + // See _batch_next_set(). + $batch['sets'][] = array('form_submit' => $function); + $batch['has_form_submits'] = TRUE; + } + else { + $function($form, $form_state); + } + $return = TRUE; + } + return $return; +} + +/** + * Files an error against a form element. + * + * When a validation error is detected, the validator calls form_set_error() to + * indicate which element needs to be changed and provide an error message. This + * causes the Form API to not execute the form submit handlers, and instead to + * re-display the form to the user with the corresponding elements rendered with + * an 'error' CSS class (shown as red by default). + * + * The standard form_set_error() behavior can be changed if a button provides + * the #limit_validation_errors property. Multistep forms not wanting to + * validate the whole form can set #limit_validation_errors on buttons to + * limit validation errors to only certain elements. For example, pressing the + * "Previous" button in a multistep form should not fire validation errors just + * because the current step has invalid values. If #limit_validation_errors is + * set on a clicked button, the button must also define a #submit property + * (may be set to an empty array). Any #submit handlers will be executed even if + * there is invalid input, so extreme care should be taken with respect to any + * actions taken by them. This is typically not a problem with buttons like + * "Previous" or "Add more" that do not invoke persistent storage of the + * submitted form values. Do not use the #limit_validation_errors property on + * buttons that trigger saving of form values to the database. + * + * The #limit_validation_errors property is a list of "sections" within + * $form_state['values'] that must contain valid values. Each "section" is an + * array with the ordered set of keys needed to reach that part of + * $form_state['values'] (i.e., the #parents property of the element). + * + * Example 1: Allow the "Previous" button to function, regardless of whether any + * user input is valid. + * + * @code + * $form['actions']['previous'] = array( + * '#type' => 'submit', + * '#value' => t('Previous'), + * '#limit_validation_errors' => array(), // No validation. + * '#submit' => array('some_submit_function'), // #submit required. + * ); + * @endcode + * + * Example 2: Require some, but not all, user input to be valid to process the + * submission of a "Previous" button. + * + * @code + * $form['actions']['previous'] = array( + * '#type' => 'submit', + * '#value' => t('Previous'), + * '#limit_validation_errors' => array( + * array('step1'), // Validate $form_state['values']['step1']. + * array('foo', 'bar'), // Validate $form_state['values']['foo']['bar']. + * ), + * '#submit' => array('some_submit_function'), // #submit required. + * ); + * @endcode + * + * This will require $form_state['values']['step1'] and everything within it + * (for example, $form_state['values']['step1']['choice']) to be valid, so + * calls to form_set_error('step1', $message) or + * form_set_error('step1][choice', $message) will prevent the submit handlers + * from running, and result in the error message being displayed to the user. + * However, calls to form_set_error('step2', $message) and + * form_set_error('step2][groupX][choiceY', $message) will be suppressed, + * resulting in the message not being displayed to the user, and the submit + * handlers will run despite $form_state['values']['step2'] and + * $form_state['values']['step2']['groupX']['choiceY'] containing invalid + * values. Errors for an invalid $form_state['values']['foo'] will be + * suppressed, but errors flagging invalid values for + * $form_state['values']['foo']['bar'] and everything within it will be + * flagged and submission prevented. + * + * Partial form validation is implemented by suppressing errors rather than by + * skipping the input processing and validation steps entirely, because some + * forms have button-level submit handlers that call Drupal API functions that + * assume that certain data exists within $form_state['values'], and while not + * doing anything with that data that requires it to be valid, PHP errors + * would be triggered if the input processing and validation steps were fully + * skipped. + * @see http://drupal.org/node/370537 + * @see http://drupal.org/node/763376 + * + * @param $name + * The name of the form element. If the #parents property of your form + * element is array('foo', 'bar', 'baz') then you may set an error on 'foo' + * or 'foo][bar][baz'. Setting an error on 'foo' sets an error for every + * element where the #parents array starts with 'foo'. + * @param $message + * The error message to present to the user. + * @param $limit_validation_errors + * Internal use only. The #limit_validation_errors property of the clicked + * button, if it exists. + * + * @return + * Return value is for internal use only. To get a list of errors, use + * form_get_errors() or form_get_error(). + */ +function form_set_error($name = NULL, $message = '', $limit_validation_errors = NULL) { + $form = &drupal_static(__FUNCTION__, array()); + $sections = &drupal_static(__FUNCTION__ . ':limit_validation_errors'); + if (isset($limit_validation_errors)) { + $sections = $limit_validation_errors; + } + + if (isset($name) && !isset($form[$name])) { + $record = TRUE; + if (isset($sections)) { + // #limit_validation_errors is an array of "sections" within which user + // input must be valid. If the element is within one of these sections, + // the error must be recorded. Otherwise, it can be suppressed. + // #limit_validation_errors can be an empty array, in which case all + // errors are suppressed. For example, a "Previous" button might want its + // submit action to be triggered even if none of the submitted values are + // valid. + $record = FALSE; + foreach ($sections as $section) { + // Exploding by '][' reconstructs the element's #parents. If the + // reconstructed #parents begin with the same keys as the specified + // section, then the element's values are within the part of + // $form_state['values'] that the clicked button requires to be valid, + // so errors for this element must be recorded. As the exploded array + // will all be strings, we need to cast every value of the section + // array to string. + if (array_slice(explode('][', $name), 0, count($section)) === array_map('strval', $section)) { + $record = TRUE; + break; + } + } + } + if ($record) { + $form[$name] = $message; + if ($message) { + drupal_set_message($message, 'error'); + } + } + } + + return $form; +} + +/** + * Clear all errors against all form elements made by form_set_error(). + */ +function form_clear_error() { + drupal_static_reset('form_set_error'); +} + +/** + * Return an associative array of all errors. + */ +function form_get_errors() { + $form = form_set_error(); + if (!empty($form)) { + return $form; + } +} + +/** + * Returns the error message filed against the given form element. + * + * Form errors higher up in the form structure override deeper errors as well as + * errors on the element itself. + */ +function form_get_error($element) { + $form = form_set_error(); + $parents = array(); + foreach ($element['#parents'] as $parent) { + $parents[] = $parent; + $key = implode('][', $parents); + if (isset($form[$key])) { + return $form[$key]; + } + } +} + +/** + * Flag an element as having an error. + */ +function form_error(&$element, $message = '') { + form_set_error(implode('][', $element['#parents']), $message); +} + +/** + * Walk through the structured form array, adding any required properties to + * each element and mapping the incoming input data to the proper elements. + * Also, execute any #process handlers attached to a specific element. + * + * This is one of the three primary functions that recursively iterates a form + * array. This one does it for completing the form building process. The other + * two are _form_validate() (invoked via drupal_validate_form() and used to + * invoke validation logic for each element) and drupal_render() (for rendering + * each element). Each of these three pipelines provides ample opportunity for + * modules to customize what happens. For example, during this function's life + * cycle, the following functions get called for each element: + * - $element['#value_callback']: A function that implements how user input is + * mapped to an element's #value property. This defaults to a function named + * 'form_type_TYPE_value' where TYPE is $element['#type']. + * - $element['#process']: An array of functions called after user input has + * been mapped to the element's #value property. These functions can be used + * to dynamically add child elements: for example, for the 'date' element + * type, one of the functions in this array is form_process_date(), which adds + * the individual 'year', 'month', 'day', etc. child elements. These functions + * can also be used to set additional properties or implement special logic + * other than adding child elements: for example, for the 'fieldset' element + * type, one of the functions in this array is form_process_fieldset(), which + * adds the attributes and JavaScript needed to make the fieldset collapsible + * if the #collapsible property is set. The #process functions are called in + * preorder traversal, meaning they are called for the parent element first, + * then for the child elements. + * - $element['#after_build']: An array of functions called after form_builder() + * is done with its processing of the element. These are called in postorder + * traversal, meaning they are called for the child elements first, then for + * the parent element. + * There are similar properties containing callback functions invoked by + * _form_validate() and drupal_render(), appropriate for those operations. + * + * Developers are strongly encouraged to integrate the functionality needed by + * their form or module within one of these three pipelines, using the + * appropriate callback property, rather than implementing their own recursive + * traversal of a form array. This facilitates proper integration between + * multiple modules. For example, module developers are familiar with the + * relative order in which hook_form_alter() implementations and #process + * functions run. A custom traversal function that affects the building of a + * form is likely to not integrate with hook_form_alter() and #process in the + * expected way. Also, deep recursion within PHP is both slow and memory + * intensive, so it is best to minimize how often it's done. + * + * As stated above, each element's #process functions are executed after its + * #value has been set. This enables those functions to execute conditional + * logic based on the current value. However, all of form_builder() runs before + * drupal_validate_form() is called, so during #process function execution, the + * element's #value has not yet been validated, so any code that requires + * validated values must reside within a submit handler. + * + * As a security measure, user input is used for an element's #value only if the + * element exists within $form, is not disabled (as per the #disabled property), + * and can be accessed (as per the #access property, except that forms submitted + * using drupal_form_submit() bypass #access restrictions). When user input is + * ignored due to #disabled and #access restrictions, the element's default + * value is used. + * + * Because of the preorder traversal, where #process functions of an element run + * before user input for its child elements is processed, and because of the + * Form API security of user input processing with respect to #access and + * #disabled described above, this generally means that #process functions + * should not use an element's (unvalidated) #value to affect the #disabled or + * #access of child elements. Use-cases where a developer may be tempted to + * implement such conditional logic usually fall into one of two categories: + * - Where user input from the current submission must affect the structure of a + * form, including properties like #access and #disabled that affect how the + * next submission needs to be processed, a multi-step workflow is needed. + * This is most commonly implemented with a submit handler setting persistent + * data within $form_state based on *validated* values in + * $form_state['values'] and setting $form_state['rebuild']. The form building + * functions must then be implemented to use the $form_state data to rebuild + * the form with the structure appropriate for the new state. + * - Where user input must affect the rendering of the form without affecting + * its structure, the necessary conditional rendering logic should reside + * within functions that run during the rendering phase (#pre_render, #theme, + * #theme_wrappers, and #post_render). + * + * @param $form_id + * A unique string identifying the form for validation, submission, + * theming, and hook_form_alter functions. + * @param $element + * An associative array containing the structure of the current element. + * @param $form_state + * A keyed array containing the current state of the form. In this + * context, it is used to accumulate information about which button + * was clicked when the form was submitted, as well as the sanitized + * $_POST data. + */ +function form_builder($form_id, &$element, &$form_state) { + // Initialize as unprocessed. + $element['#processed'] = FALSE; + + // Use element defaults. + if (isset($element['#type']) && empty($element['#defaults_loaded']) && ($info = element_info($element['#type']))) { + // Overlay $info onto $element, retaining preexisting keys in $element. + $element += $info; + $element['#defaults_loaded'] = TRUE; + } + // Assign basic defaults common for all form elements. + $element += array( + '#required' => FALSE, + '#attributes' => array(), + '#title_display' => 'before', + ); + + // Special handling if we're on the top level form element. + if (isset($element['#type']) && $element['#type'] == 'form') { + if (!empty($element['#https']) && variable_get('https', FALSE) && + !url_is_external($element['#action'])) { + global $base_root; + + // Not an external URL so ensure that it is secure. + $element['#action'] = str_replace('http://', 'https://', $base_root) . $element['#action']; + } + + // Store a reference to the complete form in $form_state prior to building + // the form. This allows advanced #process and #after_build callbacks to + // perform changes elsewhere in the form. + $form_state['complete form'] = &$element; + + // Set a flag if we have a correct form submission. This is always TRUE for + // programmed forms coming from drupal_form_submit(), or if the form_id coming + // from the POST data is set and matches the current form_id. + if ($form_state['programmed'] || (!empty($form_state['input']) && (isset($form_state['input']['form_id']) && ($form_state['input']['form_id'] == $form_id)))) { + $form_state['process_input'] = TRUE; + } + else { + $form_state['process_input'] = FALSE; + } + + // All form elements should have an #array_parents property. + $element['#array_parents'] = array(); + } + + if (!isset($element['#id'])) { + $element['#id'] = drupal_html_id('edit-' . implode('-', $element['#parents'])); + } + // Handle input elements. + if (!empty($element['#input'])) { + _form_builder_handle_input_element($form_id, $element, $form_state); + } + // Allow for elements to expand to multiple elements, e.g., radios, + // checkboxes and files. + if (isset($element['#process']) && !$element['#processed']) { + foreach ($element['#process'] as $process) { + $element = $process($element, $form_state, $form_state['complete form']); + } + $element['#processed'] = TRUE; + } + + // We start off assuming all form elements are in the correct order. + $element['#sorted'] = TRUE; + + // Recurse through all child elements. + $count = 0; + foreach (element_children($element) as $key) { + // Prior to checking properties of child elements, their default properties + // need to be loaded. + if (isset($element[$key]['#type']) && empty($element[$key]['#defaults_loaded']) && ($info = element_info($element[$key]['#type']))) { + $element[$key] += $info; + $element[$key]['#defaults_loaded'] = TRUE; + } + + // Don't squash an existing tree value. + if (!isset($element[$key]['#tree'])) { + $element[$key]['#tree'] = $element['#tree']; + } + + // Deny access to child elements if parent is denied. + if (isset($element['#access']) && !$element['#access']) { + $element[$key]['#access'] = FALSE; + } + + // Make child elements inherit their parent's #disabled and #allow_focus + // values unless they specify their own. + foreach (array('#disabled', '#allow_focus') as $property) { + if (isset($element[$property]) && !isset($element[$key][$property])) { + $element[$key][$property] = $element[$property]; + } + } + + // Don't squash existing parents value. + if (!isset($element[$key]['#parents'])) { + // Check to see if a tree of child elements is present. If so, + // continue down the tree if required. + $element[$key]['#parents'] = $element[$key]['#tree'] && $element['#tree'] ? array_merge($element['#parents'], array($key)) : array($key); + } + // Ensure #array_parents follows the actual form structure. + $array_parents = $element['#array_parents']; + $array_parents[] = $key; + $element[$key]['#array_parents'] = $array_parents; + + // Assign a decimal placeholder weight to preserve original array order. + if (!isset($element[$key]['#weight'])) { + $element[$key]['#weight'] = $count/1000; + } + else { + // If one of the child elements has a weight then we will need to sort + // later. + unset($element['#sorted']); + } + $element[$key] = form_builder($form_id, $element[$key], $form_state); + $count++; + } + + // The #after_build flag allows any piece of a form to be altered + // after normal input parsing has been completed. + if (isset($element['#after_build']) && !isset($element['#after_build_done'])) { + foreach ($element['#after_build'] as $function) { + $element = $function($element, $form_state); + } + $element['#after_build_done'] = TRUE; + } + + // If there is a file element, we need to flip a flag so later the + // form encoding can be set. + if (isset($element['#type']) && $element['#type'] == 'file') { + $form_state['has_file_element'] = TRUE; + } + + // Final tasks for the form element after form_builder() has run for all other + // elements. + if (isset($element['#type']) && $element['#type'] == 'form') { + // If there is a file element, we set the form encoding. + if (isset($form_state['has_file_element'])) { + $element['#attributes']['enctype'] = 'multipart/form-data'; + } + + // If a form contains a single textfield, and the ENTER key is pressed + // within it, Internet Explorer submits the form with no POST data + // identifying any submit button. Other browsers submit POST data as though + // the user clicked the first button. Therefore, to be as consistent as we + // can be across browsers, if no 'triggering_element' has been identified + // yet, default it to the first button. + if (!$form_state['programmed'] && !isset($form_state['triggering_element']) && !empty($form_state['buttons'])) { + $form_state['triggering_element'] = $form_state['buttons'][0]; + } + + // If the triggering element specifies "button-level" validation and submit + // handlers to run instead of the default form-level ones, then add those to + // the form state. + foreach (array('validate', 'submit') as $type) { + if (isset($form_state['triggering_element']['#' . $type])) { + $form_state[$type . '_handlers'] = $form_state['triggering_element']['#' . $type]; + } + } + + // If the triggering element executes submit handlers, then set the form + // state key that's needed for those handlers to run. + if (!empty($form_state['triggering_element']['#executes_submit_callback'])) { + $form_state['submitted'] = TRUE; + } + + // Special processing if the triggering element is a button. + if (isset($form_state['triggering_element']['#button_type'])) { + // Because there are several ways in which the triggering element could + // have been determined (including from input variables set by JavaScript + // or fallback behavior implemented for IE), and because buttons often + // have their #name property not derived from their #parents property, we + // can't assume that input processing that's happened up until here has + // resulted in $form_state['values'][BUTTON_NAME] being set. But it's + // common for forms to have several buttons named 'op' and switch on + // $form_state['values']['op'] during submit handler execution. + $form_state['values'][$form_state['triggering_element']['#name']] = $form_state['triggering_element']['#value']; + + // @todo Legacy support. Remove in Drupal 8. + $form_state['clicked_button'] = $form_state['triggering_element']; + } + } + return $element; +} + +/** + * Populate the #value and #name properties of input elements so they + * can be processed and rendered. + */ +function _form_builder_handle_input_element($form_id, &$element, &$form_state) { + if (!isset($element['#name'])) { + $name = array_shift($element['#parents']); + $element['#name'] = $name; + if ($element['#type'] == 'file') { + // To make it easier to handle $_FILES in file.inc, we place all + // file fields in the 'files' array. Also, we do not support + // nested file names. + $element['#name'] = 'files[' . $element['#name'] . ']'; + } + elseif (count($element['#parents'])) { + $element['#name'] .= '[' . implode('][', $element['#parents']) . ']'; + } + array_unshift($element['#parents'], $name); + } + + // Setting #disabled to TRUE results in user input being ignored, regardless + // of how the element is themed or whether JavaScript is used to change the + // control's attributes. However, it's good UI to let the user know that input + // is not wanted for the control. HTML supports two attributes for this: + // http://www.w3.org/TR/html401/interact/forms.html#h-17.12. If a form wants + // to start a control off with one of these attributes for UI purposes only, + // but still allow input to be processed if it's sumitted, it can set the + // desired attribute in #attributes directly rather than using #disabled. + // However, developers should think carefully about the accessibility + // implications of doing so: if the form expects input to be enterable under + // some condition triggered by JavaScript, how would someone who has + // JavaScript disabled trigger that condition? Instead, developers should + // consider whether a multi-step form would be more appropriate (#disabled can + // be changed from step to step). If one still decides to use JavaScript to + // affect when a control is enabled, then it is best for accessibility for the + // control to be enabled in the HTML, and disabled by JavaScript on document + // ready. + if (!empty($element['#disabled'])) { + if (!empty($element['#allow_focus'])) { + $element['#attributes']['readonly'] = 'readonly'; + } + else { + $element['#attributes']['disabled'] = 'disabled'; + } + } + + // With JavaScript or other easy hacking, input can be submitted even for + // elements with #access=FALSE or #disabled=TRUE. For security, these must + // not be processed. Forms that set #disabled=TRUE on an element do not + // expect input for the element, and even forms submitted with + // drupal_form_submit() must not be able to get around this. Forms that set + // #access=FALSE on an element usually allow access for some users, so forms + // submitted with drupal_form_submit() may bypass access restriction and be + // treated as high-privelege users instead. + $process_input = empty($element['#disabled']) && ($form_state['programmed'] || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access']))); + + // Set the element's #value property. + if (!isset($element['#value']) && !array_key_exists('#value', $element)) { + $value_callback = !empty($element['#value_callback']) ? $element['#value_callback'] : 'form_type_' . $element['#type'] . '_value'; + if ($process_input) { + // Get the input for the current element. NULL values in the input need to + // be explicitly distinguished from missing input. (see below) + $input_exists = NULL; + $input = drupal_array_get_nested_value($form_state['input'], $element['#parents'], $input_exists); + // For browser-submitted forms, the submitted values do not contain values + // for certain elements (empty multiple select, unchecked checkbox). + // During initial form processing, we add explicit NULL values for such + // elements in $form_state['input']. When rebuilding the form, we can + // distinguish elements having NULL input from elements that were not part + // of the initially submitted form and can therefore use default values + // for the latter, if required. Programmatically submitted forms can + // submit explicit NULL values when calling drupal_form_submit(), so we do + // not modify $form_state['input'] for them. + if (!$input_exists && !$form_state['rebuild'] && !$form_state['programmed']) { + // Add the necessary parent keys to $form_state['input'] and sets the + // element's input value to NULL. + drupal_array_set_nested_value($form_state['input'], $element['#parents'], NULL); + $input_exists = TRUE; + } + // If we have input for the current element, assign it to the #value + // property, optionally filtered through $value_callback. + if ($input_exists) { + if (function_exists($value_callback)) { + $element['#value'] = $value_callback($element, $input, $form_state); + } + if (!isset($element['#value']) && isset($input)) { + $element['#value'] = $input; + } + } + // Mark all posted values for validation. + if (isset($element['#value']) || (!empty($element['#required']))) { + $element['#needs_validation'] = TRUE; + } + } + // Load defaults. + if (!isset($element['#value'])) { + // Call #type_value without a second argument to request default_value handling. + if (function_exists($value_callback)) { + $element['#value'] = $value_callback($element, FALSE, $form_state); + } + // Final catch. If we haven't set a value yet, use the explicit default value. + // Avoid image buttons (which come with garbage value), so we only get value + // for the button actually clicked. + if (!isset($element['#value']) && empty($element['#has_garbage_value'])) { + $element['#value'] = isset($element['#default_value']) ? $element['#default_value'] : ''; + } + } + } + + // Determine which element (if any) triggered the submission of the form and + // keep track of all the clickable buttons in the form for + // form_state_values_clean(). Enforce the same input processing restrictions + // as above. + if ($process_input) { + // Detect if the element triggered the submission via Ajax. + if (_form_element_triggered_scripted_submission($element, $form_state)) { + $form_state['triggering_element'] = $element; + } + + // If the form was submitted by the browser rather than via Ajax, then it + // can only have been triggered by a button, and we need to determine which + // button within the constraints of how browsers provide this information. + if (isset($element['#button_type'])) { + // All buttons in the form need to be tracked for + // form_state_values_clean() and for the form_builder() code that handles + // a form submission containing no button information in $_POST. + $form_state['buttons'][] = $element; + if (_form_button_was_clicked($element, $form_state)) { + $form_state['triggering_element'] = $element; + } + } + } + + // Set the element's value in $form_state['values'], but only, if its key + // does not exist yet (a #value_callback may have already populated it). + if (!drupal_array_nested_key_exists($form_state['values'], $element['#parents'])) { + form_set_value($element, $element['#value'], $form_state); + } +} + +/** + * Helper function to handle the convoluted logic of button click detection. + * + * This detects button or non-button controls that trigger a form submission via + * Ajax or some other scriptable environment. These environments can set the + * special input key '_triggering_element_name' to identify the triggering + * element. If the name alone doesn't identify the element uniquely, the input + * key '_triggering_element_value' may also be set to require a match on element + * value. An example where this is needed is if there are several buttons all + * named 'op', and only differing in their value. + */ +function _form_element_triggered_scripted_submission($element, &$form_state) { + if (!empty($form_state['input']['_triggering_element_name']) && $element['#name'] == $form_state['input']['_triggering_element_name']) { + if (empty($form_state['input']['_triggering_element_value']) || $form_state['input']['_triggering_element_value'] == $element['#value']) { + return TRUE; + } + } + return FALSE; +} + +/** + * Helper function to handle the convoluted logic of button click detection. + * + * This detects button controls that trigger a form submission by being clicked + * and having the click processed by the browser rather than being captured by + * JavaScript. Essentially, it detects if the button's name and value are part + * of the POST data, but with extra code to deal with the convoluted way in + * which browsers submit data for image button clicks. + * + * This does not detect button clicks processed by Ajax (that is done in + * _form_element_triggered_scripted_submission()) and it does not detect form + * submissions from Internet Explorer in response to an ENTER key pressed in a + * textfield (form_builder() has extra code for that). + * + * Because this function contains only part of the logic needed to determine + * $form_state['triggering_element'], it should not be called from anywhere + * other than within the Form API. Form validation and submit handlers needing + * to know which button was clicked should get that information from + * $form_state['triggering_element']. + */ +function _form_button_was_clicked($element, &$form_state) { + // First detect normal 'vanilla' button clicks. Traditionally, all + // standard buttons on a form share the same name (usually 'op'), + // and the specific return value is used to determine which was + // clicked. This ONLY works as long as $form['#name'] puts the + // value at the top level of the tree of $_POST data. + if (isset($form_state['input'][$element['#name']]) && $form_state['input'][$element['#name']] == $element['#value']) { + return TRUE; + } + // When image buttons are clicked, browsers do NOT pass the form element + // value in $_POST. Instead they pass an integer representing the + // coordinates of the click on the button image. This means that image + // buttons MUST have unique $form['#name'] values, but the details of + // their $_POST data should be ignored. + elseif (!empty($element['#has_garbage_value']) && isset($element['#value']) && $element['#value'] !== '') { + return TRUE; + } + return FALSE; +} + +/** + * Removes internal Form API elements and buttons from submitted form values. + * + * This function can be used when a module wants to store all submitted form + * values, for example, by serializing them into a single database column. In + * such cases, all internal Form API values and all form button elements should + * not be contained, and this function allows to remove them before the module + * proceeds to storage. Next to button elements, the following internal values + * are removed: + * - form_id + * - form_token + * - form_build_id + * - op + * + * @param $form_state + * A keyed array containing the current state of the form, including + * submitted form values; altered by reference. + */ +function form_state_values_clean(&$form_state) { + // Remove internal Form API values. + unset($form_state['values']['form_id'], $form_state['values']['form_token'], $form_state['values']['form_build_id'], $form_state['values']['op']); + + // Remove button values. + // form_builder() collects all button elements in a form. We remove the button + // value separately for each button element. + foreach ($form_state['buttons'] as $button) { + // Remove this button's value from the submitted form values by finding + // the value corresponding to this button. + // We iterate over the #parents of this button and move a reference to + // each parent in $form_state['values']. For example, if #parents is: + // array('foo', 'bar', 'baz') + // then the corresponding $form_state['values'] part will look like this: + // array( + // 'foo' => array( + // 'bar' => array( + // 'baz' => 'button_value', + // ), + // ), + // ) + // We start by (re)moving 'baz' to $last_parent, so we are able unset it + // at the end of the iteration. Initially, $values will contain a + // reference to $form_state['values'], but in the iteration we move the + // reference to $form_state['values']['foo'], and finally to + // $form_state['values']['foo']['bar'], which is the level where we can + // unset 'baz' (that is stored in $last_parent). + $parents = $button['#parents']; + $values = &$form_state['values']; + $last_parent = array_pop($parents); + foreach ($parents as $parent) { + $values = &$values[$parent]; + } + unset($values[$last_parent]); + } +} + +/** + * Helper function to determine the value for an image button form element. + * + * @param $form + * The form element whose value is being populated. + * @param $input + * The incoming input to populate the form element. If this is FALSE, + * the element's default value should be returned. + * @param $form_state + * A keyed array containing the current state of the form. + * @return + * The data that will appear in the $form_state['values'] collection + * for this element. Return nothing to use the default. + */ +function form_type_image_button_value($form, $input, $form_state) { + if ($input !== FALSE) { + if (!empty($input)) { + // If we're dealing with Mozilla or Opera, we're lucky. It will + // return a proper value, and we can get on with things. + return $form['#return_value']; + } + else { + // Unfortunately, in IE we never get back a proper value for THIS + // form element. Instead, we get back two split values: one for the + // X and one for the Y coordinates on which the user clicked the + // button. We'll find this element in the #post data, and search + // in the same spot for its name, with '_x'. + $input = $form_state['input']; + foreach (explode('[', $form['#name']) as $element_name) { + // chop off the ] that may exist. + if (substr($element_name, -1) == ']') { + $element_name = substr($element_name, 0, -1); + } + + if (!isset($input[$element_name])) { + if (isset($input[$element_name . '_x'])) { + return $form['#return_value']; + } + return NULL; + } + $input = $input[$element_name]; + } + return $form['#return_value']; + } + } +} + +/** + * Helper function to determine the value for a checkbox form element. + * + * @param $form + * The form element whose value is being populated. +* @param $input + * The incoming input to populate the form element. If this is FALSE, + * the element's default value should be returned. + * @return + * The data that will appear in the $element_state['values'] collection + * for this element. Return nothing to use the default. + */ +function form_type_checkbox_value($element, $input = FALSE) { + if ($input === FALSE) { + // Use #default_value as the default value of a checkbox, except change + // NULL to 0, because _form_builder_handle_input_element() would otherwise + // replace NULL with empty string, but an empty string is a potentially + // valid value for a checked checkbox. + return isset($element['#default_value']) ? $element['#default_value'] : 0; + } + else { + // Checked checkboxes are submitted with a value (possibly '0' or ''): + // http://www.w3.org/TR/html401/interact/forms.html#successful-controls. + // For checked checkboxes, browsers submit the string version of + // #return_value, but we return the original #return_value. For unchecked + // checkboxes, browsers submit nothing at all, but + // _form_builder_handle_input_element() detects this, and calls this + // function with $input=NULL. Returning NULL from a value callback means to + // use the default value, which is not what is wanted when an unchecked + // checkbox is submitted, so we use integer 0 as the value indicating an + // unchecked checkbox. Therefore, modules must not use integer 0 as a + // #return_value, as doing so results in the checkbox always being treated + // as unchecked. The string '0' is allowed for #return_value. The most + // common use-case for setting #return_value to either 0 or '0' is for the + // first option within a 0-indexed array of checkboxes, and for this, + // form_process_checkboxes() uses the string rather than the integer. + return isset($input) ? $element['#return_value'] : 0; + } +} + +/** + * Helper function to determine the value for a checkboxes form element. + * + * @param $element + * The form element whose value is being populated. + * @param $input + * The incoming input to populate the form element. If this is FALSE, + * the element's default value should be returned. + * @return + * The data that will appear in the $element_state['values'] collection + * for this element. Return nothing to use the default. + */ +function form_type_checkboxes_value($element, $input = FALSE) { + if ($input === FALSE) { + $value = array(); + $element += array('#default_value' => array()); + foreach ($element['#default_value'] as $key) { + $value[$key] = $key; + } + return $value; + } + elseif (is_array($input)) { + // Programmatic form submissions use NULL to indicate that a checkbox + // should be unchecked; see drupal_form_submit(). We therefore remove all + // NULL elements from the array before constructing the return value, to + // simulate the behavior of web browsers (which do not send unchecked + // checkboxes to the server at all). This will not affect non-programmatic + // form submissions, since all values in $_POST are strings. + foreach ($input as $key => $value) { + if (!isset($value)) { + unset($input[$key]); + } + } + return drupal_map_assoc($input); + } + else { + return array(); + } +} + +/** + * Helper function to determine the value for a tableselect form element. + * + * @param $element + * The form element whose value is being populated. + * @param $input + * The incoming input to populate the form element. If this is FALSE, + * the element's default value should be returned. + * @return + * The data that will appear in the $element_state['values'] collection + * for this element. Return nothing to use the default. + */ +function form_type_tableselect_value($element, $input = FALSE) { + // If $element['#multiple'] == FALSE, then radio buttons are displayed and + // the default value handling is used. + if (isset($element['#multiple']) && $element['#multiple']) { + // Checkboxes are being displayed with the default value coming from the + // keys of the #default_value property. This differs from the checkboxes + // element which uses the array values. + if ($input === FALSE) { + $value = array(); + $element += array('#default_value' => array()); + foreach ($element['#default_value'] as $key => $flag) { + if ($flag) { + $value[$key] = $key; + } + } + return $value; + } + else { + return is_array($input) ? drupal_map_assoc($input) : array(); + } + } +} + +/** + * Helper function to determine the value for a password_confirm form + * element. + * + * @param $element + * The form element whose value is being populated. + * @param $input + * The incoming input to populate the form element. If this is FALSE, + * the element's default value should be returned. + * @return + * The data that will appear in the $element_state['values'] collection + * for this element. Return nothing to use the default. + */ +function form_type_password_confirm_value($element, $input = FALSE) { + if ($input === FALSE) { + $element += array('#default_value' => array()); + return $element['#default_value'] + array('pass1' => '', 'pass2' => ''); + } +} + +/** + * Helper function to determine the value for a select form element. + * + * @param $element + * The form element whose value is being populated. + * @param $input + * The incoming input to populate the form element. If this is FALSE, + * the element's default value should be returned. + * @return + * The data that will appear in the $element_state['values'] collection + * for this element. Return nothing to use the default. + */ +function form_type_select_value($element, $input = FALSE) { + if ($input !== FALSE) { + if (isset($element['#multiple']) && $element['#multiple']) { + // If an enabled multi-select submits NULL, it means all items are + // unselected. A disabled multi-select always submits NULL, and the + // default value should be used. + if (empty($element['#disabled'])) { + return (is_array($input)) ? drupal_map_assoc($input) : array(); + } + else { + return (isset($element['#default_value']) && is_array($element['#default_value'])) ? $element['#default_value'] : array(); + } + } + // Non-multiple select elements may have an empty option preprended to them + // (see form_process_select()). When this occurs, usually #empty_value is + // an empty string, but some forms set #empty_value to integer 0 or some + // other non-string constant. PHP receives all submitted form input as + // strings, but if the empty option is selected, set the value to match the + // empty value exactly. + elseif (isset($element['#empty_value']) && $input === (string) $element['#empty_value']) { + return $element['#empty_value']; + } + else { + return $input; + } + } +} + +/** + * Helper function to determine the value for a textfield form element. + * + * @param $element + * The form element whose value is being populated. + * @param $input + * The incoming input to populate the form element. If this is FALSE, + * the element's default value should be returned. + * @return + * The data that will appear in the $element_state['values'] collection + * for this element. Return nothing to use the default. + */ +function form_type_textfield_value($element, $input = FALSE) { + if ($input !== FALSE && $input !== NULL) { + // Equate $input to the form value to ensure it's marked for + // validation. + return str_replace(array("\r", "\n"), '', $input); + } +} + +/** + * Helper function to determine the value for form's token value. + * + * @param $element + * The form element whose value is being populated. + * @param $input + * The incoming input to populate the form element. If this is FALSE, + * the element's default value should be returned. + * @return + * The data that will appear in the $element_state['values'] collection + * for this element. Return nothing to use the default. + */ +function form_type_token_value($element, $input = FALSE) { + if ($input !== FALSE) { + return (string) $input; + } +} + +/** + * Change submitted form values during form validation. + * + * Use this function to change the submitted value of a form element in a form + * validation function, so that the changed value persists in $form_state + * through to the submission handlers. + * + * Note that form validation functions are specified in the '#validate' + * component of the form array (the value of $form['#validate'] is an array of + * validation function names). If the form does not originate in your module, + * you can implement hook_form_FORM_ID_alter() to add a validation function + * to $form['#validate']. + * + * @param $element + * The form element that should have its value updated; in most cases you can + * just pass in the element from the $form array, although the only component + * that is actually used is '#parents'. If constructing yourself, set + * $element['#parents'] to be an array giving the path through the form + * array's keys to the element whose value you want to update. For instance, + * if you want to update the value of $form['elem1']['elem2'], which should be + * stored in $form_state['values']['elem1']['elem2'], you would set + * $element['#parents'] = array('elem1','elem2'). + * @param $value + * The new value for the form element. + * @param $form_state + * Form state array where the value change should be recorded. + */ +function form_set_value($element, $value, &$form_state) { + drupal_array_set_nested_value($form_state['values'], $element['#parents'], $value, TRUE); +} + +/** + * Allows PHP array processing of multiple select options with the same value. + * + * Used for form select elements which need to validate HTML option groups + * and multiple options which may return the same value. Associative PHP arrays + * cannot handle these structures, since they share a common key. + * + * @param $array + * The form options array to process. + * + * @return + * An array with all hierarchical elements flattened to a single array. + */ +function form_options_flatten($array) { + // Always reset static var when first entering the recursion. + drupal_static_reset('_form_options_flatten'); + return _form_options_flatten($array); +} + +/** + * Helper function for form_options_flatten(). + * + * Iterates over arrays which may share common values and produces a flat + * array that has removed duplicate keys. Also handles cases where objects + * are passed as array values. + */ +function _form_options_flatten($array) { + $return = &drupal_static(__FUNCTION__); + + foreach ($array as $key => $value) { + if (is_object($value)) { + _form_options_flatten($value->option); + } + elseif (is_array($value)) { + _form_options_flatten($value); + } + else { + $return[$key] = 1; + } + } + + return $return; +} + +/** + * Processes a select list form element. + * + * This process callback is mandatory for select fields, since all user agents + * automatically preselect the first available option of single (non-multiple) + * select lists. + * + * @param $element + * The form element to process. Properties used: + * - #multiple: (optional) Indicates whether one or more options can be + * selected. Defaults to FALSE. + * - #default_value: Must be NULL or not set in case there is no value for the + * element yet, in which case a first default option is inserted by default. + * Whether this first option is a valid option depends on whether the field + * is #required or not. + * - #required: (optional) Whether the user needs to select an option (TRUE) + * or not (FALSE). Defaults to FALSE. + * - #empty_option: (optional) The label to show for the first default option. + * By default, the label is automatically set to "- Please select -" for a + * required field and "- None -" for an optional field. + * - #empty_value: (optional) The value for the first default option, which is + * used to determine whether the user submitted a value or not. + * - If #required is TRUE, this defaults to '' (an empty string). + * - If #required is not TRUE and this value isn't set, then no extra option + * is added to the select control, leaving the control in a slightly + * illogical state, because there's no way for the user to select nothing, + * since all user agents automatically preselect the first available + * option. But people are used to this being the behavior of select + * controls. + * @todo Address the above issue in Drupal 8. + * - If #required is not TRUE and this value is set (most commonly to an + * empty string), then an extra option (see #empty_option above) + * representing a "non-selection" is added with this as its value. + * + * @see _form_validate() + */ +function form_process_select($element) { + // #multiple select fields need a special #name. + if ($element['#multiple']) { + $element['#attributes']['multiple'] = 'multiple'; + $element['#attributes']['name'] = $element['#name'] . '[]'; + } + // A non-#multiple select needs special handling to prevent user agents from + // preselecting the first option without intention. #multiple select lists do + // not get an empty option, as it would not make sense, user interface-wise. + else { + $required = $element['#required']; + // If the element is required and there is no #default_value, then add an + // empty option that will fail validation, so that the user is required to + // make a choice. Also, if there's a value for #empty_value or + // #empty_option, then add an option that represents emptiness. + if (($required && !isset($element['#default_value'])) || isset($element['#empty_value']) || isset($element['#empty_option'])) { + $element += array( + '#empty_value' => '', + '#empty_option' => $required ? t('- Select -') : t('- None -'), + ); + // The empty option is prepended to #options and purposively not merged + // to prevent another option in #options mistakenly using the same value + // as #empty_value. + $empty_option = array($element['#empty_value'] => $element['#empty_option']); + $element['#options'] = $empty_option + $element['#options']; + } + } + return $element; +} + +/** + * Returns HTML for a select form element. + * + * It is possible to group options together; to do this, change the format of + * $options to an associative array in which the keys are group labels, and the + * values are associative arrays in the normal $options format. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #value, #options, #description, #extra, + * #multiple, #required, #name, #attributes, #size. + * + * @ingroup themeable + */ +function theme_select($variables) { + $element = $variables['element']; + element_set_attributes($element, array('id', 'name', 'size')); + _form_set_class($element, array('form-select')); + + return '' . form_select_options($element) . ''; +} + +/** + * Converts a select form element's options array into an HTML. + * + * @param $element + * An associative array containing the properties of the element. + * @param $choices + * Mixed: Either an associative array of items to list as choices, or an + * object with an 'option' member that is an associative array. This + * parameter is only used internally and should not be passed. + * @return + * An HTML string of options for the select form element. + */ +function form_select_options($element, $choices = NULL) { + if (!isset($choices)) { + $choices = $element['#options']; + } + // array_key_exists() accommodates the rare event where $element['#value'] is NULL. + // isset() fails in this situation. + $value_valid = isset($element['#value']) || array_key_exists('#value', $element); + $value_is_array = $value_valid && is_array($element['#value']); + $options = ''; + foreach ($choices as $key => $choice) { + if (is_array($choice)) { + $options .= ''; + $options .= form_select_options($element, $choice); + $options .= ''; + } + elseif (is_object($choice)) { + $options .= form_select_options($element, $choice->option); + } + else { + $key = (string) $key; + if ($value_valid && (!$value_is_array && (string) $element['#value'] === $key || ($value_is_array && in_array($key, $element['#value'])))) { + $selected = ' selected="selected"'; + } + else { + $selected = ''; + } + $options .= ''; + } + } + return $options; +} + +/** + * Traverses a select element's #option array looking for any values + * that hold the given key. Returns an array of indexes that match. + * + * This function is useful if you need to modify the options that are + * already in a form element; for example, to remove choices which are + * not valid because of additional filters imposed by another module. + * One example might be altering the choices in a taxonomy selector. + * To correctly handle the case of a multiple hierarchy taxonomy, + * #options arrays can now hold an array of objects, instead of a + * direct mapping of keys to labels, so that multiple choices in the + * selector can have the same key (and label). This makes it difficult + * to manipulate directly, which is why this helper function exists. + * + * This function does not support optgroups (when the elements of the + * #options array are themselves arrays), and will return FALSE if + * arrays are found. The caller must either flatten/restore or + * manually do their manipulations in this case, since returning the + * index is not sufficient, and supporting this would make the + * "helper" too complicated and cumbersome to be of any help. + * + * As usual with functions that can return array() or FALSE, do not + * forget to use === and !== if needed. + * + * @param $element + * The select element to search. + * @param $key + * The key to look for. + * @return + * An array of indexes that match the given $key. Array will be + * empty if no elements were found. FALSE if optgroups were found. + */ +function form_get_options($element, $key) { + $keys = array(); + foreach ($element['#options'] as $index => $choice) { + if (is_array($choice)) { + return FALSE; + } + elseif (is_object($choice)) { + if (isset($choice->option[$key])) { + $keys[] = $index; + } + } + elseif ($index == $key) { + $keys[] = $index; + } + } + return $keys; +} + +/** + * Returns HTML for a fieldset form element and its children. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #attributes, #children, #collapsed, #collapsible, + * #description, #id, #title, #value. + * + * @ingroup themeable + */ +function theme_fieldset($variables) { + $element = $variables['element']; + element_set_attributes($element, array('id')); + _form_set_class($element, array('form-wrapper')); + + $output = ''; + if (!empty($element['#title'])) { + // Always wrap fieldset legends in a SPAN for CSS positioning. + $output .= '' . $element['#title'] . ''; + } + $output .= '
'; + if (!empty($element['#description'])) { + $output .= '
' . $element['#description'] . '
'; + } + $output .= $element['#children']; + if (isset($element['#value'])) { + $output .= $element['#value']; + } + $output .= '
'; + $output .= "\n"; + return $output; +} + +/** + * Returns HTML for a radio button form element. + * + * Note: The input "name" attribute needs to be sanitized before output, which + * is currently done by passing all attributes to drupal_attributes(). + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #required, #return_value, #value, #attributes, #title, + * #description + * + * @ingroup themeable + */ +function theme_radio($variables) { + $element = $variables['element']; + $element['#attributes']['type'] = 'radio'; + element_set_attributes($element, array('id', 'name', '#return_value' => 'value')); + + if (isset($element['#return_value']) && $element['#value'] !== FALSE && $element['#value'] == $element['#return_value']) { + $element['#attributes']['checked'] = 'checked'; + } + _form_set_class($element, array('form-radio')); + + return ''; +} + +/** + * Returns HTML for a set of radio button form elements. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #value, #options, #description, #required, + * #attributes, #children. + * + * @ingroup themeable + */ +function theme_radios($variables) { + $element = $variables['element']; + $attributes = array(); + if (isset($element['#id'])) { + $attributes['id'] = $element['#id']; + } + $attributes['class'] = 'form-radios'; + if (!empty($element['#attributes']['class'])) { + $attributes['class'] .= ' ' . implode(' ', $element['#attributes']['class']); + } + return '' . (!empty($element['#children']) ? $element['#children'] : '') . ''; +} + +/** + * Expand a password_confirm field into two text boxes. + */ +function form_process_password_confirm($element) { + $element['pass1'] = array( + '#type' => 'password', + '#title' => t('Password'), + '#value' => empty($element['#value']) ? NULL : $element['#value']['pass1'], + '#required' => $element['#required'], + '#attributes' => array('class' => array('password-field')), + ); + $element['pass2'] = array( + '#type' => 'password', + '#title' => t('Confirm password'), + '#value' => empty($element['#value']) ? NULL : $element['#value']['pass2'], + '#required' => $element['#required'], + '#attributes' => array('class' => array('password-confirm')), + ); + $element['#element_validate'] = array('password_confirm_validate'); + $element['#tree'] = TRUE; + + if (isset($element['#size'])) { + $element['pass1']['#size'] = $element['pass2']['#size'] = $element['#size']; + } + + return $element; +} + +/** + * Validate password_confirm element. + */ +function password_confirm_validate($element, &$element_state) { + $pass1 = trim($element['pass1']['#value']); + $pass2 = trim($element['pass2']['#value']); + if (!empty($pass1) || !empty($pass2)) { + if (strcmp($pass1, $pass2)) { + form_error($element, t('The specified passwords do not match.')); + } + } + elseif ($element['#required'] && !empty($element_state['input'])) { + form_error($element, t('Password field is required.')); + } + + // Password field must be converted from a two-element array into a single + // string regardless of validation results. + form_set_value($element['pass1'], NULL, $element_state); + form_set_value($element['pass2'], NULL, $element_state); + form_set_value($element, $pass1, $element_state); + + return $element; + +} + +/** + * Returns HTML for a date selection form element. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #value, #options, #description, #required, + * #attributes. + * + * @ingroup themeable + */ +function theme_date($variables) { + $element = $variables['element']; + return '
' . drupal_render_children($element) . '
'; +} + +/** + * Roll out a single date element. + */ +function form_process_date($element) { + // Default to current date + if (empty($element['#value'])) { + $element['#value'] = array( + 'day' => format_date(REQUEST_TIME, 'custom', 'j'), + 'month' => format_date(REQUEST_TIME, 'custom', 'n'), + 'year' => format_date(REQUEST_TIME, 'custom', 'Y'), + ); + } + + $element['#tree'] = TRUE; + + // Determine the order of day, month, year in the site's chosen date format. + $format = variable_get('date_format_short', 'm/d/Y - H:i'); + $sort = array(); + $sort['day'] = max(strpos($format, 'd'), strpos($format, 'j')); + $sort['month'] = max(strpos($format, 'm'), strpos($format, 'M')); + $sort['year'] = strpos($format, 'Y'); + asort($sort); + $order = array_keys($sort); + + // Output multi-selector for date. + foreach ($order as $type) { + switch ($type) { + case 'day': + $options = drupal_map_assoc(range(1, 31)); + $title = t('Day'); + break; + + case 'month': + $options = drupal_map_assoc(range(1, 12), 'map_month'); + $title = t('Month'); + break; + + case 'year': + $options = drupal_map_assoc(range(1900, 2050)); + $title = t('Year'); + break; + } + + $element[$type] = array( + '#type' => 'select', + '#title' => $title, + '#title_display' => 'invisible', + '#value' => $element['#value'][$type], + '#attributes' => $element['#attributes'], + '#options' => $options, + ); + } + + return $element; +} + +/** + * Validates the date type to stop dates like February 30, 2006. + */ +function date_validate($form) { + if (!checkdate($form['#value']['month'], $form['#value']['day'], $form['#value']['year'])) { + form_error($form, t('The specified date is invalid.')); + } +} + +/** + * Helper function for usage with drupal_map_assoc to display month names. + */ +function map_month($month) { + $months = &drupal_static(__FUNCTION__, array( + 1 => 'Jan', + 2 => 'Feb', + 3 => 'Mar', + 4 => 'Apr', + 5 => 'May', + 6 => 'Jun', + 7 => 'Jul', + 8 => 'Aug', + 9 => 'Sep', + 10 => 'Oct', + 11 => 'Nov', + 12 => 'Dec', + )); + return t($months[$month]); +} + +/** + * If no default value is set for weight select boxes, use 0. + */ +function weight_value(&$form) { + if (isset($form['#default_value'])) { + $form['#value'] = $form['#default_value']; + } + else { + $form['#value'] = 0; + } +} + +/** + * Roll out a single radios element to a list of radios, + * using the options array as index. + */ +function form_process_radios($element) { + if (count($element['#options']) > 0) { + $weight = 0; + foreach ($element['#options'] as $key => $choice) { + // Maintain order of options as defined in #options, in case the element + // defines custom option sub-elements, but does not define all option + // sub-elements. + $weight += 0.001; + + $element += array($key => array()); + // Generate the parents as the autogenerator does, so we will have a + // unique id for each radio button. + $parents_for_id = array_merge($element['#parents'], array($key)); + $element[$key] += array( + '#type' => 'radio', + '#title' => $choice, + // The key is sanitized in drupal_attributes() during output from the + // theme function. + '#return_value' => $key, + '#default_value' => isset($element['#default_value']) ? $element['#default_value'] : NULL, + '#attributes' => $element['#attributes'], + '#parents' => $element['#parents'], + '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)), + '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, + '#weight' => $weight, + ); + } + } + return $element; +} + +/** + * Returns HTML for a checkbox form element. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #value, #return_value, #description, #required, + * #attributes. + * + * @ingroup themeable + */ +function theme_checkbox($variables) { + $element = $variables['element']; + $t = get_t(); + $element['#attributes']['type'] = 'checkbox'; + element_set_attributes($element, array('id', 'name', '#return_value' => 'value')); + + // Unchecked checkbox has #value of integer 0. + if (!empty($element['#checked'])) { + $element['#attributes']['checked'] = 'checked'; + } + _form_set_class($element, array('form-checkbox')); + + return ''; +} + +/** + * Returns HTML for a set of checkbox form elements. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #children, #attributes. + * + * @ingroup themeable + */ +function theme_checkboxes($variables) { + $element = $variables['element']; + $attributes = array(); + if (isset($element['#id'])) { + $attributes['id'] = $element['#id']; + } + $attributes['class'][] = 'form-checkboxes'; + if (!empty($element['#attributes']['class'])) { + $attributes['class'] = array_merge($attributes['class'], $element['#attributes']['class']); + } + return '' . (!empty($element['#children']) ? $element['#children'] : '') . ''; +} + +/** + * Add form_element theming to an element if title or description is set. + * + * This is used as a pre render function for checkboxes and radios. + */ +function form_pre_render_conditional_form_element($element) { + // Set the element's title attribute to show #title as a tooltip, if needed. + if (isset($element['#title']) && $element['#title_display'] == 'attribute') { + $element['#attributes']['title'] = $element['#title']; + if (!empty($element['#required'])) { + // Append an indication that this field is required. + $element['#attributes']['title'] .= ' (' . $t('Required') . ')'; + } + } + + if (isset($element['#title']) || isset($element['#description'])) { + $element['#theme_wrappers'][] = 'form_element'; + } + return $element; +} + +/** + * Sets the #checked property of a checkbox element. + */ +function form_process_checkbox($element, $form_state) { + $value = $element['#value']; + $return_value = $element['#return_value']; + // On form submission, the #value of an available and enabled checked + // checkbox is #return_value, and the #value of an available and enabled + // unchecked checkbox is integer 0. On not submitted forms, and for + // checkboxes with #access=FALSE or #disabled=TRUE, the #value is + // #default_value (integer 0 if #default_value is NULL). Most of the time, + // a string comparison of #value and #return_value is sufficient for + // determining the "checked" state, but a value of TRUE always means checked + // (even if #return_value is 'foo'), and a value of FALSE or integer 0 always + // means unchecked (even if #return_value is '' or '0'). + if ($value === TRUE || $value === FALSE || $value === 0) { + $element['#checked'] = (bool) $value; + } + else { + // Compare as strings, so that 15 is not considered equal to '15foo', but 1 + // is considered equal to '1'. This cast does not imply that either #value + // or #return_value is expected to be a string. + $element['#checked'] = ((string) $value === (string) $return_value); + } + return $element; +} + +function form_process_checkboxes($element) { + $value = is_array($element['#value']) ? $element['#value'] : array(); + $element['#tree'] = TRUE; + if (count($element['#options']) > 0) { + if (!isset($element['#default_value']) || $element['#default_value'] == 0) { + $element['#default_value'] = array(); + } + $weight = 0; + foreach ($element['#options'] as $key => $choice) { + // Integer 0 is not a valid #return_value, so use '0' instead. + // @see form_type_checkbox_value(). + // @todo For Drupal 8, cast all integer keys to strings for consistency + // with form_process_radios(). + if ($key === 0) { + $key = '0'; + } + // Maintain order of options as defined in #options, in case the element + // defines custom option sub-elements, but does not define all option + // sub-elements. + $weight += 0.001; + + $element += array($key => array()); + $element[$key] += array( + '#type' => 'checkbox', + '#title' => $choice, + '#return_value' => $key, + '#default_value' => isset($value[$key]) ? $key : NULL, + '#attributes' => $element['#attributes'], + '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, + '#weight' => $weight, + ); + } + } + return $element; +} + +/** + * Processes a form actions container element. + * + * @param $element + * An associative array containing the properties and children of the + * form actions container. + * @param $form_state + * The $form_state array for the form this element belongs to. + * + * @return + * The processed element. + */ +function form_process_actions($element, &$form_state) { + $element['#attributes']['class'][] = 'form-actions'; + return $element; +} + +/** + * Processes a container element. + * + * @param $element + * An associative array containing the properties and children of the + * container. + * @param $form_state + * The $form_state array for the form this element belongs to. + * @return + * The processed element. + */ +function form_process_container($element, &$form_state) { + // Generate the ID of the element if it's not explicitly given. + if (!isset($element['#id'])) { + $element['#id'] = drupal_html_id(implode('-', $element['#parents']) . '-wrapper'); + } + return $element; +} + +/** + * Returns HTML to wrap child elements in a container. + * + * Used for grouped form items. Can also be used as a #theme_wrapper for any + * renderable element, to surround it with a
and add attributes such as + * classes or an HTML id. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #id, #attributes, #children. + * + * @ingroup themeable + */ +function theme_container($variables) { + $element = $variables['element']; + + // Special handling for form elements. + if (isset($element['#array_parents'])) { + // Assign an html ID. + if (!isset($element['#attributes']['id'])) { + $element['#attributes']['id'] = $element['#id']; + } + // Add the 'form-wrapper' class. + $element['#attributes']['class'][] = 'form-wrapper'; + } + + return '' . $element['#children'] . '
'; +} + +/** + * Returns HTML for a table with radio buttons or checkboxes. + * + * An example of per-row options: + * @code + * $options = array(); + * $options[0]['title'] = "A red row" + * $options[0]['#attributes'] = array ('class' => array('red-row')); + * $options[1]['title'] = "A blue row" + * $options[1]['#attributes'] = array ('class' => array('blue-row')); + * + * $form['myselector'] = array ( + * '#type' => 'tableselect', + * '#title' => 'My Selector' + * '#options' => $options, + * ); + * @endcode + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties and children of + * the tableselect element. Properties used: #header, #options, #empty, + * and #js_select. The #options property is an array of selection options; + * each array element of #options is an array of properties. These + * properties can include #attributes, which is added to the + * table row's HTML attributes; see theme_table(). + * + * @ingroup themeable + */ +function theme_tableselect($variables) { + $element = $variables['element']; + $rows = array(); + $header = $element['#header']; + if (!empty($element['#options'])) { + // Generate a table row for each selectable item in #options. + foreach (element_children($element) as $key) { + $row = array(); + + $row['data'] = array(); + if (isset($element['#options'][$key]['#attributes'])) { + $row += $element['#options'][$key]['#attributes']; + } + // Render the checkbox / radio element. + $row['data'][] = drupal_render($element[$key]); + + // As theme_table only maps header and row columns by order, create the + // correct order by iterating over the header fields. + foreach ($element['#header'] as $fieldname => $title) { + $row['data'][] = $element['#options'][$key][$fieldname]; + } + $rows[] = $row; + } + // Add an empty header or a "Select all" checkbox to provide room for the + // checkboxes/radios in the first table column. + if ($element['#js_select']) { + // Add a "Select all" checkbox. + drupal_add_js('core/misc/tableselect.js'); + array_unshift($header, array('class' => array('select-all'))); + } + else { + // Add an empty header when radio buttons are displayed or a "Select all" + // checkbox is not desired. + array_unshift($header, ''); + } + } + return theme('table', array('header' => $header, 'rows' => $rows, 'empty' => $element['#empty'], 'attributes' => $element['#attributes'])); +} + +/** + * Create the correct amount of checkbox or radio elements to populate the table. + * + * @param $element + * An associative array containing the properties and children of the + * tableselect element. + * @return + * The processed element. + */ +function form_process_tableselect($element) { + + if ($element['#multiple']) { + $value = is_array($element['#value']) ? $element['#value'] : array(); + } + else { + // Advanced selection behaviour make no sense for radios. + $element['#js_select'] = FALSE; + } + + $element['#tree'] = TRUE; + + if (count($element['#options']) > 0) { + if (!isset($element['#default_value']) || $element['#default_value'] === 0) { + $element['#default_value'] = array(); + } + + // Create a checkbox or radio for each item in #options in such a way that + // the value of the tableselect element behaves as if it had been of type + // checkboxes or radios. + foreach ($element['#options'] as $key => $choice) { + // Do not overwrite manually created children. + if (!isset($element[$key])) { + if ($element['#multiple']) { + $title = ''; + if (!empty($element['#options'][$key]['title']['data']['#title'])) { + $title = t('Update @title', array( + '@title' => $element['#options'][$key]['title']['data']['#title'], + )); + } + $element[$key] = array( + '#type' => 'checkbox', + '#title' => $title, + '#title_display' => 'invisible', + '#return_value' => $key, + '#default_value' => isset($value[$key]) ? $key : NULL, + '#attributes' => $element['#attributes'], + ); + } + else { + // Generate the parents as the autogenerator does, so we will have a + // unique id for each radio button. + $parents_for_id = array_merge($element['#parents'], array($key)); + $element[$key] = array( + '#type' => 'radio', + '#title' => '', + '#return_value' => $key, + '#default_value' => ($element['#default_value'] == $key) ? $key : NULL, + '#attributes' => $element['#attributes'], + '#parents' => $element['#parents'], + '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)), + '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, + ); + } + if (isset($element['#options'][$key]['#weight'])) { + $element[$key]['#weight'] = $element['#options'][$key]['#weight']; + } + } + } + } + else { + $element['#value'] = array(); + } + return $element; +} + +/** + * Processes a machine-readable name form element. + * + * @param $element + * The form element to process. Properties used: + * - #machine_name: An associative array containing: + * - exists: A function name to invoke for checking whether a submitted + * machine name value already exists. The submitted value is passed as + * argument. In most cases, an existing API or menu argument loader + * function can be re-used. The callback is only invoked, if the submitted + * value differs from the element's #default_value. + * - source: (optional) The #array_parents of the form element containing + * the human-readable name (i.e., as contained in the $form structure) to + * use as source for the machine name. Defaults to array('name'). + * - label: (optional) A text to display as label for the machine name value + * after the human-readable name form element. Defaults to "Machine name". + * - replace_pattern: (optional) A regular expression (without delimiters) + * matching disallowed characters in the machine name. Defaults to + * '[^a-z0-9_]+'. + * - replace: (optional) A character to replace disallowed characters in the + * machine name via JavaScript. Defaults to '_' (underscore). When using a + * different character, 'replace_pattern' needs to be set accordingly. + * - error: (optional) A custom form error message string to show, if the + * machine name contains disallowed characters. + * - #maxlength: (optional) Should be set to the maximum allowed length of the + * machine name. Defaults to 64. + * - #disabled: (optional) Should be set to TRUE in case an existing machine + * name must not be changed after initial creation. + */ +function form_process_machine_name($element, &$form_state) { + // Apply default form element properties. + $element += array( + '#title' => t('Machine-readable name'), + '#description' => t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.'), + '#machine_name' => array(), + ); + // A form element that only wants to set one #machine_name property (usually + // 'source' only) would leave all other properties undefined, if the defaults + // were defined in hook_element_info(). Therefore, we apply the defaults here. + $element['#machine_name'] += array( + 'source' => array('name'), + 'target' => '#' . $element['#id'], + 'label' => t('Machine name'), + 'replace_pattern' => '[^a-z0-9_]+', + 'replace' => '_', + ); + + // The source element defaults to array('name'), but may have been overidden. + if (empty($element['#machine_name']['source'])) { + return $element; + } + + // Retrieve the form element containing the human-readable name from the + // complete form in $form_state. By reference, because we need to append + // a #field_suffix that will hold the live preview. + $key_exists = NULL; + $source = drupal_array_get_nested_value($form_state['complete form'], $element['#machine_name']['source'], $key_exists); + if (!$key_exists) { + return $element; + } + + // Append a field suffix to the source form element, which will contain + // the live preview of the machine name. + $suffix_id = $source['#id'] . '-machine-name-suffix'; + $source += array('#field_suffix' => ''); + $source['#field_suffix'] .= '  '; + + $parents = array_merge($element['#machine_name']['source'], array('#field_suffix')); + drupal_array_set_nested_value($form_state['complete form'], $parents, $source['#field_suffix']); + + $element['#machine_name']['suffix'] = '#' . $suffix_id; + + $js_settings = array( + 'type' => 'setting', + 'data' => array( + 'machineName' => array( + '#' . $source['#id'] => $element['#machine_name'], + ), + ), + ); + $element['#attached']['js'][] = 'core/misc/machine-name.js'; + $element['#attached']['js'][] = $js_settings; + + return $element; +} + +/** + * Form element validation handler for #type 'machine_name'. + * + * Note that #maxlength is validated by _form_validate() already. + */ +function form_validate_machine_name(&$element, &$form_state) { + // Verify that the machine name not only consists of replacement tokens. + if (preg_match('@^' . $element['#machine_name']['replace'] . '+$@', $element['#value'])) { + form_error($element, t('The machine-readable name must contain unique characters.')); + } + + // Verify that the machine name contains no disallowed characters. + if (preg_match('@' . $element['#machine_name']['replace_pattern'] . '@', $element['#value'])) { + if (!isset($element['#machine_name']['error'])) { + // Since a hyphen is the most common alternative replacement character, + // a corresponding validation error message is supported here. + if ($element['#machine_name']['replace'] == '-') { + form_error($element, t('The machine-readable name must contain only lowercase letters, numbers, and hyphens.')); + } + // Otherwise, we assume the default (underscore). + else { + form_error($element, t('The machine-readable name must contain only lowercase letters, numbers, and underscores.')); + } + } + else { + form_error($element, $element['#machine_name']['error']); + } + } + + // Verify that the machine name is unique. + if ($element['#default_value'] !== $element['#value']) { + $function = $element['#machine_name']['exists']; + if ($function($element['#value'], $element, $form_state)) { + form_error($element, t('The machine-readable name is already in use. It must be unique.')); + } + } +} + +/** + * Adds fieldsets to the specified group or adds group members to this + * fieldset. + * + * @param $element + * An associative array containing the properties and children of the + * fieldset. Note that $element must be taken by reference here, so processed + * child elements are taken over into $form_state. + * @param $form_state + * The $form_state array for the form this fieldset belongs to. + * @return + * The processed element. + */ +function form_process_fieldset(&$element, &$form_state) { + $parents = implode('][', $element['#parents']); + + // Each fieldset forms a new group. The #type 'vertical_tabs' basically only + // injects a new fieldset. + $form_state['groups'][$parents]['#group_exists'] = TRUE; + $element['#groups'] = &$form_state['groups']; + + // Process vertical tabs group member fieldsets. + if (isset($element['#group'])) { + // Add this fieldset to the defined group (by reference). + $group = $element['#group']; + $form_state['groups'][$group][] = &$element; + } + + // Contains form element summary functionalities. + $element['#attached']['library'][] = array('system', 'drupal.form'); + + // The .form-wrapper class is required for #states to treat fieldsets like + // containers. + if (!isset($element['#attributes']['class'])) { + $element['#attributes']['class'] = array(); + } + + // Collapsible fieldsets + if (!empty($element['#collapsible'])) { + $element['#attached']['library'][] = array('system', 'drupal.collapse'); + $element['#attributes']['class'][] = 'collapsible'; + if (!empty($element['#collapsed'])) { + $element['#attributes']['class'][] = 'collapsed'; + } + } + + return $element; +} + +/** + * Adds members of this group as actual elements for rendering. + * + * @param $element + * An associative array containing the properties and children of the + * fieldset. + * + * @return + * The modified element with all group members. + */ +function form_pre_render_fieldset($element) { + // Fieldsets may be rendered outside of a Form API context. + if (!isset($element['#parents']) || !isset($element['#groups'])) { + return $element; + } + // Inject group member elements belonging to this group. + $parents = implode('][', $element['#parents']); + $children = element_children($element['#groups'][$parents]); + if (!empty($children)) { + foreach ($children as $key) { + // Break references and indicate that the element should be rendered as + // group member. + $child = (array) $element['#groups'][$parents][$key]; + $child['#group_fieldset'] = TRUE; + // Inject the element as new child element. + $element[] = $child; + + $sort = TRUE; + } + // Re-sort the element's children if we injected group member elements. + if (isset($sort)) { + $element['#sorted'] = FALSE; + } + } + + if (isset($element['#group'])) { + $group = $element['#group']; + // If this element belongs to a group, but the group-holding element does + // not exist, we need to render it (at its original location). + if (!isset($element['#groups'][$group]['#group_exists'])) { + // Intentionally empty to clarify the flow; we simply return $element. + } + // If we injected this element into the group, then we want to render it. + elseif (!empty($element['#group_fieldset'])) { + // Intentionally empty to clarify the flow; we simply return $element. + } + // Otherwise, this element belongs to a group and the group exists, so we do + // not render it. + elseif (element_children($element['#groups'][$group])) { + $element['#printed'] = TRUE; + } + } + + return $element; +} + +/** + * Creates a group formatted as vertical tabs. + * + * @param $element + * An associative array containing the properties and children of the + * fieldset. + * @param $form_state + * The $form_state array for the form this vertical tab widget belongs to. + * @return + * The processed element. + */ +function form_process_vertical_tabs($element, &$form_state) { + // Inject a new fieldset as child, so that form_process_fieldset() processes + // this fieldset like any other fieldset. + $element['group'] = array( + '#type' => 'fieldset', + '#theme_wrappers' => array(), + '#parents' => $element['#parents'], + ); + + // The JavaScript stores the currently selected tab in this hidden + // field so that the active tab can be restored the next time the + // form is rendered, e.g. on preview pages or when form validation + // fails. + $name = implode('__', $element['#parents']); + if (isset($form_state['values'][$name . '__active_tab'])) { + $element['#default_tab'] = $form_state['values'][$name . '__active_tab']; + } + $element[$name . '__active_tab'] = array( + '#type' => 'hidden', + '#default_value' => $element['#default_tab'], + '#attributes' => array('class' => array('vertical-tabs-active-tab')), + ); + + return $element; +} + +/** + * Returns HTML for an element's children fieldsets as vertical tabs. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties and children of the + * fieldset. Properties used: #children. + * + * @ingroup themeable + */ +function theme_vertical_tabs($variables) { + $element = $variables['element']; + // Add required JavaScript and Stylesheet. + drupal_add_library('system', 'drupal.vertical-tabs'); + + $output = '

' . t('Vertical Tabs') . '

'; + $output .= '
' . $element['#children'] . '
'; + return $output; +} + +/** + * Returns HTML for a submit button form element. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #attributes, #button_type, #name, #value. + * + * @ingroup themeable + */ +function theme_submit($variables) { + return theme('button', $variables['element']); +} + +/** + * Returns HTML for a button form element. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #attributes, #button_type, #name, #value. + * + * @ingroup themeable + */ +function theme_button($variables) { + $element = $variables['element']; + $element['#attributes']['type'] = 'submit'; + element_set_attributes($element, array('id', 'name', 'value')); + + $element['#attributes']['class'][] = 'form-' . $element['#button_type']; + if (!empty($element['#attributes']['disabled'])) { + $element['#attributes']['class'][] = 'form-button-disabled'; + } + + return ''; +} + +/** + * Returns HTML for an image button form element. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #attributes, #button_type, #name, #value, #title, #src. + * + * @ingroup themeable + */ +function theme_image_button($variables) { + $element = $variables['element']; + $element['#attributes']['type'] = 'image'; + element_set_attributes($element, array('id', 'name', 'value')); + + $element['#attributes']['src'] = file_create_url($element['#src']); + if (!empty($element['#title'])) { + $element['#attributes']['alt'] = $element['#title']; + $element['#attributes']['title'] = $element['#title']; + } + + $element['#attributes']['class'][] = 'form-' . $element['#button_type']; + if (!empty($element['#attributes']['disabled'])) { + $element['#attributes']['class'][] = 'form-button-disabled'; + } + + return ''; +} + +/** + * Returns HTML for a hidden form element. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #name, #value, #attributes. + * + * @ingroup themeable + */ +function theme_hidden($variables) { + $element = $variables['element']; + $element['#attributes']['type'] = 'hidden'; + element_set_attributes($element, array('name', 'value')); + return '\n"; +} + +/** + * Returns HTML for a textfield form element. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #value, #description, #size, #maxlength, + * #required, #attributes, #autocomplete_path. + * + * @ingroup themeable + */ +function theme_textfield($variables) { + $element = $variables['element']; + $element['#attributes']['type'] = 'text'; + element_set_attributes($element, array('id', 'name', 'value', 'size', 'maxlength')); + _form_set_class($element, array('form-text')); + + $extra = ''; + if ($element['#autocomplete_path'] && drupal_valid_path($element['#autocomplete_path'])) { + drupal_add_library('system', 'drupal.autocomplete'); + $element['#attributes']['class'][] = 'form-autocomplete'; + + $attributes = array(); + $attributes['type'] = 'hidden'; + $attributes['id'] = $element['#attributes']['id'] . '-autocomplete'; + $attributes['value'] = url($element['#autocomplete_path'], array('absolute' => TRUE)); + $attributes['disabled'] = 'disabled'; + $attributes['class'][] = 'autocomplete'; + $extra = ''; + } + + $output = ''; + + return $output . $extra; +} + +/** + * Returns HTML for a form. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #action, #method, #attributes, #children + * + * @ingroup themeable + */ +function theme_form($variables) { + $element = $variables['element']; + if (isset($element['#action'])) { + $element['#attributes']['action'] = drupal_strip_dangerous_protocols($element['#action']); + } + element_set_attributes($element, array('method', 'id')); + if (empty($element['#attributes']['accept-charset'])) { + $element['#attributes']['accept-charset'] = "UTF-8"; + } + // Anonymous DIV to satisfy XHTML compliance. + return '
' . $element['#children'] . '
'; +} + +/** + * Returns HTML for a textarea form element. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #value, #description, #rows, #cols, #required, + * #attributes + * + * @ingroup themeable + */ +function theme_textarea($variables) { + $element = $variables['element']; + element_set_attributes($element, array('id', 'name', 'cols', 'rows')); + _form_set_class($element, array('form-textarea')); + + $wrapper_attributes = array( + 'class' => array('form-textarea-wrapper'), + ); + + // Add resizable behavior. + if (!empty($element['#resizable'])) { + drupal_add_library('system', 'drupal.textarea'); + $wrapper_attributes['class'][] = 'resizable'; + } + + $output = ''; + $output .= '' . check_plain($element['#value']) . ''; + $output .= ''; + return $output; +} + +/** + * Returns HTML for a password form element. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #value, #description, #size, #maxlength, + * #required, #attributes. + * + * @ingroup themeable + */ +function theme_password($variables) { + $element = $variables['element']; + $element['#attributes']['type'] = 'password'; + element_set_attributes($element, array('id', 'name', 'size', 'maxlength')); + _form_set_class($element, array('form-text')); + + return ''; +} + +/** + * Expand weight elements into selects. + */ +function form_process_weight($element) { + for ($n = (-1 * $element['#delta']); $n <= $element['#delta']; $n++) { + $weights[$n] = $n; + } + $element['#options'] = $weights; + $element['#type'] = 'select'; + $element['#is_weight'] = TRUE; + $element += element_info('select'); + return $element; +} + +/** + * Returns HTML for a file upload form element. + * + * For assistance with handling the uploaded file correctly, see the API + * provided by file.inc. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #name, #size, #description, #required, + * #attributes. + * + * @ingroup themeable + */ +function theme_file($variables) { + $element = $variables['element']; + $element['#attributes']['type'] = 'file'; + element_set_attributes($element, array('id', 'name', 'size')); + _form_set_class($element, array('form-file')); + + return ''; +} + +/** + * Returns HTML for a form element. + * + * Each form element is wrapped in a DIV container having the following CSS + * classes: + * - form-item: Generic for all form elements. + * - form-type-#type: The internal element #type. + * - form-item-#name: The internal form element #name (usually derived from the + * $form structure and set via form_builder()). + * - form-disabled: Only set if the form element is #disabled. + * + * In addition to the element itself, the DIV contains a label for the element + * based on the optional #title_display property, and an optional #description. + * + * The optional #title_display property can have these values: + * - before: The label is output before the element. This is the default. + * The label includes the #title and the required marker, if #required. + * - after: The label is output after the element. For example, this is used + * for radio and checkbox #type elements as set in system_element_info(). + * If the #title is empty but the field is #required, the label will + * contain only the required marker. + * - invisible: Labels are critical for screen readers to enable them to + * properly navigate through forms but can be visually distracting. This + * property hides the label for everyone except screen readers. + * - attribute: Set the title attribute on the element to create a tooltip + * but output no label element. This is supported only for checkboxes + * and radios in form_pre_render_conditional_form_element(). It is used + * where a visual label is not needed, such as a table of checkboxes where + * the row and column provide the context. The tooltip will include the + * title and required marker. + * + * If the #title property is not set, then the label and any required marker + * will not be output, regardless of the #title_display or #required values. + * This can be useful in cases such as the password_confirm element, which + * creates children elements that have their own labels and required markers, + * but the parent element should have neither. Use this carefully because a + * field without an associated label can cause accessibility challenges. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #title_display, #description, #id, #required, + * #children, #type, #name. + * + * @ingroup themeable + */ +function theme_form_element($variables) { + $element = &$variables['element']; + // This is also used in the installer, pre-database setup. + $t = get_t(); + + // This function is invoked as theme wrapper, but the rendered form element + // may not necessarily have been processed by form_builder(). + $element += array( + '#title_display' => 'before', + ); + + // Add element #id for #type 'item'. + if (isset($element['#markup']) && !empty($element['#id'])) { + $attributes['id'] = $element['#id']; + } + // Add element's #type and #name as class to aid with JS/CSS selectors. + $attributes['class'] = array('form-item'); + if (!empty($element['#type'])) { + $attributes['class'][] = 'form-type-' . strtr($element['#type'], '_', '-'); + } + if (!empty($element['#name'])) { + $attributes['class'][] = 'form-item-' . strtr($element['#name'], array(' ' => '-', '_' => '-', '[' => '-', ']' => '')); + } + // Add a class for disabled elements to facilitate cross-browser styling. + if (!empty($element['#attributes']['disabled'])) { + $attributes['class'][] = 'form-disabled'; + } + $output = '' . "\n"; + + // If #title is not set, we don't display any label or required marker. + if (!isset($element['#title'])) { + $element['#title_display'] = 'none'; + } + $prefix = isset($element['#field_prefix']) ? '' . $element['#field_prefix'] . ' ' : ''; + $suffix = isset($element['#field_suffix']) ? ' ' . $element['#field_suffix'] . '' : ''; + + switch ($element['#title_display']) { + case 'before': + case 'invisible': + $output .= ' ' . theme('form_element_label', $variables); + $output .= ' ' . $prefix . $element['#children'] . $suffix . "\n"; + break; + + case 'after': + $output .= ' ' . $prefix . $element['#children'] . $suffix; + $output .= ' ' . theme('form_element_label', $variables) . "\n"; + break; + + case 'none': + case 'attribute': + // Output no label and no required marker, only the children. + $output .= ' ' . $prefix . $element['#children'] . $suffix . "\n"; + break; + } + + if (!empty($element['#description'])) { + $output .= '
' . $element['#description'] . "
\n"; + } + + $output .= "\n"; + + return $output; +} + +/** + * Returns HTML for a marker for required form elements. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * + * @ingroup themeable + */ +function theme_form_required_marker($variables) { + // This is also used in the installer, pre-database setup. + $t = get_t(); + $attributes = array( + 'class' => 'form-required', + 'title' => $t('This field is required.'), + ); + return '*'; +} + +/** + * Returns HTML for a form element label and required marker. + * + * Form element labels include the #title and a #required marker. The label is + * associated with the element itself by the element #id. Labels may appear + * before or after elements, depending on theme_form_element() and #title_display. + * + * This function will not be called for elements with no labels, depending on + * #title_display. For elements that have an empty #title and are not required, + * this function will output no label (''). For required elements that have an + * empty #title, this will output the required marker alone within the label. + * The label will use the #id to associate the marker with the field that is + * required. That is especially important for screenreader users to know + * which field is required. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #required, #title, #id, #value, #description. + * + * @ingroup themeable + */ +function theme_form_element_label($variables) { + $element = $variables['element']; + // This is also used in the installer, pre-database setup. + $t = get_t(); + + // If title and required marker are both empty, output no label. + if (empty($element['#title']) && empty($element['#required'])) { + return ''; + } + + // If the element is required, a required marker is appended to the label. + $required = !empty($element['#required']) ? theme('form_required_marker', array('element' => $element)) : ''; + + $title = filter_xss_admin($element['#title']); + + $attributes = array(); + // Style the label as class option to display inline with the element. + if ($element['#title_display'] == 'after') { + $attributes['class'] = 'option'; + } + // Show label only to screen readers to avoid disruption in visual flows. + elseif ($element['#title_display'] == 'invisible') { + $attributes['class'] = 'element-invisible'; + } + + if (!empty($element['#id'])) { + $attributes['for'] = $element['#id']; + } + + // The leading whitespace helps visually separate fields from inline labels. + return ' ' . $t('!title !required', array('!title' => $title, '!required' => $required)) . "\n"; +} + +/** + * Sets a form element's class attribute. + * + * Adds 'required' and 'error' classes as needed. + * + * @param $element + * The form element. + * @param $name + * Array of new class names to be added. + */ +function _form_set_class(&$element, $class = array()) { + if (!empty($class)) { + if (!isset($element['#attributes']['class'])) { + $element['#attributes']['class'] = array(); + } + $element['#attributes']['class'] = array_merge($element['#attributes']['class'], $class); + } + // This function is invoked from form element theme functions, but the + // rendered form element may not necessarily have been processed by + // form_builder(). + if (!empty($element['#required'])) { + $element['#attributes']['class'][] = 'required'; + } + if (isset($element['#parents']) && form_get_error($element)) { + $element['#attributes']['class'][] = 'error'; + } +} + +/** + * @} End of "defgroup form_api". + */ + +/** + * @defgroup batch Batch operations + * @{ + * Create and process batch operations. + * + * Functions allowing forms processing to be spread out over several page + * requests, thus ensuring that the processing does not get interrupted + * because of a PHP timeout, while allowing the user to receive feedback + * on the progress of the ongoing operations. + * + * The API is primarily designed to integrate nicely with the Form API + * workflow, but can also be used by non-Form API scripts (like update.php) + * or even simple page callbacks (which should probably be used sparingly). + * + * Example: + * @code + * $batch = array( + * 'title' => t('Exporting'), + * 'operations' => array( + * array('my_function_1', array($account->uid, 'story')), + * array('my_function_2', array()), + * ), + * 'finished' => 'my_finished_callback', + * 'file' => 'path_to_file_containing_myfunctions', + * ); + * batch_set($batch); + * // only needed if not inside a form _submit handler : + * batch_process(); + * @endcode + * + * Note: if the batch 'title', 'init_message', 'progress_message', or + * 'error_message' could contain any user input, it is the responsibility of + * the code calling batch_set() to sanitize them first with a function like + * check_plain() or filter_xss(). Furthermore, if the batch operation + * returns any user input in the 'results' or 'message' keys of $context, + * it must also sanitize them first. + * + * Sample batch operations: + * @code + * // Simple and artificial: load a node of a given type for a given user + * function my_function_1($uid, $type, &$context) { + * // The $context array gathers batch context information about the execution (read), + * // as well as 'return values' for the current operation (write) + * // The following keys are provided : + * // 'results' (read / write): The array of results gathered so far by + * // the batch processing, for the current operation to append its own. + * // 'message' (write): A text message displayed in the progress page. + * // The following keys allow for multi-step operations : + * // 'sandbox' (read / write): An array that can be freely used to + * // store persistent data between iterations. It is recommended to + * // use this instead of $_SESSION, which is unsafe if the user + * // continues browsing in a separate window while the batch is processing. + * // 'finished' (write): A float number between 0 and 1 informing + * // the processing engine of the completion level for the operation. + * // 1 (or no value explicitly set) means the operation is finished + * // and the batch processing can continue to the next operation. + * + * $node = node_load(array('uid' => $uid, 'type' => $type)); + * $context['results'][] = $node->nid . ' : ' . check_plain($node->title); + * $context['message'] = check_plain($node->title); + * } + * + * // More advanced example: multi-step operation - load all nodes, five by five + * function my_function_2(&$context) { + * if (empty($context['sandbox'])) { + * $context['sandbox']['progress'] = 0; + * $context['sandbox']['current_node'] = 0; + * $context['sandbox']['max'] = db_query('SELECT COUNT(DISTINCT nid) FROM {node}')->fetchField(); + * } + * $limit = 5; + * $result = db_select('node') + * ->fields('node', array('nid')) + * ->condition('nid', $context['sandbox']['current_node'], '>') + * ->orderBy('nid') + * ->range(0, $limit) + * ->execute(); + * foreach ($result as $row) { + * $node = node_load($row->nid, NULL, TRUE); + * $context['results'][] = $node->nid . ' : ' . check_plain($node->title); + * $context['sandbox']['progress']++; + * $context['sandbox']['current_node'] = $node->nid; + * $context['message'] = check_plain($node->title); + * } + * if ($context['sandbox']['progress'] != $context['sandbox']['max']) { + * $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; + * } + * } + * @endcode + * + * Sample 'finished' callback: + * @code + * function batch_test_finished($success, $results, $operations) { + * // The 'success' parameter means no fatal PHP errors were detected. All + * // other error management should be handled using 'results'. + * if ($success) { + * $message = format_plural(count($results), 'One post processed.', '@count posts processed.'); + * } + * else { + * $message = t('Finished with an error.'); + * } + * drupal_set_message($message); + * // Providing data for the redirected page is done through $_SESSION. + * foreach ($results as $result) { + * $items[] = t('Loaded node %title.', array('%title' => $result)); + * } + * $_SESSION['my_batch_results'] = $items; + * } + * @endcode + */ + +/** + * Opens a new batch. + * + * @param $batch + * An array defining the batch. The following keys can be used -- only + * 'operations' is required, and batch_init() provides default values for + * the messages. + * - 'operations': Array of function calls to be performed. + * Example: + * @code + * array( + * array('my_function_1', array($arg1)), + * array('my_function_2', array($arg2_1, $arg2_2)), + * ) + * @endcode + * - 'title': Title for the progress page. Only safe strings should be passed. + * Defaults to t('Processing'). + * - 'init_message': Message displayed while the processing is initialized. + * Defaults to t('Initializing.'). + * - 'progress_message': Message displayed while processing the batch. + * Available placeholders are @current, @remaining, @total, @percentage, + * @estimate and @elapsed. Defaults to t('Completed @current of @total.'). + * - 'error_message': Message displayed if an error occurred while processing + * the batch. Defaults to t('An error has occurred.'). + * - 'finished': Name of a function to be executed after the batch has + * completed. This should be used to perform any result massaging that + * may be needed, and possibly save data in $_SESSION for display after + * final page redirection. + * - 'file': Path to the file containing the definitions of the + * 'operations' and 'finished' functions, for instance if they don't + * reside in the main .module file. The path should be relative to + * base_path(), and thus should be built using drupal_get_path(). + * - 'css': Array of paths to CSS files to be used on the progress page. + * - 'url_options': options passed to url() when constructing redirect + * URLs for the batch. + * + * Operations are added as new batch sets. Batch sets are used to ensure + * clean code independence, ensuring that several batches submitted by + * different parts of the code (core / contrib modules) can be processed + * correctly while not interfering or having to cope with each other. Each + * batch set gets to specify his own UI messages, operates on its own set + * of operations and results, and triggers its own 'finished' callback. + * Batch sets are processed sequentially, with the progress bar starting + * fresh for every new set. + */ +function batch_set($batch_definition) { + if ($batch_definition) { + $batch =& batch_get(); + + // Initialize the batch if needed. + if (empty($batch)) { + $batch = array( + 'sets' => array(), + 'has_form_submits' => FALSE, + ); + } + + // Base and default properties for the batch set. + // Use get_t() to allow batches at install time. + $t = get_t(); + $init = array( + 'sandbox' => array(), + 'results' => array(), + 'success' => FALSE, + 'start' => 0, + 'elapsed' => 0, + ); + $defaults = array( + 'title' => $t('Processing'), + 'init_message' => $t('Initializing.'), + 'progress_message' => $t('Completed @current of @total.'), + 'error_message' => $t('An error has occurred.'), + 'css' => array(), + ); + $batch_set = $init + $batch_definition + $defaults; + + // Tweak init_message to avoid the bottom of the page flickering down after + // init phase. + $batch_set['init_message'] .= '
 '; + + // The non-concurrent workflow of batch execution allows us to save + // numberOfItems() queries by handling our own counter. + $batch_set['total'] = count($batch_set['operations']); + $batch_set['count'] = $batch_set['total']; + + // Add the set to the batch. + if (empty($batch['id'])) { + // The batch is not running yet. Simply add the new set. + $batch['sets'][] = $batch_set; + } + else { + // The set is being added while the batch is running. Insert the new set + // right after the current one to ensure execution order, and store its + // operations in a queue. + $index = $batch['current_set'] + 1; + $slice1 = array_slice($batch['sets'], 0, $index); + $slice2 = array_slice($batch['sets'], $index); + $batch['sets'] = array_merge($slice1, array($batch_set), $slice2); + _batch_populate_queue($batch, $index); + } + } +} + +/** + * Processes the batch. + * + * Unless the batch has been marked with 'progressive' = FALSE, the function + * issues a drupal_goto and thus ends page execution. + * + * This function is generally not needed in form submit handlers; + * Form API takes care of batches that were set during form submission. + * + * @param $redirect + * (optional) Path to redirect to when the batch has finished processing. + * @param $url + * (optional - should only be used for separate scripts like update.php) + * URL of the batch processing page. + * @param $redirect_callback + * (optional) Specify a function to be called to redirect to the progressive + * processing page. By default drupal_goto() will be used to redirect to a + * page which will do the progressive page. Specifying another function will + * allow the progressive processing to be processed differently. + */ +function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = 'drupal_goto') { + $batch =& batch_get(); + + drupal_theme_initialize(); + + if (isset($batch)) { + // Add process information + $process_info = array( + 'current_set' => 0, + 'progressive' => TRUE, + 'url' => $url, + 'url_options' => array(), + 'source_url' => $_GET['q'], + 'redirect' => $redirect, + 'theme' => $GLOBALS['theme_key'], + 'redirect_callback' => $redirect_callback, + ); + $batch += $process_info; + + // The batch is now completely built. Allow other modules to make changes + // to the batch so that it is easier to reuse batch processes in other + // environments. + drupal_alter('batch', $batch); + + // Assign an arbitrary id: don't rely on a serial column in the 'batch' + // table, since non-progressive batches skip database storage completely. + $batch['id'] = db_next_id(); + + // Move operations to a job queue. Non-progressive batches will use a + // memory-based queue. + foreach ($batch['sets'] as $key => $batch_set) { + _batch_populate_queue($batch, $key); + } + + // Initiate processing. + if ($batch['progressive']) { + // Now that we have a batch id, we can generate the redirection link in + // the generic error message. + $t = get_t(); + $batch['error_message'] = $t('Please continue to the error page', array('@error_url' => url($url, array('query' => array('id' => $batch['id'], 'op' => 'finished'))))); + + // Clear the way for the drupal_goto() redirection to the batch processing + // page, by saving and unsetting the 'destination', if there is any. + if (isset($_GET['destination'])) { + $batch['destination'] = $_GET['destination']; + unset($_GET['destination']); + } + + // Store the batch. + db_insert('batch') + ->fields(array( + 'bid' => $batch['id'], + 'timestamp' => REQUEST_TIME, + 'token' => drupal_get_token($batch['id']), + 'batch' => serialize($batch), + )) + ->execute(); + + // Set the batch number in the session to guarantee that it will stay alive. + $_SESSION['batches'][$batch['id']] = TRUE; + + // Redirect for processing. + $function = $batch['redirect_callback']; + if (function_exists($function)) { + $function($batch['url'], array('query' => array('op' => 'start', 'id' => $batch['id']))); + } + } + else { + // Non-progressive execution: bypass the whole progressbar workflow + // and execute the batch in one pass. + require_once DRUPAL_ROOT . '/core/includes/batch.inc'; + _batch_process(); + } + } +} + +/** + * Retrieves the current batch. + */ +function &batch_get() { + // Not drupal_static(), because Batch API operates at a lower level than most + // use-cases for resetting static variables, and we specifically do not want a + // global drupal_static_reset() resetting the batch information. Functions + // that are part of the Batch API and need to reset the batch information may + // call batch_get() and manipulate the result by reference. Functions that are + // not part of the Batch API can also do this, but shouldn't. + static $batch = array(); + return $batch; +} + +/** + * Populates a job queue with the operations of a batch set. + * + * Depending on whether the batch is progressive or not, the BatchQueue or + * BatchMemoryQueue handler classes will be used. + * + * @param $batch + * The batch array. + * @param $set_id + * The id of the set to process. + * @return + * The name and class of the queue are added by reference to the batch set. + */ +function _batch_populate_queue(&$batch, $set_id) { + $batch_set = &$batch['sets'][$set_id]; + + if (isset($batch_set['operations'])) { + $batch_set += array( + 'queue' => array( + 'name' => 'drupal_batch:' . $batch['id'] . ':' . $set_id, + 'class' => $batch['progressive'] ? 'BatchQueue' : 'BatchMemoryQueue', + ), + ); + + $queue = _batch_queue($batch_set); + $queue->createQueue(); + foreach ($batch_set['operations'] as $operation) { + $queue->createItem($operation); + } + + unset($batch_set['operations']); + } +} + +/** + * Returns a queue object for a batch set. + * + * @param $batch_set + * The batch set. + * @return + * The queue object. + */ +function _batch_queue($batch_set) { + static $queues; + + // The class autoloader is not available when running update.php, so make + // sure the files are manually included. + if (!isset($queues)) { + $queues = array(); + require_once DRUPAL_ROOT . '/core/modules/system/system.queue.inc'; + require_once DRUPAL_ROOT . '/core/includes/batch.queue.inc'; + } + + if (isset($batch_set['queue'])) { + $name = $batch_set['queue']['name']; + $class = $batch_set['queue']['class']; + + if (!isset($queues[$class][$name])) { + $queues[$class][$name] = new $class($name); + } + return $queues[$class][$name]; + } +} + +/** + * @} End of "defgroup batch". + */ diff --git a/includes/graph.inc b/core/includes/graph.inc similarity index 100% rename from includes/graph.inc rename to core/includes/graph.inc diff --git a/includes/image.inc b/core/includes/image.inc similarity index 100% rename from includes/image.inc rename to core/includes/image.inc diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc new file mode 100644 index 0000000..4696446 --- /dev/null +++ b/core/includes/install.core.inc @@ -0,0 +1,1831 @@ + $interactive) + install_state_defaults(); + try { + // Begin the page request. This adds information about the current state of + // the Drupal installation to the passed-in array. + install_begin_request($install_state); + // Based on the installation state, run the remaining tasks for this page + // request, and collect any output. + $output = install_run_tasks($install_state); + } + catch (Exception $e) { + // When an installation error occurs, either send the error to the web + // browser or pass on the exception so the calling script can use it. + if ($install_state['interactive']) { + install_display_output($e->getMessage(), $install_state); + } + else { + throw $e; + } + } + // All available tasks for this page request are now complete. Interactive + // installations can send output to the browser or redirect the user to the + // next page. + if ($install_state['interactive']) { + if ($install_state['parameters_changed']) { + // Redirect to the correct page if the URL parameters have changed. + install_goto(install_redirect_url($install_state)); + } + elseif (isset($output)) { + // Display a page only if some output is available. Otherwise it is + // possible that we are printing a JSON page and theme output should + // not be shown. + install_display_output($output, $install_state); + } + } +} + +/** + * Returns an array of default settings for the global installation state. + * + * The installation state is initialized with these settings at the beginning + * of each page request. They may evolve during the page request, but they are + * initialized again once the next request begins. + * + * Non-interactive Drupal installations can override some of these default + * settings by passing in an array to the installation script, most notably + * 'parameters' (which contains one-time parameters such as 'profile' and + * 'locale' that are normally passed in via the URL) and 'forms' (which can + * be used to programmatically submit forms during the installation; the keys + * of each element indicate the name of the installation task that the form + * submission is for, and the values are used as the $form_state['values'] + * array that is passed on to the form submission via drupal_form_submit()). + * + * @see drupal_form_submit() + */ +function install_state_defaults() { + $defaults = array( + // The current task being processed. + 'active_task' => NULL, + // The last task that was completed during the previous installation + // request. + 'completed_task' => NULL, + // This becomes TRUE only when Drupal's system module is installed. + 'database_tables_exist' => FALSE, + // An array of forms to be programmatically submitted during the + // installation. The keys of each element indicate the name of the + // installation task that the form submission is for, and the values are + // used as the $form_state['values'] array that is passed on to the form + // submission via drupal_form_submit(). + 'forms' => array(), + // This becomes TRUE only at the end of the installation process, after + // all available tasks have been completed and Drupal is fully installed. + // It is used by the installer to store correct information in the database + // about the completed installation, as well as to inform theme functions + // that all tasks are finished (so that the task list can be displayed + // correctly). + 'installation_finished' => FALSE, + // Whether or not this installation is interactive. By default this will + // be set to FALSE if settings are passed in to install_drupal(). + 'interactive' => TRUE, + // An array of available languages for the installation. + 'locales' => array(), + // An array of parameters for the installation, pre-populated by the URL + // or by the settings passed in to install_drupal(). This is primarily + // used to store 'profile' (the name of the chosen installation profile) + // and 'locale' (the name of the chosen installation language), since + // these settings need to persist from page request to page request before + // the database is available for storage. + 'parameters' => array(), + // Whether or not the parameters have changed during the current page + // request. For interactive installations, this will trigger a page + // redirect. + 'parameters_changed' => FALSE, + // An array of information about the chosen installation profile. This will + // be filled in based on the profile's .info file. + 'profile_info' => array(), + // An array of available installation profiles. + 'profiles' => array(), + // An array of server variables that will be substituted into the global + // $_SERVER array via drupal_override_server_variables(). Used by + // non-interactive installations only. + 'server' => array(), + // This becomes TRUE only when a valid database connection can be + // established. + 'settings_verified' => FALSE, + // Installation tasks can set this to TRUE to force the page request to + // end (even if there is no themable output), in the case of an interactive + // installation. This is needed only rarely; for example, it would be used + // by an installation task that prints JSON output rather than returning a + // themed page. The most common example of this is during batch processing, + // but the Drupal installer automatically takes care of setting this + // parameter properly in that case, so that individual installation tasks + // which implement the batch API do not need to set it themselves. + 'stop_page_request' => FALSE, + // Installation tasks can set this to TRUE to indicate that the task should + // be run again, even if it normally wouldn't be. This can be used, for + // example, if a single task needs to be spread out over multiple page + // requests, or if it needs to perform some validation before allowing + // itself to be marked complete. The most common examples of this are batch + // processing and form submissions, but the Drupal installer automatically + // takes care of setting this parameter properly in those cases, so that + // individual installation tasks which implement the batch API or form API + // do not need to set it themselves. + 'task_not_complete' => FALSE, + // A list of installation tasks which have already been performed during + // the current page request. + 'tasks_performed' => array(), + ); + return $defaults; +} + +/** + * Begin an installation request, modifying the installation state as needed. + * + * This function performs commands that must run at the beginning of every page + * request. It throws an exception if the installation should not proceed. + * + * @param $install_state + * An array of information about the current installation state. This is + * modified with information gleaned from the beginning of the page request. + */ +function install_begin_request(&$install_state) { + // Add any installation parameters passed in via the URL. + $install_state['parameters'] += $_GET; + + // Validate certain core settings that are used throughout the installation. + if (!empty($install_state['parameters']['profile'])) { + $install_state['parameters']['profile'] = preg_replace('/[^a-zA-Z_0-9]/', '', $install_state['parameters']['profile']); + } + if (!empty($install_state['parameters']['locale'])) { + $install_state['parameters']['locale'] = preg_replace('/[^a-zA-Z_0-9\-]/', '', $install_state['parameters']['locale']); + } + + // Allow command line scripts to override server variables used by Drupal. + require_once DRUPAL_ROOT . '/core/includes/bootstrap.inc'; + if (!$install_state['interactive']) { + drupal_override_server_variables($install_state['server']); + } + + // 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 no + // installation be permitted using such a prefix. + if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], "simpletest") !== FALSE) { + header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); + exit; + } + + drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION); + + // This must go after drupal_bootstrap(), which unsets globals! + global $conf; + + require_once DRUPAL_ROOT . '/core/modules/system/system.install'; + require_once DRUPAL_ROOT . '/core/includes/common.inc'; + require_once DRUPAL_ROOT . '/core/includes/file.inc'; + require_once DRUPAL_ROOT . '/core/includes/install.inc'; + require_once DRUPAL_ROOT . '/' . variable_get('path_inc', 'core/includes/path.inc'); + + // Load module basics (needed for hook invokes). + include_once DRUPAL_ROOT . '/core/includes/module.inc'; + include_once DRUPAL_ROOT . '/core/includes/session.inc'; + + // Set up $language, so t() caller functions will still work. + drupal_language_initialize(); + + include_once DRUPAL_ROOT . '/core/includes/entity.inc'; + require_once DRUPAL_ROOT . '/core/includes/ajax.inc'; + $module_list['system']['filename'] = 'core/modules/system/system.module'; + $module_list['user']['filename'] = 'core/modules/user/user.module'; + module_list(TRUE, FALSE, FALSE, $module_list); + drupal_load('module', 'system'); + drupal_load('module', 'user'); + + // Load the cache infrastructure using a "fake" cache implementation that + // does not attempt to write to the database. We need this during the initial + // part of the installer because the database is not available yet. We + // continue to use it even when the database does become available, in order + // to preserve consistency between interactive and command-line installations + // (the latter complete in one page request and therefore are forced to + // continue using the cache implementation they started with) and also + // because any data put in the cache during the installer is inherently + // suspect, due to the fact that Drupal is not fully set up yet. + require_once DRUPAL_ROOT . '/core/includes/cache.inc'; + require_once DRUPAL_ROOT . '/core/includes/cache-install.inc'; + $conf['cache_default_class'] = 'DrupalFakeCache'; + + // Prepare for themed output. We need to run this at the beginning of the + // page request to avoid a different theme accidentally getting set. (We also + // need to run it even in the case of command-line installations, to prevent + // any code in the installer that happens to initialize the theme system from + // accessing the database before it is set up yet.) + drupal_maintenance_theme(); + + // Check existing settings.php. + $install_state['settings_verified'] = install_verify_settings(); + + if ($install_state['settings_verified']) { + // Initialize the database system. Note that the connection + // won't be initialized until it is actually requested. + require_once DRUPAL_ROOT . '/core/includes/database/database.inc'; + + // Verify the last completed task in the database, if there is one. + $task = install_verify_completed_task(); + } + else { + $task = NULL; + + // Since previous versions of Drupal stored database connection information + // in the 'db_url' variable, we should never let an installation proceed if + // this variable is defined and the settings file was not verified above + // (otherwise we risk installing over an existing site whose settings file + // has not yet been updated). + if (!empty($GLOBALS['db_url'])) { + throw new Exception(install_already_done_error()); + } + } + + // Modify the installation state as appropriate. + $install_state['completed_task'] = $task; + $install_state['database_tables_exist'] = !empty($task); +} + +/** + * Runs all tasks for the current installation request. + * + * In the case of an interactive installation, all tasks will be attempted + * until one is reached that has output which needs to be displayed to the + * user, or until a page redirect is required. Otherwise, tasks will be + * attempted until the installation is finished. + * + * @param $install_state + * An array of information about the current installation state. This is + * passed along to each task, so it can be modified if necessary. + * + * @return + * HTML output from the last completed task. + */ +function install_run_tasks(&$install_state) { + do { + // Obtain a list of tasks to perform. The list of tasks itself can be + // dynamic (e.g., some might be defined by the installation profile, + // which is not necessarily known until the earlier tasks have run), + // so we regenerate the remaining tasks based on the installation state, + // each time through the loop. + $tasks_to_perform = install_tasks_to_perform($install_state); + // Run the first task on the list. + reset($tasks_to_perform); + $task_name = key($tasks_to_perform); + $task = array_shift($tasks_to_perform); + $install_state['active_task'] = $task_name; + $original_parameters = $install_state['parameters']; + $output = install_run_task($task, $install_state); + $install_state['parameters_changed'] = ($install_state['parameters'] != $original_parameters); + // Store this task as having been performed during the current request, + // and save it to the database as completed, if we need to and if the + // database is in a state that allows us to do so. Also mark the + // installation as 'done' when we have run out of tasks. + if (!$install_state['task_not_complete']) { + $install_state['tasks_performed'][] = $task_name; + $install_state['installation_finished'] = empty($tasks_to_perform); + if ($install_state['database_tables_exist'] && ($task['run'] == INSTALL_TASK_RUN_IF_NOT_COMPLETED || $install_state['installation_finished'])) { + variable_set('install_task', $install_state['installation_finished'] ? 'done' : $task_name); + } + } + // Stop when there are no tasks left. In the case of an interactive + // installation, also stop if we have some output to send to the browser, + // the URL parameters have changed, or an end to the page request was + // specifically called for. + $finished = empty($tasks_to_perform) || ($install_state['interactive'] && (isset($output) || $install_state['parameters_changed'] || $install_state['stop_page_request'])); + } while (!$finished); + return $output; +} + +/** + * Runs an individual installation task. + * + * @param $task + * An array of information about the task to be run. + * @param $install_state + * An array of information about the current installation state. This is + * passed in by reference so that it can be modified by the task. + * + * @return + * The output of the task function, if there is any. + */ +function install_run_task($task, &$install_state) { + $function = $task['function']; + + if ($task['type'] == 'form') { + require_once DRUPAL_ROOT . '/core/includes/form.inc'; + if ($install_state['interactive']) { + // For interactive forms, build the form and ensure that it will not + // redirect, since the installer handles its own redirection only after + // marking the form submission task complete. + $form_state = array( + // We need to pass $install_state by reference in order for forms to + // modify it, since the form API will use it in call_user_func_array(), + // which requires that referenced variables be passed explicitly. + 'build_info' => array('args' => array(&$install_state)), + 'no_redirect' => TRUE, + ); + $form = drupal_build_form($function, $form_state); + // If a successful form submission did not occur, the form needs to be + // rendered, which means the task is not complete yet. + if (empty($form_state['executed'])) { + $install_state['task_not_complete'] = TRUE; + return drupal_render($form); + } + // Otherwise, return nothing so the next task will run in the same + // request. + return; + } + else { + // For non-interactive forms, submit the form programmatically with the + // values taken from the installation state. Throw an exception if any + // errors were encountered. + $form_state = array( + 'values' => !empty($install_state['forms'][$function]) ? $install_state['forms'][$function] : array(), + // We need to pass $install_state by reference in order for forms to + // modify it, since the form API will use it in call_user_func_array(), + // which requires that referenced variables be passed explicitly. + 'build_info' => array('args' => array(&$install_state)), + ); + drupal_form_submit($function, $form_state); + $errors = form_get_errors(); + if (!empty($errors)) { + throw new Exception(implode("\n", $errors)); + } + } + } + + elseif ($task['type'] == 'batch') { + // Start a new batch based on the task function, if one is not running + // already. + $current_batch = variable_get('install_current_batch'); + if (!$install_state['interactive'] || !$current_batch) { + $batch = $function($install_state); + if (empty($batch)) { + // If the task did some processing and decided no batch was necessary, + // there is nothing more to do here. + return; + } + batch_set($batch); + // For interactive batches, we need to store the fact that this batch + // task is currently running. Otherwise, we need to make sure the batch + // will complete in one page request. + if ($install_state['interactive']) { + variable_set('install_current_batch', $function); + } + else { + $batch =& batch_get(); + $batch['progressive'] = FALSE; + } + // Process the batch. For progressive batches, this will redirect. + // Otherwise, the batch will complete. + batch_process(install_redirect_url($install_state), install_full_redirect_url($install_state)); + } + // If we are in the middle of processing this batch, keep sending back + // any output from the batch process, until the task is complete. + elseif ($current_batch == $function) { + include_once DRUPAL_ROOT . '/core/includes/batch.inc'; + $output = _batch_page(); + // The task is complete when we try to access the batch page and receive + // FALSE in return, since this means we are at a URL where we are no + // longer requesting a batch ID. + if ($output === FALSE) { + // Return nothing so the next task will run in the same request. + variable_del('install_current_batch'); + return; + } + else { + // We need to force the page request to end if the task is not + // complete, since the batch API sometimes prints JSON output + // rather than returning a themed page. + $install_state['task_not_complete'] = $install_state['stop_page_request'] = TRUE; + return $output; + } + } + } + + else { + // For normal tasks, just return the function result, whatever it is. + return $function($install_state); + } +} + +/** + * Returns a list of tasks to perform during the current installation request. + * + * Note that the list of tasks can change based on the installation state as + * the page request evolves (for example, if an installation profile hasn't + * been selected yet, we don't yet know which profile tasks need to be run). + * + * @param $install_state + * An array of information about the current installation state. + * + * @return + * A list of tasks to be performed, with associated metadata. + */ +function install_tasks_to_perform($install_state) { + // Start with a list of all currently available tasks. + $tasks = install_tasks($install_state); + foreach ($tasks as $name => $task) { + // Remove any tasks that were already performed or that never should run. + // Also, if we started this page request with an indication of the last + // task that was completed, skip that task and all those that come before + // it, unless they are marked as always needing to run. + if ($task['run'] == INSTALL_TASK_SKIP || in_array($name, $install_state['tasks_performed']) || (!empty($install_state['completed_task']) && empty($completed_task_found) && $task['run'] != INSTALL_TASK_RUN_IF_REACHED)) { + unset($tasks[$name]); + } + if (!empty($install_state['completed_task']) && $name == $install_state['completed_task']) { + $completed_task_found = TRUE; + } + } + return $tasks; +} + +/** + * Returns a list of all tasks the installer currently knows about. + * + * This function will return tasks regardless of whether or not they are + * intended to run on the current page request. However, the list can change + * based on the installation state (for example, if an installation profile + * hasn't been selected yet, we don't yet know which profile tasks will be + * available). + * + * @param $install_state + * An array of information about the current installation state. + * + * @return + * A list of tasks, with associated metadata. + */ +function install_tasks($install_state) { + // Determine whether translation import tasks will need to be performed. + $needs_translations = count($install_state['locales']) > 1 && !empty($install_state['parameters']['locale']) && $install_state['parameters']['locale'] != 'en'; + + // Start with the core installation tasks that run before handing control + // to the install profile. + $tasks = array( + 'install_select_profile' => array( + 'display_name' => st('Choose profile'), + 'display' => count($install_state['profiles']) != 1, + 'run' => INSTALL_TASK_RUN_IF_REACHED, + ), + 'install_select_locale' => array( + 'display_name' => st('Choose language'), + 'run' => INSTALL_TASK_RUN_IF_REACHED, + ), + 'install_load_profile' => array( + 'run' => INSTALL_TASK_RUN_IF_REACHED, + ), + 'install_verify_requirements' => array( + 'display_name' => st('Verify requirements'), + ), + 'install_settings_form' => array( + 'display_name' => st('Set up database'), + 'type' => 'form', + 'run' => $install_state['settings_verified'] ? INSTALL_TASK_SKIP : INSTALL_TASK_RUN_IF_NOT_COMPLETED, + ), + 'install_system_module' => array( + ), + 'install_bootstrap_full' => array( + 'run' => INSTALL_TASK_RUN_IF_REACHED, + ), + 'install_profile_modules' => array( + 'display_name' => count($install_state['profiles']) == 1 ? st('Install site') : st('Install profile'), + 'type' => 'batch', + ), + 'install_import_locales' => array( + 'display_name' => st('Set up translations'), + 'display' => $needs_translations, + 'type' => 'batch', + 'run' => $needs_translations ? INSTALL_TASK_RUN_IF_NOT_COMPLETED : INSTALL_TASK_SKIP, + ), + 'install_configure_form' => array( + 'display_name' => st('Configure site'), + 'type' => 'form', + ), + ); + + // Now add any tasks defined by the installation profile. + if (!empty($install_state['parameters']['profile'])) { + $function = $install_state['parameters']['profile'] . '_install_tasks'; + if (function_exists($function)) { + $result = $function($install_state); + if (is_array($result)) { + $tasks += $result; + } + } + } + + // Finish by adding the remaining core tasks. + $tasks += array( + 'install_import_locales_remaining' => array( + 'display_name' => st('Finish translations'), + 'display' => $needs_translations, + 'type' => 'batch', + 'run' => $needs_translations ? INSTALL_TASK_RUN_IF_NOT_COMPLETED : INSTALL_TASK_SKIP, + ), + 'install_finished' => array( + 'display_name' => st('Finished'), + ), + ); + + // Allow the installation profile to modify the full list of tasks. + if (!empty($install_state['parameters']['profile'])) { + $profile_file = DRUPAL_ROOT . '/profiles/' . $install_state['parameters']['profile'] . '/' . $install_state['parameters']['profile'] . '.profile'; + if (is_file($profile_file)) { + include_once $profile_file; + $function = $install_state['parameters']['profile'] . '_install_tasks_alter'; + if (function_exists($function)) { + $function($tasks, $install_state); + } + } + } + + // Fill in default parameters for each task before returning the list. + foreach ($tasks as $task_name => &$task) { + $task += array( + 'display_name' => NULL, + 'display' => !empty($task['display_name']), + 'type' => 'normal', + 'run' => INSTALL_TASK_RUN_IF_NOT_COMPLETED, + 'function' => $task_name, + ); + } + return $tasks; +} + +/** + * Returns a list of tasks that should be displayed to the end user. + * + * The output of this function is a list suitable for sending to + * theme_task_list(). + * + * @param $install_state + * An array of information about the current installation state. + * + * @return + * A list of tasks, with keys equal to the machine-readable task name and + * values equal to the name that should be displayed. + * + * @see theme_task_list() + */ +function install_tasks_to_display($install_state) { + $displayed_tasks = array(); + foreach (install_tasks($install_state) as $name => $task) { + if ($task['display']) { + $displayed_tasks[$name] = $task['display_name']; + } + } + return $displayed_tasks; +} + +/** + * Returns the URL that should be redirected to during an installation request. + * + * The output of this function is suitable for sending to install_goto(). + * + * @param $install_state + * An array of information about the current installation state. + * + * @return + * The URL to redirect to. + * + * @see install_full_redirect_url() + */ +function install_redirect_url($install_state) { + return 'core/install.php?' . drupal_http_build_query($install_state['parameters']); +} + +/** + * Returns the complete URL redirected to during an installation request. + * + * @param $install_state + * An array of information about the current installation state. + * + * @return + * The complete URL to redirect to. + * + * @see install_redirect_url() + */ +function install_full_redirect_url($install_state) { + global $base_url; + return $base_url . '/' . install_redirect_url($install_state); +} + +/** + * Displays themed installer output and ends the page request. + * + * Installation tasks should use drupal_set_title() to set the desired page + * title, but otherwise this function takes care of theming the overall page + * output during every step of the installation. + * + * @param $output + * The content to display on the main part of the page. + * @param $install_state + * An array of information about the current installation state. + */ +function install_display_output($output, $install_state) { + drupal_page_header(); + // Only show the task list if there is an active task; otherwise, the page + // request has ended before tasks have even been started, so there is nothing + // meaningful to show. + if (isset($install_state['active_task'])) { + // Let the theming function know when every step of the installation has + // been completed. + $active_task = $install_state['installation_finished'] ? NULL : $install_state['active_task']; + drupal_add_region_content('sidebar_first', theme('task_list', array('items' => install_tasks_to_display($install_state), 'active' => $active_task))); + } + print theme('install_page', array('content' => $output)); + exit; +} + +/** + * Installation task; verify the requirements for installing Drupal. + * + * @param $install_state + * An array of information about the current installation state. + * + * @return + * A themed status report, or an exception if there are requirement errors. + * Otherwise, no output is returned, so that the next task can be run + * in the same page request. + */ +function install_verify_requirements(&$install_state) { + // Check the installation requirements for Drupal and this profile. + $requirements = install_check_requirements($install_state); + + // Verify existence of all required modules. + $requirements += drupal_verify_profile($install_state); + + // Check the severity of the requirements reported. + $severity = drupal_requirements_severity($requirements); + + if ($severity == REQUIREMENT_ERROR) { + if ($install_state['interactive']) { + drupal_set_title(st('Requirements problem')); + $status_report = theme('status_report', array('requirements' => $requirements)); + $status_report .= st('Check the error messages and proceed with the installation.', array('!url' => check_url(request_uri()))); + return $status_report; + } + else { + // Throw an exception showing all unmet requirements. + $failures = array(); + foreach ($requirements as $requirement) { + if (isset($requirement['severity']) && $requirement['severity'] == REQUIREMENT_ERROR) { + $failures[] = $requirement['title'] . ': ' . $requirement['value'] . "\n\n" . $requirement['description']; + } + } + throw new Exception(implode("\n\n", $failures)); + } + } +} + +/** + * Installation task; install the Drupal system module. + * + * @param $install_state + * An array of information about the current installation state. + */ +function install_system_module(&$install_state) { + // Install system.module. + drupal_install_system(); + + // Enable the user module so that sessions can be recorded during the + // upcoming bootstrap step. + module_enable(array('user'), FALSE); + + // Save the list of other modules to install for the upcoming tasks. + // variable_set() can be used now that system.module is installed. + $modules = $install_state['profile_info']['dependencies']; + + // The install profile is also a module, which needs to be installed + // after all the dependencies have been installed. + $modules[] = drupal_get_profile(); + + variable_set('install_profile_modules', array_diff($modules, array('system'))); + $install_state['database_tables_exist'] = TRUE; +} + +/** + * Verify and return the last installation task that was completed. + * + * @return + * The last completed task, if there is one. An exception is thrown if Drupal + * is already installed. + */ +function install_verify_completed_task() { + try { + if ($result = db_query("SELECT value FROM {variable} WHERE name = :name", array('name' => 'install_task'))) { + $task = unserialize($result->fetchField()); + } + } + // Do not trigger an error if the database query fails, since the database + // might not be set up yet. + catch (Exception $e) { + } + if (isset($task)) { + if ($task == 'done') { + throw new Exception(install_already_done_error()); + } + return $task; + } +} + +/** + * Verifies the existing settings in settings.php. + */ +function install_verify_settings() { + global $databases; + + // Verify existing settings (if any). + if (!empty($databases) && install_verify_pdo()) { + $database = $databases['default']['default']; + drupal_static_reset('conf_path'); + $settings_file = './' . conf_path(FALSE) . '/settings.php'; + $errors = install_database_errors($database, $settings_file); + if (empty($errors)) { + return TRUE; + } + } + return FALSE; +} + +/** + * Verify PDO library. + */ +function install_verify_pdo() { + // PDO was moved to PHP core in 5.2.0, but the old extension (targeting 5.0 + // and 5.1) is still available from PECL, and can still be built without + // errors. To verify that the correct version is in use, we check the + // PDO::ATTR_DEFAULT_FETCH_MODE constant, which is not available in the + // PECL extension. + return extension_loaded('pdo') && defined('PDO::ATTR_DEFAULT_FETCH_MODE'); +} + +/** + * Installation task; define a form to configure and rewrite settings.php. + * + * @param $form_state + * An associative array containing the current state of the form. + * @param $install_state + * An array of information about the current installation state. + * + * @return + * The form API definition for the database configuration form. + */ +function install_settings_form($form, &$form_state, &$install_state) { + global $databases; + $profile = $install_state['parameters']['profile']; + $install_locale = $install_state['parameters']['locale']; + + drupal_static_reset('conf_path'); + $conf_path = './' . conf_path(FALSE); + $settings_file = $conf_path . '/settings.php'; + $database = isset($databases['default']['default']) ? $databases['default']['default'] : array(); + + drupal_set_title(st('Database configuration')); + + $drivers = drupal_get_database_types(); + $drivers_keys = array_keys($drivers); + + $form['driver'] = array( + '#type' => 'radios', + '#title' => st('Database type'), + '#required' => TRUE, + '#default_value' => !empty($database['driver']) ? $database['driver'] : current($drivers_keys), + '#description' => st('The type of database your @drupal data will be stored in.', array('@drupal' => drupal_install_profile_distribution_name())), + ); + if (count($drivers) == 1) { + $form['driver']['#disabled'] = TRUE; + $form['driver']['#description'] .= ' ' . st('Your PHP configuration only supports a single database type, so it has been automatically selected.'); + } + + // Add driver specific configuration options. + foreach ($drivers as $key => $driver) { + $form['driver']['#options'][$key] = $driver->name(); + + $form['settings'][$key] = $driver->getFormOptions($database); + $form['settings'][$key]['#prefix'] = '

' . st('@driver_name settings', array('@driver_name' => $driver->name())) . '

'; + $form['settings'][$key]['#type'] = 'container'; + $form['settings'][$key]['#tree'] = TRUE; + $form['settings'][$key]['advanced_options']['#parents'] = array($key); + $form['settings'][$key]['#states'] = array( + 'visible' => array( + ':input[name=driver]' => array('value' => $key), + ) + ); + } + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['save'] = array( + '#type' => 'submit', + '#value' => st('Save and continue'), + '#limit_validation_errors' => array( + array('driver'), + array(isset($form_state['input']['driver']) ? $form_state['input']['driver'] : current($drivers_keys)), + ), + '#submit' => array('install_settings_form_submit'), + ); + + $form['errors'] = array(); + $form['settings_file'] = array('#type' => 'value', '#value' => $settings_file); + + return $form; +} + +/** + * Form API validate for install_settings form. + */ +function install_settings_form_validate($form, &$form_state) { + $driver = $form_state['values']['driver']; + $database = $form_state['values'][$driver]; + $database['driver'] = $driver; + + // TODO: remove when PIFR will be updated to use 'db_prefix' instead of + // 'prefix' in the database settings form. + $database['prefix'] = $database['db_prefix']; + unset($database['db_prefix']); + + $form_state['storage']['database'] = $database; + $errors = install_database_errors($database, $form_state['values']['settings_file']); + foreach ($errors as $name => $message) { + form_set_error($name, $message); + } +} + +/** + * Checks a database connection and returns any errors. + */ +function install_database_errors($database, $settings_file) { + global $databases; + $errors = array(); + + // Check database type. + $database_types = drupal_get_database_types(); + $driver = $database['driver']; + if (!isset($database_types[$driver])) { + $errors['driver'] = st("In your %settings_file file you have configured @drupal to use a %driver server, however your PHP installation currently does not support this database type.", array('%settings_file' => $settings_file, '@drupal' => drupal_install_profile_distribution_name(), '%driver' => $driver)); + } + else { + // Run driver specific validation + $errors += $database_types[$driver]->validateDatabaseSettings($database); + + // Run tasks associated with the database type. Any errors are caught in the + // calling function. + $databases['default']['default'] = $database; + // Just changing the global doesn't get the new information processed. + // We tell tell the Database class to re-parse $databases. + Database::parseConnectionInfo(); + + try { + db_run_tasks($driver); + } + catch (DatabaseTaskException $e) { + // These are generic errors, so we do not have any specific key of the + // database connection array to attach them to; therefore, we just put + // them in the error array with standard numeric keys. + $errors[$driver . '][0'] = $e->getMessage(); + } + } + return $errors; +} + +/** + * Form API submit for install_settings form. + */ +function install_settings_form_submit($form, &$form_state) { + global $install_state; + + // Update global settings array and save. + $settings['databases'] = array( + 'value' => array('default' => array('default' => $form_state['storage']['database'])), + 'required' => TRUE, + ); + $settings['drupal_hash_salt'] = array( + 'value' => drupal_hash_base64(drupal_random_bytes(55)), + 'required' => TRUE, + ); + drupal_rewrite_settings($settings); + // Indicate that the settings file has been verified, and check the database + // for the last completed task, now that we have a valid connection. This + // last step is important since we want to trigger an error if the new + // database already has Drupal installed. + $install_state['settings_verified'] = TRUE; + $install_state['completed_task'] = install_verify_completed_task(); +} + +/** + * Finds all .profile files. + */ +function install_find_profiles() { + return file_scan_directory('./profiles', '/\.profile$/', array('key' => 'name')); +} + +/** + * Installation task; select which profile to install. + * + * @param $install_state + * An array of information about the current installation state. The chosen + * profile will be added here, if it was not already selected previously, as + * will a list of all available profiles. + * + * @return + * For interactive installations, a form allowing the profile to be selected, + * if the user has a choice that needs to be made. Otherwise, an exception is + * thrown if a profile cannot be chosen automatically. + */ +function install_select_profile(&$install_state) { + $install_state['profiles'] += install_find_profiles(); + if (empty($install_state['parameters']['profile'])) { + // Try to find a profile. + $profile = _install_select_profile($install_state['profiles']); + if (empty($profile)) { + // We still don't have a profile, so display a form for selecting one. + // Only do this in the case of interactive installations, since this is + // not a real form with submit handlers (the database isn't even set up + // yet), rather just a convenience method for setting parameters in the + // URL. + if ($install_state['interactive']) { + include_once DRUPAL_ROOT . '/core/includes/form.inc'; + drupal_set_title(st('Select an installation profile')); + $form = drupal_get_form('install_select_profile_form', $install_state['profiles']); + return drupal_render($form); + } + else { + throw new Exception(install_no_profile_error()); + } + } + else { + $install_state['parameters']['profile'] = $profile; + } + } +} + +/** + * Helper function for automatically selecting an installation profile from a + * list or from a selection passed in via $_POST. + */ +function _install_select_profile($profiles) { + if (sizeof($profiles) == 0) { + throw new Exception(install_no_profile_error()); + } + // Don't need to choose profile if only one available. + if (sizeof($profiles) == 1) { + $profile = array_pop($profiles); + // TODO: is this right? + require_once DRUPAL_ROOT . '/' . $profile->uri; + return $profile->name; + } + else { + foreach ($profiles as $profile) { + if (!empty($_POST['profile']) && ($_POST['profile'] == $profile->name)) { + return $profile->name; + } + } + } +} + +/** + * Form API array definition for the profile selection form. + * + * @param $form_state + * Array of metadata about state of form processing. + * @param $profile_files + * Array of .profile files, as returned from file_scan_directory(). + */ +function install_select_profile_form($form, &$form_state, $profile_files) { + $profiles = array(); + $names = array(); + + foreach ($profile_files as $profile) { + // TODO: is this right? + include_once DRUPAL_ROOT . '/' . $profile->uri; + + $details = install_profile_info($profile->name); + // Don't show hidden profiles. This is used by to hide the testing profile, + // which only exists to speed up test runs. + if ($details['hidden'] === TRUE) { + continue; + } + $profiles[$profile->name] = $details; + + // Determine the name of the profile; default to file name if defined name + // is unspecified. + $name = isset($details['name']) ? $details['name'] : $profile->name; + $names[$profile->name] = $name; + } + + // Display radio buttons alphabetically by human-readable name, but always + // put the core profiles first (if they are present in the filesystem). + natcasesort($names); + if (isset($names['minimal'])) { + // If the expert ("Minimal") core profile is present, put it in front of + // any non-core profiles rather than including it with them alphabetically, + // since the other profiles might be intended to group together in a + // particular way. + $names = array('minimal' => $names['minimal']) + $names; + } + if (isset($names['standard'])) { + // If the default ("Standard") core profile is present, put it at the very + // top of the list. This profile will have its radio button pre-selected, + // so we want it to always appear at the top. + $names = array('standard' => $names['standard']) + $names; + } + + foreach ($names as $profile => $name) { + $form['profile'][$name] = array( + '#type' => 'radio', + '#value' => 'standard', + '#return_value' => $profile, + '#title' => $name, + '#description' => isset($profiles[$profile]['description']) ? $profiles[$profile]['description'] : '', + '#parents' => array('profile'), + ); + } + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => st('Save and continue'), + ); + return $form; +} + +/** + * Find all .po files for the current profile. + */ +function install_find_locales($profilename) { + $locales = file_scan_directory('./profiles/' . $profilename . '/translations', '/\.po$/', array('recurse' => FALSE)); + array_unshift($locales, (object) array('name' => 'en')); + foreach ($locales as $key => $locale) { + // The locale (file name) might be drupal-7.2.cs.po instead of cs.po. + $locales[$key]->langcode = preg_replace('!^(.+\.)?([^\.]+)$!', '\2', $locale->name); + // Language codes cannot exceed 12 characters to fit into the {languages} + // table. + if (strlen($locales[$key]->langcode) > 12) { + unset($locales[$key]); + } + } + return $locales; +} + +/** + * Installation task; select which locale to use for the current profile. + * + * @param $install_state + * An array of information about the current installation state. The chosen + * locale will be added here, if it was not already selected previously, as + * will a list of all available locales. + * + * @return + * For interactive installations, a form or other page output allowing the + * locale to be selected or providing information about locale selection, if + * a locale has not been chosen. Otherwise, an exception is thrown if a + * locale cannot be chosen automatically. + */ +function install_select_locale(&$install_state) { + // Find all available locales. + $profilename = $install_state['parameters']['profile']; + $locales = install_find_locales($profilename); + $install_state['locales'] += $locales; + + if (!empty($_POST['locale'])) { + foreach ($locales as $locale) { + if ($_POST['locale'] == $locale->langcode) { + $install_state['parameters']['locale'] = $locale->langcode; + return; + } + } + } + + if (empty($install_state['parameters']['locale'])) { + // If only the built-in (English) language is available, and we are + // performing an interactive installation, inform the user that the + // installer can be localized. Otherwise we assume the user knows what he + // is doing. + if (count($locales) == 1) { + if ($install_state['interactive']) { + drupal_set_title(st('Choose language')); + if (!empty($install_state['parameters']['localize'])) { + $output = '

Follow these steps to translate Drupal into your language:

'; + $output .= '
    '; + $output .= '
  1. Download a translation from the translation server.
  2. '; + $output .= '
  3. Place it into the following directory: +
    +/profiles/' . $profilename . '/translations/
    +
  4. '; + $output .= '
'; + $output .= '

For more information on installing Drupal in different languages, visit the drupal.org handbook page.

'; + $output .= '

How should the installation continue?

'; + $output .= ''; + } + else { + include_once DRUPAL_ROOT . '/core/includes/form.inc'; + $elements = drupal_get_form('install_select_locale_form', $locales, $profilename); + $output = drupal_render($elements); + } + return $output; + } + // One language, but not an interactive installation. Assume the user + // knows what he is doing. + $locale = current($locales); + $install_state['parameters']['locale'] = $locale->name; + return; + } + else { + // Allow profile to pre-select the language, skipping the selection. + $function = $profilename . '_profile_details'; + if (function_exists($function)) { + $details = $function(); + if (isset($details['language'])) { + foreach ($locales as $locale) { + if ($details['language'] == $locale->name) { + $install_state['parameters']['locale'] = $locale->name; + return; + } + } + } + } + + // We still don't have a locale, so display a form for selecting one. + // Only do this in the case of interactive installations, since this is + // not a real form with submit handlers (the database isn't even set up + // yet), rather just a convenience method for setting parameters in the + // URL. + if ($install_state['interactive']) { + drupal_set_title(st('Choose language')); + include_once DRUPAL_ROOT . '/core/includes/form.inc'; + $elements = drupal_get_form('install_select_locale_form', $locales, $profilename); + return drupal_render($elements); + } + else { + throw new Exception(st('Sorry, you must select a language to continue the installation.')); + } + } + } +} + +/** + * Form API array definition for language selection. + */ +function install_select_locale_form($form, &$form_state, $locales, $profilename) { + include_once DRUPAL_ROOT . '/core/includes/iso.inc'; + $languages = _locale_get_predefined_list(); + foreach ($locales as $locale) { + $name = $locale->langcode; + if (isset($languages[$name])) { + $name = $languages[$name][0] . (isset($languages[$name][1]) ? ' ' . st('(@language)', array('@language' => $languages[$name][1])) : ''); + } + $form['locale'][$locale->langcode] = array( + '#type' => 'radio', + '#return_value' => $locale->langcode, + '#default_value' => $locale->langcode == 'en' ? 'en' : '', + '#title' => $name . ($locale->langcode == 'en' ? ' ' . st('(built-in)') : ''), + '#parents' => array('locale') + ); + } + if (count($locales) == 1) { + $form['help'] = array( + '#markup' => '

' . st('Learn how to install Drupal in other languages') . '

', + ); + } + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => st('Save and continue'), + ); + return $form; +} + +/** + * Indicates that there are no profiles available. + */ +function install_no_profile_error() { + drupal_set_title(st('No profiles available')); + return st('We were unable to find any installation profiles. Installation profiles tell us what modules to enable and what schema to install in the database. A profile is necessary to continue with the installation process.'); +} + +/** + * Indicates that Drupal has already been installed. + */ +function install_already_done_error() { + global $base_url; + + drupal_set_title(st('Drupal already installed')); + return st('
  • To start over, you must empty your existing database.
  • To install to a different database, edit the appropriate settings.php file in the sites folder.
  • To upgrade an existing installation, proceed to the update script.
  • View your existing site.
', array('@base-url' => $base_url)); +} + +/** + * Installation task; load information about the chosen profile. + * + * @param $install_state + * An array of information about the current installation state. The loaded + * profile information will be added here, or an exception will be thrown if + * the profile cannot be loaded. + */ +function install_load_profile(&$install_state) { + $profile_file = DRUPAL_ROOT . '/profiles/' . $install_state['parameters']['profile'] . '/' . $install_state['parameters']['profile'] . '.profile'; + if (is_file($profile_file)) { + include_once $profile_file; + $install_state['profile_info'] = install_profile_info($install_state['parameters']['profile'], $install_state['parameters']['locale']); + } + else { + throw new Exception(st('Sorry, the profile you have chosen cannot be loaded.')); + } +} + +/** + * Installation task; perform a full bootstrap of Drupal. + * + * @param $install_state + * An array of information about the current installation state. + */ +function install_bootstrap_full(&$install_state) { + drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); +} + +/** + * Installation task; install required modules via a batch process. + * + * @param $install_state + * An array of information about the current installation state. + * + * @return + * The batch definition. + */ +function install_profile_modules(&$install_state) { + $modules = variable_get('install_profile_modules', array()); + $files = system_rebuild_module_data(); + variable_del('install_profile_modules'); + + // Always install required modules first. Respect the dependencies between + // the modules. + $required = array(); + $non_required = array(); + // Although the profile module is marked as required, it needs to go after + // every dependency, including non-required ones. So clear its required + // flag for now to allow it to install late. + $files[$install_state['parameters']['profile']]->info['required'] = FALSE; + // Add modules that other modules depend on. + foreach ($modules as $module) { + if ($files[$module]->requires) { + $modules = array_merge($modules, array_keys($files[$module]->requires)); + } + } + $modules = array_unique($modules); + foreach ($modules as $module) { + if (!empty($files[$module]->info['required'])) { + $required[$module] = $files[$module]->sort; + } + else { + $non_required[$module] = $files[$module]->sort; + } + } + arsort($required); + arsort($non_required); + + $operations = array(); + foreach ($required + $non_required as $module => $weight) { + $operations[] = array('_install_module_batch', array($module, $files[$module]->info['name'])); + } + $batch = array( + 'operations' => $operations, + 'title' => st('Installing @drupal', array('@drupal' => drupal_install_profile_distribution_name())), + 'error_message' => st('The installation has encountered an error.'), + 'finished' => '_install_profile_modules_finished', + ); + return $batch; +} + +/** + * Installation task; import languages via a batch process. + * + * @param $install_state + * An array of information about the current installation state. + * + * @return + * The batch definition, if there are language files to import. + */ +function install_import_locales(&$install_state) { + include_once DRUPAL_ROOT . '/core/includes/locale.inc'; + $install_locale = $install_state['parameters']['locale']; + + include_once DRUPAL_ROOT . '/core/includes/iso.inc'; + $predefined = _locale_get_predefined_list(); + if (!isset($predefined[$install_locale])) { + // Drupal does not know about this language, so we prefill its values with + // our best guess. The user will be able to edit afterwards. + locale_add_language($install_locale, $install_locale, $install_locale, LANGUAGE_LTR, '', '', TRUE, TRUE); + } + else { + // A known predefined language, details will be filled in properly. + locale_add_language($install_locale, NULL, NULL, NULL, '', '', TRUE, TRUE); + } + + // Collect files to import for this language. + $batch = locale_batch_by_language($install_locale, NULL); + if (!empty($batch)) { + // Remember components we cover in this batch set. + variable_set('install_locale_batch_components', $batch['#components']); + return $batch; + } +} + +/** + * Installation task; configure settings for the new site. + * + * @param $form_state + * An associative array containing the current state of the form. + * @param $install_state + * An array of information about the current installation state. + * + * @return + * The form API definition for the site configuration form. + */ +function install_configure_form($form, &$form_state, &$install_state) { + if (variable_get('site_name', FALSE) || variable_get('site_mail', FALSE)) { + // Site already configured: This should never happen, means re-running the + // installer, possibly by an attacker after the 'install_task' variable got + // accidentally blown somewhere. Stop it now. + throw new Exception(install_already_done_error()); + } + + drupal_set_title(st('Configure site')); + + // Warn about settings.php permissions risk + $settings_dir = conf_path(); + $settings_file = $settings_dir . '/settings.php'; + // Check that $_POST is empty so we only show this message when the form is + // first displayed, not on the next page after it is submitted. (We do not + // want to repeat it multiple times because it is a general warning that is + // not related to the rest of the installation process; it would also be + // especially out of place on the last page of the installer, where it would + // distract from the message that the Drupal installation has completed + // successfully.) + if (empty($_POST) && (!drupal_verify_install_file(DRUPAL_ROOT . '/' . $settings_file, FILE_EXIST|FILE_READABLE|FILE_NOT_WRITABLE) || !drupal_verify_install_file(DRUPAL_ROOT . '/' . $settings_dir, FILE_NOT_WRITABLE, 'dir'))) { + drupal_set_message(st('All necessary changes to %dir and %file have been made, so you should remove write permissions to them now in order to avoid security risks. If you are unsure how to do so, consult the online handbook.', array('%dir' => $settings_dir, '%file' => $settings_file, '@handbook_url' => 'http://drupal.org/server-permissions')), 'warning'); + } + + drupal_add_js(drupal_get_path('module', 'system') . '/system.js'); + // Add JavaScript time zone detection. + drupal_add_js('core/misc/timezone.js'); + // We add these strings as settings because JavaScript translation does not + // work on install time. + drupal_add_js(array('copyFieldValue' => array('edit-site-mail' => array('edit-account-mail'))), 'setting'); + drupal_add_js('jQuery(function () { Drupal.cleanURLsInstallCheck(); });', 'inline'); + // Add JS to show / hide the 'Email administrator about site updates' elements + drupal_add_js('jQuery(function () { Drupal.hideEmailAdministratorCheckbox() });', 'inline'); + // Build menu to allow clean URL check. + menu_rebuild(); + + // Cache a fully-built schema. This is necessary for any invocation of + // index.php because: (1) setting cache table entries requires schema + // information, (2) that occurs during bootstrap before any module are + // loaded, so (3) if there is no cached schema, drupal_get_schema() will + // try to generate one but with no loaded modules will return nothing. + // + // This logically could be done during the 'install_finished' task, but the + // clean URL check requires it now. + drupal_get_schema(NULL, TRUE); + + // Return the form. + return _install_configure_form($form, $form_state, $install_state); +} + +/** + * Installation task; import remaining languages via a batch process. + * + * @param $install_state + * An array of information about the current installation state. + * + * @return + * The batch definition, if there are language files to import. + */ +function install_import_locales_remaining(&$install_state) { + include_once DRUPAL_ROOT . '/core/includes/locale.inc'; + // Collect files to import for this language. Skip components already covered + // in the initial batch set. + $install_locale = $install_state['parameters']['locale']; + $batch = locale_batch_by_language($install_locale, NULL, variable_get('install_locale_batch_components', array())); + // Remove temporary variable. + variable_del('install_locale_batch_components'); + return $batch; +} + +/** + * Installation task; perform final steps and display a 'finished' page. + * + * @param $install_state + * An array of information about the current installation state. + * + * @return + * A message informing the user that the installation is complete. + */ +function install_finished(&$install_state) { + drupal_set_title(st('@drupal installation complete', array('@drupal' => drupal_install_profile_distribution_name())), PASS_THROUGH); + $messages = drupal_set_message(); + $output = '

' . st('Congratulations, you installed @drupal!', array('@drupal' => drupal_install_profile_distribution_name())) . '

'; + $output .= '

' . (isset($messages['error']) ? st('Review the messages above before visiting your new site.', array('@url' => url(''))) : st('Visit your new site.', array('@url' => url('')))) . '

'; + + // Flush all caches to ensure that any full bootstraps during the installer + // do not leave stale cached data, and that any content types or other items + // registered by the install profile are registered correctly. + drupal_flush_all_caches(); + + // Remember the profile which was used. + variable_set('install_profile', drupal_get_profile()); + + // Install profiles are always loaded last + db_update('system') + ->fields(array('weight' => 1000)) + ->condition('type', 'module') + ->condition('name', drupal_get_profile()) + ->execute(); + + // Cache a fully-built schema. + drupal_get_schema(NULL, TRUE); + + // Run cron to populate update status tables (if available) so that users + // will be warned if they've installed an out of date Drupal version. + // Will also trigger indexing of profile-supplied content or feeds. + drupal_cron_run(); + + return $output; +} + +/** + * Batch callback for batch installation of modules. + */ +function _install_module_batch($module, $module_name, &$context) { + // Install and enable the module right away, so that the module will be + // loaded by drupal_bootstrap in subsequent batch requests, and other + // modules possibly depending on it can safely perform their installation + // steps. + module_enable(array($module), FALSE); + $context['results'][] = $module; + $context['message'] = st('Installed %module module.', array('%module' => $module_name)); +} + +/** + * 'Finished' callback for module installation batch. + */ +function _install_profile_modules_finished($success, $results, $operations) { + // Flush all caches to complete the module installation process. Subsequent + // installation tasks will now have full access to the profile's modules. + drupal_flush_all_caches(); +} + +/** + * Checks installation requirements and reports any errors. + */ +function install_check_requirements($install_state) { + $profile = $install_state['parameters']['profile']; + + // Check the profile requirements. + $requirements = drupal_check_profile($profile); + + // If Drupal is not set up already, we need to create a settings file. + if (!$install_state['settings_verified']) { + $writable = FALSE; + $conf_path = './' . conf_path(FALSE, TRUE); + $settings_file = $conf_path . '/settings.php'; + $default_settings_file = './sites/default/default.settings.php'; + $file = $conf_path; + $exists = FALSE; + // Verify that the directory exists. + if (drupal_verify_install_file($conf_path, FILE_EXIST, 'dir')) { + // Check if a settings.php file already exists. + $file = $settings_file; + if (drupal_verify_install_file($settings_file, FILE_EXIST)) { + // If it does, make sure it is writable. + $writable = drupal_verify_install_file($settings_file, FILE_READABLE|FILE_WRITABLE); + $exists = TRUE; + } + } + + // If default.settings.php does not exist, or is not readable, throw an + // error. + if (!drupal_verify_install_file($default_settings_file, FILE_EXIST|FILE_READABLE)) { + $requirements['default settings file exists'] = array( + 'title' => st('Default settings file'), + 'value' => st('The default settings file does not exist.'), + 'severity' => REQUIREMENT_ERROR, + 'description' => st('The @drupal installer requires that the %default-file file not be modified in any way from the original download.', array('@drupal' => drupal_install_profile_distribution_name(), '%default-file' => $default_settings_file)), + ); + } + // Otherwise, if settings.php does not exist yet, we can try to copy + // default.settings.php to create it. + elseif (!$exists) { + $copied = drupal_verify_install_file($conf_path, FILE_EXIST|FILE_WRITABLE, 'dir') && @copy($default_settings_file, $settings_file); + if ($copied) { + // If the new settings file has the same owner as default.settings.php, + // this means default.settings.php is owned by the webserver user. + // This is an inherent security weakness because it allows a malicious + // webserver process to append arbitrary PHP code and then execute it. + // However, it is also a common configuration on shared hosting, and + // there is nothing Drupal can do to prevent it. In this situation, + // having settings.php also owned by the webserver does not introduce + // any additional security risk, so we keep the file in place. + if (fileowner($default_settings_file) === fileowner($settings_file)) { + $writable = drupal_verify_install_file($settings_file, FILE_READABLE|FILE_WRITABLE); + $exists = TRUE; + } + // If settings.php and default.settings.php have different owners, this + // probably means the server is set up "securely" (with the webserver + // running as its own user, distinct from the user who owns all the + // Drupal PHP files), although with either a group or world writable + // sites directory. Keeping settings.php owned by the webserver would + // therefore introduce a security risk. It would also cause a usability + // problem, since site owners who do not have root access to the file + // system would be unable to edit their settings file later on. We + // therefore must delete the file we just created and force the + // administrator to log on to the server and create it manually. + else { + $deleted = @drupal_unlink($settings_file); + // We expect deleting the file to be successful (since we just + // created it ourselves above), but if it fails somehow, we set a + // variable so we can display a one-time error message to the + // administrator at the bottom of the requirements list. We also try + // to make the file writable, to eliminate any conflicting error + // messages in the requirements list. + $exists = !$deleted; + if ($exists) { + $settings_file_ownership_error = TRUE; + $writable = drupal_verify_install_file($settings_file, FILE_READABLE|FILE_WRITABLE); + } + } + } + } + + // If settings.php does not exist, throw an error. + if (!$exists) { + $requirements['settings file exists'] = array( + 'title' => st('Settings file'), + 'value' => st('The settings file does not exist.'), + 'severity' => REQUIREMENT_ERROR, + 'description' => st('The @drupal installer requires that you create a settings file as part of the installation process. Copy the %default_file file to %file. More details about installing Drupal are available in INSTALL.txt.', array('@drupal' => drupal_install_profile_distribution_name(), '%file' => $file, '%default_file' => $default_settings_file, '@install_txt' => base_path() . 'core/INSTALL.txt')), + ); + } + else { + $requirements['settings file exists'] = array( + 'title' => st('Settings file'), + 'value' => st('The %file file exists.', array('%file' => $file)), + ); + // If settings.php is not writable, throw an error. + if (!$writable) { + $requirements['settings file writable'] = array( + 'title' => st('Settings file'), + 'value' => st('The settings file is not writable.'), + 'severity' => REQUIREMENT_ERROR, + 'description' => st('The @drupal installer requires write permissions to %file during the installation process. If you are unsure how to grant file permissions, consult the online handbook.', array('@drupal' => drupal_install_profile_distribution_name(), '%file' => $file, '@handbook_url' => 'http://drupal.org/server-permissions')), + ); + } + else { + $requirements['settings file'] = array( + 'title' => st('Settings file'), + 'value' => st('The settings file is writable.'), + ); + } + if (!empty($settings_file_ownership_error)) { + $requirements['settings file ownership'] = array( + 'title' => st('Settings file'), + 'value' => st('The settings file is owned by the web server.'), + 'severity' => REQUIREMENT_ERROR, + 'description' => st('The @drupal installer failed to create a settings file with proper file ownership. Log on to your web server, remove the existing %file file, and create a new one by copying the %default_file file to %file. More details about installing Drupal are available in INSTALL.txt. If you have problems with the file permissions on your server, consult the online handbook.', array('@drupal' => drupal_install_profile_distribution_name(), '%file' => $file, '%default_file' => $default_settings_file, '@install_txt' => base_path() . 'core/INSTALL.txt', '@handbook_url' => 'http://drupal.org/server-permissions')), + ); + } + } + } + return $requirements; +} + +/** + * Forms API array definition for site configuration. + */ +function _install_configure_form($form, &$form_state, &$install_state) { + include_once DRUPAL_ROOT . '/core/includes/locale.inc'; + + $form['site_information'] = array( + '#type' => 'fieldset', + '#title' => st('Site information'), + '#collapsible' => FALSE, + ); + $form['site_information']['site_name'] = array( + '#type' => 'textfield', + '#title' => st('Site name'), + '#required' => TRUE, + '#weight' => -20, + ); + $form['site_information']['site_mail'] = array( + '#type' => 'textfield', + '#title' => st('Site e-mail address'), + '#default_value' => ini_get('sendmail_from'), + '#description' => st("Automated e-mails, such as registration information, will be sent from this address. Use an address ending in your site's domain to help prevent these e-mails from being flagged as spam."), + '#required' => TRUE, + '#weight' => -15, + ); + $form['admin_account'] = array( + '#type' => 'fieldset', + '#title' => st('Site maintenance account'), + '#collapsible' => FALSE, + ); + + $form['admin_account']['account']['#tree'] = TRUE; + $form['admin_account']['account']['name'] = array('#type' => 'textfield', + '#title' => st('Username'), + '#maxlength' => USERNAME_MAX_LENGTH, + '#description' => st('Spaces are allowed; punctuation is not allowed except for periods, hyphens, and underscores.'), + '#required' => TRUE, + '#weight' => -10, + '#attributes' => array('class' => array('username')), + ); + + $form['admin_account']['account']['mail'] = array('#type' => 'textfield', + '#title' => st('E-mail address'), + '#maxlength' => EMAIL_MAX_LENGTH, + '#required' => TRUE, + '#weight' => -5, + ); + $form['admin_account']['account']['pass'] = array( + '#type' => 'password_confirm', + '#required' => TRUE, + '#size' => 25, + '#weight' => 0, + ); + + $form['server_settings'] = array( + '#type' => 'fieldset', + '#title' => st('Server settings'), + '#collapsible' => FALSE, + ); + + $countries = country_get_list(); + $form['server_settings']['site_default_country'] = array( + '#type' => 'select', + '#title' => st('Default country'), + '#empty_value' => '', + '#default_value' => variable_get('site_default_country', NULL), + '#options' => $countries, + '#description' => st('Select the default country for the site.'), + '#weight' => 0, + ); + + $form['server_settings']['date_default_timezone'] = array( + '#type' => 'select', + '#title' => st('Default time zone'), + '#default_value' => date_default_timezone_get(), + '#options' => system_time_zones(), + '#description' => st('By default, dates in this site will be displayed in the chosen time zone.'), + '#weight' => 5, + '#attributes' => array('class' => array('timezone-detect')), + ); + + $form['server_settings']['clean_url'] = array( + '#type' => 'hidden', + '#default_value' => 0, + '#attributes' => array('id' => 'edit-clean-url', 'class' => array('install')), + ); + + $form['update_notifications'] = array( + '#type' => 'fieldset', + '#title' => st('Update notifications'), + '#collapsible' => FALSE, + ); + $form['update_notifications']['update_status_module'] = array( + '#type' => 'checkboxes', + '#options' => array( + 1 => st('Check for updates automatically'), + 2 => st('Receive e-mail notifications'), + ), + '#default_value' => array(1, 2), + '#description' => st('The system will notify you when updates and important security releases are available for installed components. Anonymous information about your site is sent to Drupal.org.', array('@drupal' => 'http://drupal.org')), + '#weight' => 15, + ); + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => st('Save and continue'), + '#weight' => 15, + ); + + return $form; +} + +/** + * Forms API validate for the site configuration form. + */ +function install_configure_form_validate($form, &$form_state) { + if ($error = user_validate_name($form_state['values']['account']['name'])) { + form_error($form['admin_account']['account']['name'], $error); + } + if ($error = user_validate_mail($form_state['values']['account']['mail'])) { + form_error($form['admin_account']['account']['mail'], $error); + } + if ($error = user_validate_mail($form_state['values']['site_mail'])) { + form_error($form['site_information']['site_mail'], $error); + } +} + +/** + * Forms API submit for the site configuration form. + */ +function install_configure_form_submit($form, &$form_state) { + global $user; + + variable_set('site_name', $form_state['values']['site_name']); + variable_set('site_mail', $form_state['values']['site_mail']); + variable_set('date_default_timezone', $form_state['values']['date_default_timezone']); + variable_set('site_default_country', $form_state['values']['site_default_country']); + + // Enable update.module if this option was selected. + if ($form_state['values']['update_status_module'][1]) { + module_enable(array('update'), FALSE); + + // Add the site maintenance account's email address to the list of + // addresses to be notified when updates are available, if selected. + if ($form_state['values']['update_status_module'][2]) { + variable_set('update_notify_emails', array($form_state['values']['account']['mail'])); + } + } + + // We precreated user 1 with placeholder values. Let's save the real values. + $account = user_load(1); + $merge_data = array('init' => $form_state['values']['account']['mail'], 'roles' => !empty($account->roles) ? $account->roles : array(), 'status' => 1); + user_save($account, array_merge($form_state['values']['account'], $merge_data)); + // Load global $user and perform final login tasks. + $user = user_load(1); + user_login_finalize(); + + if (isset($form_state['values']['clean_url'])) { + variable_set('clean_url', $form_state['values']['clean_url']); + } + + // Record when this install ran. + variable_set('install_time', $_SERVER['REQUEST_TIME']); +} diff --git a/core/includes/install.inc b/core/includes/install.inc new file mode 100644 index 0000000..ce4253d --- /dev/null +++ b/core/includes/install.inc @@ -0,0 +1,1245 @@ + $schema_version) { + if ($schema_version > -1) { + module_load_install($module); + } + } +} + +/** + * Returns an array of available schema versions for a module. + * + * @param $module + * A module name. + * @return + * If the module has updates, an array of available updates sorted by version. + * Otherwise, FALSE. + */ +function drupal_get_schema_versions($module) { + $updates = &drupal_static(__FUNCTION__, NULL); + if (!isset($updates[$module])) { + $updates = array(); + + foreach (module_list() as $loaded_module) { + $updates[$loaded_module] = array(); + } + + // Prepare regular expression to match all possible defined hook_update_N(). + $regexp = '/^(?P.+)_update_(?P\d+)$/'; + $functions = get_defined_functions(); + // Narrow this down to functions ending with an integer, since all + // hook_update_N() functions end this way, and there are other + // possible functions which match '_update_'. We use preg_grep() here + // instead of foreaching through all defined functions, since the loop + // through all PHP functions can take significant page execution time + // and this function is called on every administrative page via + // system_requirements(). + foreach (preg_grep('/_\d+$/', $functions['user']) as $function) { + // If this function is a module update function, add it to the list of + // module updates. + if (preg_match($regexp, $function, $matches)) { + $updates[$matches['module']][] = $matches['version']; + } + } + // Ensure that updates are applied in numerical order. + foreach ($updates as &$module_updates) { + sort($module_updates, SORT_NUMERIC); + } + } + return empty($updates[$module]) ? FALSE : $updates[$module]; +} + +/** + * Returns the currently installed schema version for a module. + * + * @param $module + * A module name. + * @param $reset + * Set to TRUE after modifying the system table. + * @param $array + * Set to TRUE if you want to get information about all modules in the + * system. + * @return + * The currently installed schema version, or SCHEMA_UNINSTALLED if the + * module is not installed. + */ +function drupal_get_installed_schema_version($module, $reset = FALSE, $array = FALSE) { + static $versions = array(); + + if ($reset) { + $versions = array(); + } + + if (!$versions) { + $versions = array(); + $result = db_query("SELECT name, schema_version FROM {system} WHERE type = :type", array(':type' => 'module')); + foreach ($result as $row) { + $versions[$row->name] = $row->schema_version; + } + } + + if ($array) { + return $versions; + } + else { + return isset($versions[$module]) ? $versions[$module] : SCHEMA_UNINSTALLED; + } +} + +/** + * Update the installed version information for a module. + * + * @param $module + * A module name. + * @param $version + * The new schema version. + */ +function drupal_set_installed_schema_version($module, $version) { + db_update('system') + ->fields(array('schema_version' => $version)) + ->condition('name', $module) + ->execute(); + + // Reset the static cache of module schema versions. + drupal_get_installed_schema_version(NULL, TRUE); +} + +/** + * Loads the install profile, extracting its defined distribution name. + * + * @return + * The distribution name defined in the profile's .info file. Defaults to + * "Drupal" if none is explicitly provided by the install profile. + * + * @see install_profile_info() + */ +function drupal_install_profile_distribution_name() { + // During installation, the profile information is stored in the global + // installation state (it might not be saved anywhere yet). + if (drupal_installation_attempted()) { + global $install_state; + return $install_state['profile_info']['distribution_name']; + } + // At all other times, we load the profile via standard methods. + else { + $profile = drupal_get_profile(); + $info = system_get_info('module', $profile); + return $info['distribution_name']; + } +} + +/** + * Auto detect the base_url with PHP predefined variables. + * + * @param $file + * The name of the file calling this function so we can strip it out of + * the URI when generating the base_url. + * @return + * The auto-detected $base_url that should be configured in settings.php + */ +function drupal_detect_baseurl($file = 'core/install.php') { + $proto = $_SERVER['HTTPS'] ? 'https://' : 'http://'; + $host = $_SERVER['SERVER_NAME']; + $port = ($_SERVER['SERVER_PORT'] == 80 ? '' : ':' . $_SERVER['SERVER_PORT']); + $uri = preg_replace("/\?.*/", '', $_SERVER['REQUEST_URI']); + $dir = str_replace("/$file", '', $uri); + + return "$proto$host$port$dir"; +} + +/** + * Detect all supported databases that are compiled into PHP. + * + * @return + * An array of database types compiled into PHP. + */ +function drupal_detect_database_types() { + $databases = drupal_get_database_types(); + + foreach ($databases as $driver => $installer) { + $databases[$driver] = $installer->name(); + } + + return $databases; +} + +/** + * Return all supported database installer objects that are compiled into PHP. + * + * @return + * An array of database installer objects compiled into PHP. + */ +function drupal_get_database_types() { + $databases = array(); + + // We define a driver as a directory in /core/includes/database that in turn + // contains a database.inc file. That allows us to drop in additional drivers + // without modifying the installer. + // Because we have no registry yet, we need to also include the install.inc + // file for the driver explicitly. + require_once DRUPAL_ROOT . '/core/includes/database/database.inc'; + foreach (file_scan_directory(DRUPAL_ROOT . '/core/includes/database', '/^[a-z]*$/i', array('recurse' => FALSE)) as $file) { + if (file_exists($file->uri . '/database.inc') && file_exists($file->uri . '/install.inc')) { + $drivers[$file->filename] = $file->uri; + } + } + + foreach ($drivers as $driver => $file) { + $installer = db_installer_object($driver); + if ($installer->installable()) { + $databases[$driver] = $installer; + } + } + + // Usability: unconditionally put the MySQL driver on top. + if (isset($databases['mysql'])) { + $mysql_database = $databases['mysql']; + unset($databases['mysql']); + $databases = array('mysql' => $mysql_database) + $databases; + } + + return $databases; +} + +/** + * Database installer structure. + * + * Defines basic Drupal requirements for databases. + */ +abstract class DatabaseTasks { + + /** + * Structure that describes each task to run. + * + * @var array + * + * Each value of the tasks array is an associative array defining the function + * to call (optional) and any arguments to be passed to the function. + */ + protected $tasks = array( + array( + 'function' => 'checkEngineVersion', + 'arguments' => array(), + ), + array( + 'arguments' => array( + 'CREATE TABLE {drupal_install_test} (id int NULL)', + 'Drupal can use CREATE TABLE database commands.', + 'Failed to CREATE a test table on your database server with the command %query. The server reports the following message: %error.

Are you sure the configured username has the necessary permissions to create tables in the database?

', + TRUE, + ), + ), + array( + 'arguments' => array( + 'INSERT INTO {drupal_install_test} (id) VALUES (1)', + 'Drupal can use INSERT database commands.', + 'Failed to INSERT a value into a test table on your database server. We tried inserting a value with the command %query and the server reported the following error: %error.', + ), + ), + array( + 'arguments' => array( + 'UPDATE {drupal_install_test} SET id = 2', + 'Drupal can use UPDATE database commands.', + 'Failed to UPDATE a value in a test table on your database server. We tried updating a value with the command %query and the server reported the following error: %error.', + ), + ), + array( + 'arguments' => array( + 'DELETE FROM {drupal_install_test}', + 'Drupal can use DELETE database commands.', + 'Failed to DELETE a value from a test table on your database server. We tried deleting a value with the command %query and the server reported the following error: %error.', + ), + ), + array( + 'arguments' => array( + 'DROP TABLE {drupal_install_test}', + 'Drupal can use DROP TABLE database commands.', + 'Failed to DROP a test table from your database server. We tried dropping a table with the command %query and the server reported the following error %error.', + ), + ), + ); + + /** + * Results from tasks. + * + * @var array + */ + protected $results = array(); + + /** + * Ensure the PDO driver is supported by the version of PHP in use. + */ + protected function hasPdoDriver() { + return in_array($this->pdoDriver, PDO::getAvailableDrivers()); + } + + /** + * Assert test as failed. + */ + protected function fail($message) { + $this->results[$message] = FALSE; + } + + /** + * Assert test as a pass. + */ + protected function pass($message) { + $this->results[$message] = TRUE; + } + + /** + * Check whether Drupal is installable on the database. + */ + public function installable() { + return $this->hasPdoDriver() && empty($this->error); + } + + /** + * Return the human-readable name of the driver. + */ + abstract public function name(); + + /** + * Return the minimum required version of the engine. + * + * @return + * A version string. If not NULL, it will be checked against the version + * reported by the Database engine using version_compare(). + */ + public function minimumVersion() { + return NULL; + } + + /** + * Run database tasks and tests to see if Drupal can run on the database. + */ + public function runTasks() { + // We need to establish a connection before we can run tests. + if ($this->connect()) { + foreach ($this->tasks as $task) { + if (!isset($task['function'])) { + $task['function'] = 'runTestQuery'; + } + if (method_exists($this, $task['function'])) { + // Returning false is fatal. No other tasks can run. + if (FALSE === call_user_func_array(array($this, $task['function']), $task['arguments'])) { + break; + } + } + else { + throw new DatabaseTaskException(st("Failed to run all tasks against the database server. The task %task wasn't found.", array('%task' => $task['function']))); + } + } + } + // Check for failed results and compile message + $message = ''; + foreach ($this->results as $result => $success) { + if (!$success) { + $message .= '

' . $result . '

'; + } + } + if (!empty($message)) { + $message = '

In order for Drupal to work, and to continue with the installation process, you must resolve all issues reported below. For more help with configuring your database server, see the installation handbook. If you are unsure what any of this means you should probably contact your hosting provider.

' . $message; + throw new DatabaseTaskException($message); + } + } + + /** + * Check if we can connect to the database. + */ + protected function connect() { + try { + // This doesn't actually test the connection. + db_set_active(); + // Now actually do a check. + Database::getConnection(); + $this->pass('Drupal can CONNECT to the database ok.'); + } + catch (Exception $e) { + $this->fail(st('Failed to connect to your database server. The server reports the following message: %error.
  • Is the database server running?
  • Does the database exist, and have you entered the correct database name?
  • Have you entered the correct username and password?
  • Have you entered the correct database hostname?
', array('%error' => $e->getMessage()))); + return FALSE; + } + return TRUE; + } + + /** + * Run SQL tests to ensure the database can execute commands with the current user. + */ + protected function runTestQuery($query, $pass, $fail, $fatal = FALSE) { + try { + db_query($query); + $this->pass(st($pass)); + } + catch (Exception $e) { + $this->fail(st($fail, array('%query' => $query, '%error' => $e->getMessage(), '%name' => $this->name()))); + return !$fatal; + } + } + + /** + * Check the engine version. + */ + protected function checkEngineVersion() { + if ($this->minimumVersion() && version_compare(Database::getConnection()->version(), $this->minimumVersion(), '<')) { + $this->fail(st("The database version %version is less than the minimum required version %minimum_version.", array('%version' => Database::getConnection()->version(), '%minimum_version' => $this->minimumVersion()))); + } + } + + /** + * Return driver specific configuration options. + * + * @param $database + * An array of driver specific configuration options. + * + * @return + * The options form array. + */ + public function getFormOptions($database) { + $form['database'] = array( + '#type' => 'textfield', + '#title' => st('Database name'), + '#default_value' => empty($database['database']) ? '' : $database['database'], + '#size' => 45, + '#required' => TRUE, + '#description' => st('The name of the database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('@drupal' => drupal_install_profile_distribution_name())), + ); + + $form['username'] = array( + '#type' => 'textfield', + '#title' => st('Database username'), + '#default_value' => empty($database['username']) ? '' : $database['username'], + '#required' => TRUE, + '#size' => 45, + ); + + $form['password'] = array( + '#type' => 'password', + '#title' => st('Database password'), + '#default_value' => empty($database['password']) ? '' : $database['password'], + '#required' => FALSE, + '#size' => 45, + ); + + $form['advanced_options'] = array( + '#type' => 'fieldset', + '#title' => st('Advanced options'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#description' => st("These options are only necessary for some sites. If you're not sure what you should enter here, leave the default settings or check with your hosting provider."), + '#weight' => 10, + ); + + $profile = drupal_get_profile(); + $db_prefix = ($profile == 'standard') ? 'drupal_' : $profile . '_'; + $form['advanced_options']['db_prefix'] = array( + '#type' => 'textfield', + '#title' => st('Table prefix'), + '#default_value' => '', + '#size' => 45, + '#description' => st('If more than one application will be sharing this database, enter a table prefix such as %prefix for your @drupal site here.', array('@drupal' => drupal_install_profile_distribution_name(), '%prefix' => $db_prefix)), + '#weight' => 10, + ); + + $form['advanced_options']['host'] = array( + '#type' => 'textfield', + '#title' => st('Database host'), + '#default_value' => empty($database['host']) ? 'localhost' : $database['host'], + '#size' => 45, + // Hostnames can be 255 characters long. + '#maxlength' => 255, + '#required' => TRUE, + '#description' => st('If your database is located on a different server, change this.'), + ); + + $form['advanced_options']['port'] = array( + '#type' => 'textfield', + '#title' => st('Database port'), + '#default_value' => empty($database['port']) ? '' : $database['port'], + '#size' => 45, + // The maximum port number is 65536, 5 digits. + '#maxlength' => 5, + '#description' => st('If your database server is listening to a non-standard port, enter its number.'), + ); + + return $form; + } + + /** + * Validates driver specific configuration settings. + * + * Checks to ensure correct basic database settings and that a proper + * connection to the database can be established. + * + * @param $database + * An array of driver specific configuration options. + * + * @return + * An array of driver configuration errors, keyed by form element name. + */ + public function validateDatabaseSettings($database) { + $errors = array(); + + // Verify the table prefix. + if (!empty($database['prefix']) && is_string($database['prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['prefix'])) { + $errors[$database['driver'] . '][advanced_options][db_prefix'] = st('The database table prefix you have entered, %prefix, is invalid. The table prefix can only contain alphanumeric characters, periods, or underscores.', array('%prefix' => $database['prefix'])); + } + + // Verify the database port. + if (!empty($database['port']) && !is_numeric($database['port'])) { + $errors[$database['driver'] . '][advanced_options][port'] = st('Database port must be a number.'); + } + + return $errors; + } + +} + +/** + * Exception thrown if the database installer fails. + */ +class DatabaseTaskException extends Exception { +} + +/** + * Replace values in settings.php with values in the submitted array. + * + * @param $settings + * An array of settings that need to be updated. + */ +function drupal_rewrite_settings($settings = array(), $prefix = '') { + $default_settings = 'sites/default/default.settings.php'; + drupal_static_reset('conf_path'); + $settings_file = conf_path(FALSE) . '/' . $prefix . 'settings.php'; + + // Build list of setting names and insert the values into the global namespace. + $keys = array(); + foreach ($settings as $setting => $data) { + $GLOBALS[$setting] = $data['value']; + $keys[] = $setting; + } + + $buffer = NULL; + $first = TRUE; + if ($fp = fopen(DRUPAL_ROOT . '/' . $default_settings, 'r')) { + // Step line by line through settings.php. + while (!feof($fp)) { + $line = fgets($fp); + if ($first && substr($line, 0, 5) != ' $data) { + if ($data['required']) { + $buffer .= "\$$setting = " . var_export($data['value'], TRUE) . ";\n"; + } + } + + $fp = fopen(DRUPAL_ROOT . '/' . $settings_file, 'w'); + if ($fp && fwrite($fp, $buffer) === FALSE) { + throw new Exception(st('Failed to modify %settings. Verify the file permissions.', array('%settings' => $settings_file))); + } + } + else { + throw new Exception(st('Failed to open %settings. Verify the file permissions.', array('%settings' => $default_settings))); + } +} + +/** + * Verify an install profile for installation. + * + * @param $install_state + * An array of information about the current installation state. + * @return + * The list of modules to install. + */ +function drupal_verify_profile($install_state) { + $profile = $install_state['parameters']['profile']; + $locale = $install_state['parameters']['locale']; + + include_once DRUPAL_ROOT . '/core/includes/file.inc'; + include_once DRUPAL_ROOT . '/core/includes/common.inc'; + + $profile_file = DRUPAL_ROOT . "/profiles/$profile/$profile.profile"; + + if (!isset($profile) || !file_exists($profile_file)) { + throw new Exception(install_no_profile_error()); + } + $info = $install_state['profile_info']; + + // Get a list of modules that exist in Drupal's assorted subdirectories. + $present_modules = array(); + foreach (drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0) as $present_module) { + $present_modules[] = $present_module->name; + } + + // The install profile is also a module, which needs to be installed after all the other dependencies + // have been installed. + $present_modules[] = drupal_get_profile(); + + // Verify that all of the profile's required modules are present. + $missing_modules = array_diff($info['dependencies'], $present_modules); + + $requirements = array(); + + if (count($missing_modules)) { + $modules = array(); + foreach ($missing_modules as $module) { + $modules[] = '' . drupal_ucfirst($module) . ''; + } + $requirements['required_modules'] = array( + 'title' => st('Required modules'), + 'value' => st('Required modules not found.'), + 'severity' => REQUIREMENT_ERROR, + 'description' => st('The following modules are required but were not found. Move them into the appropriate modules subdirectory, such as sites/all/modules. Missing modules: !modules', array('!modules' => implode(', ', $modules))), + ); + } + return $requirements; +} + +/** + * Callback to install the system module. + * + * Separated from the installation of other modules so core system + * functions can be made available while other modules are installed. + */ +function drupal_install_system() { + $system_path = drupal_get_path('module', 'system'); + require_once DRUPAL_ROOT . '/' . $system_path . '/system.install'; + module_invoke('system', 'install'); + + $system_versions = drupal_get_schema_versions('system'); + $system_version = $system_versions ? max($system_versions) : SCHEMA_INSTALLED; + db_insert('system') + ->fields(array('filename', 'name', 'type', 'owner', 'status', 'schema_version', 'bootstrap')) + ->values(array( + 'filename' => $system_path . '/system.module', + 'name' => 'system', + 'type' => 'module', + 'owner' => '', + 'status' => 1, + 'schema_version' => $system_version, + 'bootstrap' => 0, + )) + ->execute(); + system_rebuild_module_data(); +} + +/** + * Uninstalls a given list of modules. + * + * @param $module_list + * The modules to uninstall. + * @param $uninstall_dependents + * If TRUE, the function will check that all modules which depend on the + * passed-in module list either are already uninstalled or contained in the + * list, and it will ensure that the modules are uninstalled in the correct + * order. This incurs a significant performance cost, so use FALSE if you + * know $module_list is already complete and in the correct order. + * + * @return + * FALSE if one or more dependent modules are missing from the list, TRUE + * otherwise. + */ +function drupal_uninstall_modules($module_list = array(), $uninstall_dependents = TRUE) { + if ($uninstall_dependents) { + // Get all module data so we can find dependents and sort. + $module_data = system_rebuild_module_data(); + // Create an associative array with weights as values. + $module_list = array_flip(array_values($module_list)); + + $profile = drupal_get_profile(); + while (list($module) = each($module_list)) { + if (!isset($module_data[$module]) || drupal_get_installed_schema_version($module) == SCHEMA_UNINSTALLED) { + // This module doesn't exist or is already uninstalled, skip it. + unset($module_list[$module]); + continue; + } + $module_list[$module] = $module_data[$module]->sort; + + // If the module has any dependents which are not already uninstalled and + // not included in the passed-in list, abort. It is not safe to uninstall + // them automatically because uninstalling a module is a destructive + // operation. + foreach (array_keys($module_data[$module]->required_by) as $dependent) { + if (!isset($module_list[$dependent]) && drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED && $dependent != $profile) { + return FALSE; + } + } + } + + // Sort the module list by pre-calculated weights. + asort($module_list); + $module_list = array_keys($module_list); + } + + foreach ($module_list as $module) { + // Uninstall the module. + module_load_install($module); + module_invoke($module, 'uninstall'); + drupal_uninstall_schema($module); + + watchdog('system', '%module module uninstalled.', array('%module' => $module), LOG_INFO); + drupal_set_installed_schema_version($module, SCHEMA_UNINSTALLED); + } + + if (!empty($module_list)) { + // Call hook_module_uninstall to let other modules act + module_invoke_all('modules_uninstalled', $module_list); + } + + return TRUE; +} + +/** + * Verify the state of the specified file. + * + * @param $file + * The file to check for. + * @param $mask + * An optional bitmask created from various FILE_* constants. + * @param $type + * The type of file. Can be file (default), dir, or link. + * @return + * TRUE on success or FALSE on failure. A message is set for the latter. + */ +function drupal_verify_install_file($file, $mask = NULL, $type = 'file') { + $return = TRUE; + // Check for files that shouldn't be there. + if (isset($mask) && ($mask & FILE_NOT_EXIST) && file_exists($file)) { + return FALSE; + } + // Verify that the file is the type of file it is supposed to be. + if (isset($type) && file_exists($file)) { + $check = 'is_' . $type; + if (!function_exists($check) || !$check($file)) { + $return = FALSE; + } + } + + // Verify file permissions. + if (isset($mask)) { + $masks = array(FILE_EXIST, FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE); + foreach ($masks as $current_mask) { + if ($mask & $current_mask) { + switch ($current_mask) { + case FILE_EXIST: + if (!file_exists($file)) { + if ($type == 'dir') { + drupal_install_mkdir($file, $mask); + } + if (!file_exists($file)) { + $return = FALSE; + } + } + break; + case FILE_READABLE: + if (!is_readable($file) && !drupal_install_fix_file($file, $mask)) { + $return = FALSE; + } + break; + case FILE_WRITABLE: + if (!is_writable($file) && !drupal_install_fix_file($file, $mask)) { + $return = FALSE; + } + break; + case FILE_EXECUTABLE: + if (!is_executable($file) && !drupal_install_fix_file($file, $mask)) { + $return = FALSE; + } + break; + case FILE_NOT_READABLE: + if (is_readable($file) && !drupal_install_fix_file($file, $mask)) { + $return = FALSE; + } + break; + case FILE_NOT_WRITABLE: + if (is_writable($file) && !drupal_install_fix_file($file, $mask)) { + $return = FALSE; + } + break; + case FILE_NOT_EXECUTABLE: + if (is_executable($file) && !drupal_install_fix_file($file, $mask)) { + $return = FALSE; + } + break; + } + } + } + } + return $return; +} + +/** + * Create a directory with specified permissions. + * + * @param $file + * The name of the directory to create; + * @param $mask + * The permissions of the directory to create. + * @param $message + * (optional) Whether to output messages. Defaults to TRUE. + * @return + * TRUE/FALSE whether or not the directory was successfully created. + */ +function drupal_install_mkdir($file, $mask, $message = TRUE) { + $mod = 0; + $masks = array(FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE); + foreach ($masks as $m) { + if ($mask & $m) { + switch ($m) { + case FILE_READABLE: + $mod |= 0444; + break; + case FILE_WRITABLE: + $mod |= 0222; + break; + case FILE_EXECUTABLE: + $mod |= 0111; + break; + } + } + } + + if (@drupal_mkdir($file, $mod)) { + return TRUE; + } + else { + return FALSE; + } +} + +/** + * Attempt to fix file permissions. + * + * The general approach here is that, because we do not know the security + * setup of the webserver, we apply our permission changes to all three + * digits of the file permission (i.e. user, group and all). + * + * To ensure that the values behave as expected (and numbers don't carry + * from one digit to the next) we do the calculation on the octal value + * using bitwise operations. This lets us remove, for example, 0222 from + * 0700 and get the correct value of 0500. + * + * @param $file + * The name of the file with permissions to fix. + * @param $mask + * The desired permissions for the file. + * @param $message + * (optional) Whether to output messages. Defaults to TRUE. + * @return + * TRUE/FALSE whether or not we were able to fix the file's permissions. + */ +function drupal_install_fix_file($file, $mask, $message = TRUE) { + // If $file does not exist, fileperms() issues a PHP warning. + if (!file_exists($file)) { + return FALSE; + } + + $mod = fileperms($file) & 0777; + $masks = array(FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE); + + // FILE_READABLE, FILE_WRITABLE, and FILE_EXECUTABLE permission strings + // can theoretically be 0400, 0200, and 0100 respectively, but to be safe + // we set all three access types in case the administrator intends to + // change the owner of settings.php after installation. + foreach ($masks as $m) { + if ($mask & $m) { + switch ($m) { + case FILE_READABLE: + if (!is_readable($file)) { + $mod |= 0444; + } + break; + case FILE_WRITABLE: + if (!is_writable($file)) { + $mod |= 0222; + } + break; + case FILE_EXECUTABLE: + if (!is_executable($file)) { + $mod |= 0111; + } + break; + case FILE_NOT_READABLE: + if (is_readable($file)) { + $mod &= ~0444; + } + break; + case FILE_NOT_WRITABLE: + if (is_writable($file)) { + $mod &= ~0222; + } + break; + case FILE_NOT_EXECUTABLE: + if (is_executable($file)) { + $mod &= ~0111; + } + break; + } + } + } + + // chmod() will work if the web server is running as owner of the file. + // If PHP safe_mode is enabled the currently executing script must also + // have the same owner. + if (@chmod($file, $mod)) { + return TRUE; + } + else { + return FALSE; + } +} + + +/** + * Send the user to a different installer page. + * + * This issues an on-site HTTP redirect. Messages (and errors) are erased. + * + * @param $path + * An installer path. + */ +function install_goto($path) { + global $base_url; + include_once DRUPAL_ROOT . '/core/includes/common.inc'; + header('Location: ' . $base_url . '/' . $path); + header('Cache-Control: no-cache'); // Not a permanent redirect. + drupal_exit(); +} + +/** + * Functional equivalent of t(), used when some systems are not available. + * + * Used during the install process, when database, theme, and localization + * system is possibly not yet available. + * + * 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 get_t() + * @ingroup sanitization + */ +function st($string, array $args = array(), array $options = array()) { + static $locale_strings = NULL; + global $install_state; + + if (empty($options['context'])) { + $options['context'] = ''; + } + + if (!isset($locale_strings)) { + $locale_strings = array(); + if (isset($install_state['parameters']['profile']) && isset($install_state['parameters']['locale'])) { + // If the given locale was selected, there should be at least one .po file + // with its name ending in {$install_state['parameters']['locale']}.po + // This might or might not be the entire filename. It is also possible + // that multiple files end with the same extension, even if unlikely. + $po_files = file_scan_directory('./profiles/' . $install_state['parameters']['profile'] . '/translations', '/'. $install_state['parameters']['locale'] .'\.po$/', array('recurse' => FALSE)); + if (count($po_files)) { + require_once DRUPAL_ROOT . '/core/includes/locale.inc'; + foreach ($po_files as $po_file) { + _locale_import_read_po('mem-store', $po_file); + } + $locale_strings = _locale_import_one_string('mem-report'); + } + } + } + + require_once DRUPAL_ROOT . '/core/includes/theme.inc'; + // Transform arguments before inserting them + foreach ($args as $key => $value) { + switch ($key[0]) { + // Escaped only + case '@': + $args[$key] = check_plain($value); + break; + // Escaped and placeholder + case '%': + default: + $args[$key] = '' . check_plain($value) . ''; + break; + // Pass-through + case '!': + } + } + return strtr((!empty($locale_strings[$options['context']][$string]) ? $locale_strings[$options['context']][$string] : $string), $args); +} + +/** + * Check an install profile's requirements. + * + * @param $profile + * Name of install profile to check. + * @return + * Array of the install profile's requirements. + */ +function drupal_check_profile($profile) { + include_once DRUPAL_ROOT . '/core/includes/file.inc'; + + $profile_file = DRUPAL_ROOT . "/profiles/$profile/$profile.profile"; + + if (!isset($profile) || !file_exists($profile_file)) { + throw new Exception(install_no_profile_error()); + } + + $info = install_profile_info($profile); + + // Collect requirement testing results. + $requirements = array(); + foreach ($info['dependencies'] as $module) { + module_load_install($module); + $function = $module . '_requirements'; + if (function_exists($function)) { + $requirements = array_merge($requirements, $function('install')); + } + } + return $requirements; +} + +/** + * Extract highest severity from requirements array. + * + * @param $requirements + * An array of requirements, in the same format as is returned by + * hook_requirements(). + * @return + * The highest severity in the array. + */ +function drupal_requirements_severity(&$requirements) { + $severity = REQUIREMENT_OK; + foreach ($requirements as $requirement) { + if (isset($requirement['severity'])) { + $severity = max($severity, $requirement['severity']); + } + } + return $severity; +} + +/** + * Check a module's requirements. + * + * @param $module + * Machine name of module to check. + * @return + * TRUE/FALSE depending on the requirements are in place. + */ +function drupal_check_module($module) { + module_load_install($module); + if (module_hook($module, 'requirements')) { + // Check requirements + $requirements = module_invoke($module, 'requirements', 'install'); + if (is_array($requirements) && drupal_requirements_severity($requirements) == REQUIREMENT_ERROR) { + // Print any error messages + foreach ($requirements as $requirement) { + if (isset($requirement['severity']) && $requirement['severity'] == REQUIREMENT_ERROR) { + $message = $requirement['description']; + if (isset($requirement['value']) && $requirement['value']) { + $message .= ' (' . t('Currently using !item !version', array('!item' => $requirement['title'], '!version' => $requirement['value'])) . ')'; + } + drupal_set_message($message, 'error'); + } + } + return FALSE; + } + } + return TRUE; +} + +/** + * Retrieve info about an install profile from its .info file. + * + * The information stored in a profile .info file is similar to that stored in + * a normal Drupal module .info file. For example: + * - name: The real name of the install profile for display purposes. + * - description: A brief description of the profile. + * - dependencies: An array of shortnames of other modules this install profile requires. + * + * Additional, less commonly-used information that can appear in a profile.info + * file but not in a normal Drupal module .info file includes: + * - distribution_name: The name of the Drupal distribution that is being + * installed, to be shown throughout the installation process. Defaults to + * 'Drupal'. + * + * Note that this function does an expensive file system scan to get info file + * information for dependencies. If you only need information from the info + * file itself, use system_get_info(). + * + * Example of .info file: + * @code + * name = Minimal + * description = Start fresh, with only a few modules enabled. + * dependencies[] = block + * dependencies[] = dblog + * @endcode + * + * @param profile + * Name of profile. + * @param locale + * Name of locale used (if any). + * @return + * The info array. + */ +function install_profile_info($profile, $locale = 'en') { + $cache = &drupal_static(__FUNCTION__, array()); + + if (!isset($cache[$profile])) { + // Set defaults for module info. + $defaults = array( + 'dependencies' => array(), + 'description' => '', + 'distribution_name' => 'Drupal', + 'version' => NULL, + 'hidden' => FALSE, + 'php' => DRUPAL_MINIMUM_PHP, + ); + $info = drupal_parse_info_file("profiles/$profile/$profile.info") + $defaults; + $info['dependencies'] = array_unique(array_merge( + drupal_required_modules(), + $info['dependencies'], + ($locale != 'en' && !empty($locale) ? array('locale') : array())) + ); + + // drupal_required_modules() includes the current profile as a dependency. + // Since a module can't depend on itself we remove that element of the array. + array_shift($info['dependencies']); + + $cache[$profile] = $info; + } + return $cache[$profile]; +} + +/** + * Ensures the environment for a Drupal database on a predefined connection. + * + * This will run tasks that check that Drupal can perform all of the functions + * on a database, that Drupal needs. Tasks include simple checks like CREATE + * TABLE to database specific functions like stored procedures and client + * encoding. + */ +function db_run_tasks($driver) { + db_installer_object($driver)->runTasks(); + return TRUE; +} + +/** + * Returns a database installer object. + * + * @param $driver + * The name of the driver. + */ +function db_installer_object($driver) { + Database::loadDriverFile($driver, array('install.inc')); + $task_class = 'DatabaseTasks_' . $driver; + return new $task_class(); +} diff --git a/includes/iso.inc b/core/includes/iso.inc similarity index 100% rename from includes/iso.inc rename to core/includes/iso.inc diff --git a/includes/language.inc b/core/includes/language.inc similarity index 100% rename from includes/language.inc rename to core/includes/language.inc diff --git a/core/includes/locale.inc b/core/includes/locale.inc new file mode 100644 index 0000000..0536ee2 --- /dev/null +++ b/core/includes/locale.inc @@ -0,0 +1,2293 @@ +language) ? $language->language : FALSE; +} + +/** + * Identify language from the Accept-language HTTP header we got. + * + * We perform browser accept-language parsing only if page cache is disabled, + * otherwise we would cache a user-specific preference. + * + * @param $languages + * An array of valid language objects. + * + * @return + * A valid language code on success, FALSE otherwise. + */ +function locale_language_from_browser($languages) { + // Specified by the user via the browser's Accept Language setting + // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5" + $browser_langs = array(); + + if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + $browser_accept = explode(",", $_SERVER['HTTP_ACCEPT_LANGUAGE']); + foreach ($browser_accept as $langpart) { + // The language part is either a code or a code with a quality. + // We cannot do anything with a * code, so it is skipped. + // If the quality is missing, it is assumed to be 1 according to the RFC. + if (preg_match("!([a-z-]+)(;q=([0-9\\.]+))?!", trim($langpart), $found)) { + $browser_langs[$found[1]] = (isset($found[3]) ? (float) $found[3] : 1.0); + } + } + } + + // Order the codes by quality + arsort($browser_langs); + + // Try to find the first preferred language we have + foreach ($browser_langs as $langcode => $q) { + if (isset($languages[$langcode])) { + return $langcode; + } + } + + return FALSE; +} + +/** + * Identify language from the user preferences. + * + * @param $languages + * An array of valid language objects. + * + * @return + * A valid language code on success, FALSE otherwise. + */ +function locale_language_from_user($languages) { + // User preference (only for logged users). + global $user; + + if ($user->uid) { + return $user->language; + } + + // No language preference from the user. + return FALSE; +} + +/** + * Identify language from a request/session parameter. + * + * @param $languages + * An array of valid language objects. + * + * @return + * A valid language code on success, FALSE otherwise. + */ +function locale_language_from_session($languages) { + $param = variable_get('locale_language_negotiation_session_param', 'language'); + + // Request parameter: we need to update the session parameter only if we have + // an authenticated user. + if (isset($_GET[$param]) && isset($languages[$langcode = $_GET[$param]])) { + global $user; + if ($user->uid) { + $_SESSION[$param] = $langcode; + } + return $langcode; + } + + // Session parameter. + if (isset($_SESSION[$param])) { + return $_SESSION[$param]; + } + + return FALSE; +} + +/** + * Identify language via URL prefix or domain. + * + * @param $languages + * An array of valid language objects. + * + * @return + * A valid language code on success, FALSE otherwise. + */ +function locale_language_from_url($languages) { + $language_url = FALSE; + + if (!language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_URL)) { + return $language_url; + } + + switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) { + case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX: + // $_GET['q'] might not be available at this time, because + // path initialization runs after the language bootstrap phase. + list($language, $_GET['q']) = language_url_split_prefix(isset($_GET['q']) ? $_GET['q'] : NULL, $languages); + if ($language !== FALSE) { + $language_url = $language->language; + } + break; + + case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN: + foreach ($languages as $language) { + $host = parse_url($language->domain, PHP_URL_HOST); + if ($host && ($_SERVER['HTTP_HOST'] == $host)) { + $language_url = $language->language; + break; + } + } + break; + } + + return $language_url; +} + +/** + * Determines the language to be assigned to URLs when none is detected. + * + * The language negotiation process has a fallback chain that ends with the + * default language provider. Each built-in language type has a separate + * initialization: + * - Interface language, which is the only configurable one, always gets a valid + * value. If no request-specific language is detected, the default language + * will be used. + * - Content language merely inherits the interface language by default. + * - URL language is detected from the requested URL and will be used to rewrite + * URLs appearing in the page being rendered. If no language can be detected, + * there are two possibilities: + * - If the default language has no configured path prefix or domain, then the + * default language is used. This guarantees that (missing) URL prefixes are + * preserved when navigating through the site. + * - If the default language has a configured path prefix or domain, a + * requested URL having an empty prefix or domain is an anomaly that must be + * fixed. This is done by introducing a prefix or domain in the rendered + * page matching the detected interface language. + * + * @param $languages + * (optional) An array of valid language objects. This is passed by + * language_provider_invoke() to every language provider callback, but it is + * not actually needed here. Defaults to NULL. + * @param $language_type + * (optional) The language type to fall back to. Defaults to the interface + * language. + * + * @return + * A valid language code. + */ +function locale_language_url_fallback($language = NULL, $language_type = LANGUAGE_TYPE_INTERFACE) { + $default = language_default(); + $prefix = (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX) == LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX); + + // If the default language is not configured to convey language information, + // a missing URL language information indicates that URL language should be + // the default one, otherwise we fall back to an already detected language. + if (($prefix && empty($default->prefix)) || (!$prefix && empty($default->domain))) { + return $default->language; + } + else { + return $GLOBALS[$language_type]->language; + } +} + +/** + * Return the URL language switcher block. Translation links may be provided by + * other modules. + */ +function locale_language_switcher_url($type, $path) { + $languages = language_list('enabled'); + $links = array(); + + foreach ($languages[1] as $language) { + $links[$language->language] = array( + 'href' => $path, + 'title' => $language->native, + 'language' => $language, + 'attributes' => array('class' => array('language-link')), + ); + } + + return $links; +} + +/** + * Return the session language switcher block. + */ +function locale_language_switcher_session($type, $path) { + drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css'); + + $param = variable_get('locale_language_negotiation_session_param', 'language'); + $language_query = isset($_SESSION[$param]) ? $_SESSION[$param] : $GLOBALS[$type]->language; + + $languages = language_list('enabled'); + $links = array(); + + $query = $_GET; + unset($query['q']); + + foreach ($languages[1] as $language) { + $langcode = $language->language; + $links[$langcode] = array( + 'href' => $path, + 'title' => $language->native, + 'attributes' => array('class' => array('language-link')), + 'query' => $query, + ); + if ($language_query != $langcode) { + $links[$langcode]['query'][$param] = $langcode; + } + else { + $links[$langcode]['attributes']['class'][] = ' session-active'; + } + } + + return $links; +} + +/** + * Rewrite URLs for the URL language provider. + */ +function locale_language_url_rewrite_url(&$path, &$options) { + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['languages'] = &drupal_static(__FUNCTION__); + } + $languages = &$drupal_static_fast['languages']; + + if (!isset($languages)) { + $languages = language_list('enabled'); + $languages = array_flip(array_keys($languages[1])); + } + + // Language can be passed as an option, or we go for current URL language. + if (!isset($options['language'])) { + global $language_url; + $options['language'] = $language_url; + } + // We allow only enabled languages here. + elseif (!isset($languages[$options['language']->language])) { + unset($options['language']); + return; + } + + if (isset($options['language'])) { + switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) { + case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN: + if ($options['language']->domain) { + // Ask for an absolute URL with our modified base_url. + $options['absolute'] = TRUE; + $options['base_url'] = $options['language']->domain; + } + break; + + case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX: + if (!empty($options['language']->prefix)) { + $options['prefix'] = $options['language']->prefix . '/'; + } + break; + } + } +} + +/** + * Rewrite URLs for the Session language provider. + */ +function locale_language_url_rewrite_session(&$path, &$options) { + static $query_rewrite, $query_param, $query_value; + + // The following values are not supposed to change during a single page + // request processing. + if (!isset($query_rewrite)) { + global $user; + if (!$user->uid) { + $languages = language_list('enabled'); + $languages = $languages[1]; + $query_param = check_plain(variable_get('locale_language_negotiation_session_param', 'language')); + $query_value = isset($_GET[$query_param]) ? check_plain($_GET[$query_param]) : NULL; + $query_rewrite = isset($languages[$query_value]) && language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_SESSION); + } + else { + $query_rewrite = FALSE; + } + } + + // If the user is anonymous, the user language provider is enabled, and the + // corresponding option has been set, we must preserve any explicit user + // language preference even with cookies disabled. + if ($query_rewrite) { + if (is_string($options['query'])) { + $options['query'] = drupal_get_query_array($options['query']); + } + if (!isset($options['query'][$query_param])) { + $options['query'][$query_param] = $query_value; + } + } +} + +/** + * @} End of "locale-languages-negotiation" + */ + +/** + * Check that a string is safe to be added or imported as a translation. + * + * This test can be used to detect possibly bad translation strings. It should + * not have any false positives. But it is only a test, not a transformation, + * as it destroys valid HTML. We cannot reliably filter translation strings + * on import because some strings are irreversibly corrupted. For example, + * a & in the translation would get encoded to &amp; by filter_xss() + * before being put in the database, and thus would be displayed incorrectly. + * + * The allowed tag list is like filter_xss_admin(), but omitting div and img as + * not needed for translation and likely to cause layout issues (div) or a + * possible attack vector (img). + */ +function locale_string_is_safe($string) { + return decode_entities($string) == decode_entities(filter_xss($string, array('a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'ins', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'ul', 'var'))); +} + +/** + * @defgroup locale-api-add Language addition API + * @{ + * Add a language. + * + * The language addition API is used to create languages and store them. + */ + +/** + * API function to add a language. + * + * @param $langcode + * Language code. + * @param $name + * English name of the language + * @param $native + * Native name of the language + * @param $direction + * LANGUAGE_LTR or LANGUAGE_RTL + * @param $domain + * Optional custom domain name with protocol, without + * trailing slash (eg. http://de.example.com). + * @param $prefix + * Optional path prefix for the language. Defaults to the + * language code if omitted. + * @param $enabled + * Optionally TRUE to enable the language when created or FALSE to disable. + * @param $default + * Optionally set this language to be the default. + */ +function locale_add_language($langcode, $name = NULL, $native = NULL, $direction = LANGUAGE_LTR, $domain = '', $prefix = '', $enabled = TRUE, $default = FALSE) { + // Default prefix on language code. + if (empty($prefix)) { + $prefix = $langcode; + } + + // If name was not set, we add a predefined language. + if (!isset($name)) { + include_once DRUPAL_ROOT . '/core/includes/iso.inc'; + $predefined = _locale_get_predefined_list(); + $name = $predefined[$langcode][0]; + $native = isset($predefined[$langcode][1]) ? $predefined[$langcode][1] : $predefined[$langcode][0]; + $direction = isset($predefined[$langcode][2]) ? $predefined[$langcode][2] : LANGUAGE_LTR; + } + + db_insert('languages') + ->fields(array( + 'language' => $langcode, + 'name' => $name, + 'native' => $native, + 'direction' => $direction, + 'domain' => $domain, + 'prefix' => $prefix, + 'enabled' => $enabled, + )) + ->execute(); + + // Only set it as default if enabled. + if ($enabled && $default) { + variable_set('language_default', (object) array('language' => $langcode, 'name' => $name, 'native' => $native, 'direction' => $direction, 'enabled' => (int) $enabled, 'plurals' => 0, 'formula' => '', 'domain' => '', 'prefix' => $prefix, 'weight' => 0, 'javascript' => '')); + } + + if ($enabled) { + // Increment enabled language count if we are adding an enabled language. + variable_set('language_count', variable_get('language_count', 1) + 1); + } + + // Kill the static cache in language_list(). + drupal_static_reset('language_list'); + + // Force JavaScript translation file creation for the newly added language. + _locale_invalidate_js($langcode); + + watchdog('locale', 'The %language language (%code) has been created.', array('%language' => $name, '%code' => $langcode)); + + module_invoke_all('multilingual_settings_changed'); +} +/** + * @} End of "locale-api-add" + */ + +/** + * @defgroup locale-api-import-export Translation import/export API. + * @{ + * Functions to import and export translations. + * + * These functions provide the ability to import translations from + * external files and to export translations and translation templates. + */ + +/** + * Parses Gettext Portable Object file information and inserts into database + * + * @param $file + * Drupal file object corresponding to the PO file to import. + * @param $langcode + * Language code. + * @param $mode + * Should existing translations be replaced LOCALE_IMPORT_KEEP or + * LOCALE_IMPORT_OVERWRITE. + * @param $group + * Text group to import PO file into (eg. 'default' for interface + * translations). + */ +function _locale_import_po($file, $langcode, $mode, $group = NULL) { + // Try to allocate enough time to parse and import the data. + drupal_set_time_limit(240); + + // Check if we have the language already in the database. + if (!db_query("SELECT COUNT(language) FROM {languages} WHERE language = :language", array(':language' => $langcode))->fetchField()) { + drupal_set_message(t('The language selected for import is not supported.'), 'error'); + return FALSE; + } + + // Get strings from file (returns on failure after a partial import, or on success) + $status = _locale_import_read_po('db-store', $file, $mode, $langcode, $group); + if ($status === FALSE) { + // Error messages are set in _locale_import_read_po(). + return FALSE; + } + + // Get status information on import process. + list($header_done, $additions, $updates, $deletes, $skips) = _locale_import_one_string('db-report'); + + if (!$header_done) { + drupal_set_message(t('The translation file %filename appears to have a missing or malformed header.', array('%filename' => $file->filename)), 'error'); + } + + // Clear cache and force refresh of JavaScript translations. + _locale_invalidate_js($langcode); + cache_clear_all('locale:', 'cache', TRUE); + + // Rebuild the menu, strings may have changed. + menu_rebuild(); + + drupal_set_message(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes))); + watchdog('locale', 'Imported %file into %locale: %number new strings added, %update updated and %delete removed.', array('%file' => $file->filename, '%locale' => $langcode, '%number' => $additions, '%update' => $updates, '%delete' => $deletes)); + if ($skips) { + $skip_message = format_plural($skips, 'One translation string was skipped because it contains disallowed HTML.', '@count translation strings were skipped because they contain disallowed HTML.'); + drupal_set_message($skip_message); + watchdog('locale', '@count disallowed HTML string(s) in %file', array('@count' => $skips, '%file' => $file->uri), LOG_WARNING); + } + return TRUE; +} + +/** + * Parses Gettext Portable Object file into an array + * + * @param $op + * Storage operation type: db-store or mem-store. + * @param $file + * Drupal file object corresponding to the PO file to import. + * @param $mode + * Should existing translations be replaced LOCALE_IMPORT_KEEP or + * LOCALE_IMPORT_OVERWRITE. + * @param $lang + * Language code. + * @param $group + * Text group to import PO file into (eg. 'default' for interface + * translations). + */ +function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL, $group = 'default') { + + // The file will get closed by PHP on returning from this function. + $fd = fopen($file->uri, 'rb'); + if (!$fd) { + _locale_import_message('The translation import failed, because the file %filename could not be read.', $file); + return FALSE; + } + + /* + * The parser context. Can be: + * - 'COMMENT' (#) + * - 'MSGID' (msgid) + * - 'MSGID_PLURAL' (msgid_plural) + * - 'MSGCTXT' (msgctxt) + * - 'MSGSTR' (msgstr or msgstr[]) + * - 'MSGSTR_ARR' (msgstr_arg) + */ + $context = 'COMMENT'; + + // Current entry being read. + $current = array(); + + // Current plurality for 'msgstr[]'. + $plural = 0; + + // Current line. + $lineno = 0; + + while (!feof($fd)) { + // A line should not be longer than 10 * 1024. + $line = fgets($fd, 10 * 1024); + + if ($lineno == 0) { + // The first line might come with a UTF-8 BOM, which should be removed. + $line = str_replace("\xEF\xBB\xBF", '', $line); + } + + $lineno++; + + // Trim away the linefeed. + $line = trim(strtr($line, array("\\\n" => ""))); + + if (!strncmp('#', $line, 1)) { + // Lines starting with '#' are comments. + + if ($context == 'COMMENT') { + // Already in comment token, insert the comment. + $current['#'][] = substr($line, 1); + } + elseif (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { + // We are currently in string token, close it out. + _locale_import_one_string($op, $current, $mode, $lang, $file, $group); + + // Start a new entry for the comment. + $current = array(); + $current['#'][] = substr($line, 1); + + $context = 'COMMENT'; + } + else { + // A comment following any other token is a syntax error. + _locale_import_message('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', $file, $lineno); + return FALSE; + } + } + elseif (!strncmp('msgid_plural', $line, 12)) { + // A plural form for the current message. + + if ($context != 'MSGID') { + // A plural form cannot be added to anything else but the id directly. + _locale_import_message('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', $file, $lineno); + return FALSE; + } + + // Remove 'msgid_plural' and trim away whitespace. + $line = trim(substr($line, 12)); + // At this point, $line should now contain only the plural form. + + $quoted = _locale_import_parse_quoted($line); + if ($quoted === FALSE) { + // The plural form must be wrapped in quotes. + _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); + return FALSE; + } + + // Append the plural form to the current entry. + $current['msgid'] .= "\0" . $quoted; + + $context = 'MSGID_PLURAL'; + } + elseif (!strncmp('msgid', $line, 5)) { + // Starting a new message. + + if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { + // We are currently in a message string, close it out. + _locale_import_one_string($op, $current, $mode, $lang, $file, $group); + + // Start a new context for the id. + $current = array(); + } + elseif ($context == 'MSGID') { + // We are currently already in the context, meaning we passed an id with no data. + _locale_import_message('The translation file %filename contains an error: "msgid" is unexpected on line %line.', $file, $lineno); + return FALSE; + } + + // Remove 'msgid' and trim away whitespace. + $line = trim(substr($line, 5)); + // At this point, $line should now contain only the message id. + + $quoted = _locale_import_parse_quoted($line); + if ($quoted === FALSE) { + // The message id must be wrapped in quotes. + _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); + return FALSE; + } + + $current['msgid'] = $quoted; + $context = 'MSGID'; + } + elseif (!strncmp('msgctxt', $line, 7)) { + // Starting a new context. + + if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { + // We are currently in a message, start a new one. + _locale_import_one_string($op, $current, $mode, $lang, $file, $group); + $current = array(); + } + elseif (!empty($current['msgctxt'])) { + // A context cannot apply to another context. + _locale_import_message('The translation file %filename contains an error: "msgctxt" is unexpected on line %line.', $file, $lineno); + return FALSE; + } + + // Remove 'msgctxt' and trim away whitespaces. + $line = trim(substr($line, 7)); + // At this point, $line should now contain the context. + + $quoted = _locale_import_parse_quoted($line); + if ($quoted === FALSE) { + // The context string must be quoted. + _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); + return FALSE; + } + + $current['msgctxt'] = $quoted; + + $context = 'MSGCTXT'; + } + elseif (!strncmp('msgstr[', $line, 7)) { + // A message string for a specific plurality. + + if (($context != 'MSGID') && ($context != 'MSGCTXT') && ($context != 'MSGID_PLURAL') && ($context != 'MSGSTR_ARR')) { + // Message strings must come after msgid, msgxtxt, msgid_plural, or other msgstr[] entries. + _locale_import_message('The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.', $file, $lineno); + return FALSE; + } + + // Ensure the plurality is terminated. + if (strpos($line, ']') === FALSE) { + _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); + return FALSE; + } + + // Extract the plurality. + $frombracket = strstr($line, '['); + $plural = substr($frombracket, 1, strpos($frombracket, ']') - 1); + + // Skip to the next whitespace and trim away any further whitespace, bringing $line to the message data. + $line = trim(strstr($line, " ")); + + $quoted = _locale_import_parse_quoted($line); + if ($quoted === FALSE) { + // The string must be quoted. + _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); + return FALSE; + } + + $current['msgstr'][$plural] = $quoted; + + $context = 'MSGSTR_ARR'; + } + elseif (!strncmp("msgstr", $line, 6)) { + // A string for the an id or context. + + if (($context != 'MSGID') && ($context != 'MSGCTXT')) { + // Strings are only valid within an id or context scope. + _locale_import_message('The translation file %filename contains an error: "msgstr" is unexpected on line %line.', $file, $lineno); + return FALSE; + } + + // Remove 'msgstr' and trim away away whitespaces. + $line = trim(substr($line, 6)); + // At this point, $line should now contain the message. + + $quoted = _locale_import_parse_quoted($line); + if ($quoted === FALSE) { + // The string must be quoted. + _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); + return FALSE; + } + + $current['msgstr'] = $quoted; + + $context = 'MSGSTR'; + } + elseif ($line != '') { + // Anything that is not a token may be a continuation of a previous token. + + $quoted = _locale_import_parse_quoted($line); + if ($quoted === FALSE) { + // The string must be quoted. + _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); + return FALSE; + } + + // Append the string to the current context. + if (($context == 'MSGID') || ($context == 'MSGID_PLURAL')) { + $current['msgid'] .= $quoted; + } + elseif ($context == 'MSGCTXT') { + $current['msgctxt'] .= $quoted; + } + elseif ($context == 'MSGSTR') { + $current['msgstr'] .= $quoted; + } + elseif ($context == 'MSGSTR_ARR') { + $current['msgstr'][$plural] .= $quoted; + } + else { + // No valid context to append to. + _locale_import_message('The translation file %filename contains an error: there is an unexpected string on line %line.', $file, $lineno); + return FALSE; + } + } + } + + // End of PO file, closed out the last entry. + if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { + _locale_import_one_string($op, $current, $mode, $lang, $file, $group); + } + elseif ($context != 'COMMENT') { + _locale_import_message('The translation file %filename ended unexpectedly at line %line.', $file, $lineno); + return FALSE; + } +} + +/** + * Sets an error message occurred during locale file parsing. + * + * @param $message + * The message to be translated. + * @param $file + * Drupal file object corresponding to the PO file to import. + * @param $lineno + * An optional line number argument. + */ +function _locale_import_message($message, $file, $lineno = NULL) { + $vars = array('%filename' => $file->filename); + if (isset($lineno)) { + $vars['%line'] = $lineno; + } + $t = get_t(); + drupal_set_message($t($message, $vars), 'error'); +} + +/** + * Imports a string into the database + * + * @param $op + * Operation to perform: 'db-store', 'db-report', 'mem-store' or 'mem-report'. + * @param $value + * Details of the string stored. + * @param $mode + * Should existing translations be replaced LOCALE_IMPORT_KEEP or + * LOCALE_IMPORT_OVERWRITE. + * @param $lang + * Language to store the string in. + * @param $file + * Object representation of file being imported, only required when op is + * 'db-store'. + * @param $group + * Text group to import PO file into (eg. 'default' for interface + * translations). + */ +function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NULL, $file = NULL, $group = 'default') { + $report = &drupal_static(__FUNCTION__, array('additions' => 0, 'updates' => 0, 'deletes' => 0, 'skips' => 0)); + $header_done = &drupal_static(__FUNCTION__ . ':header_done', FALSE); + $strings = &drupal_static(__FUNCTION__ . ':strings', array()); + + switch ($op) { + // Return stored strings + case 'mem-report': + return $strings; + + // Store string in memory (only supports single strings) + case 'mem-store': + $strings[isset($value['msgctxt']) ? $value['msgctxt'] : ''][$value['msgid']] = $value['msgstr']; + return; + + // Called at end of import to inform the user + case 'db-report': + return array($header_done, $report['additions'], $report['updates'], $report['deletes'], $report['skips']); + + // Store the string we got in the database. + case 'db-store': + // We got header information. + if ($value['msgid'] == '') { + $languages = language_list(); + if (($mode != LOCALE_IMPORT_KEEP) || empty($languages[$lang]->plurals)) { + // Since we only need to parse the header if we ought to update the + // plural formula, only run this if we don't need to keep existing + // data untouched or if we don't have an existing plural formula. + $header = _locale_import_parse_header($value['msgstr']); + + // Get the plural formula and update in database. + if (isset($header["Plural-Forms"]) && $p = _locale_import_parse_plural_forms($header["Plural-Forms"], $file->uri)) { + list($nplurals, $plural) = $p; + db_update('languages') + ->fields(array( + 'plurals' => $nplurals, + 'formula' => $plural, + )) + ->condition('language', $lang) + ->execute(); + } + else { + db_update('languages') + ->fields(array( + 'plurals' => 0, + 'formula' => '', + )) + ->condition('language', $lang) + ->execute(); + } + } + $header_done = TRUE; + } + + else { + // Some real string to import. + $comments = _locale_import_shorten_comments(empty($value['#']) ? array() : $value['#']); + + if (strpos($value['msgid'], "\0")) { + // This string has plural versions. + $english = explode("\0", $value['msgid'], 2); + $entries = array_keys($value['msgstr']); + for ($i = 3; $i <= count($entries); $i++) { + $english[] = $english[1]; + } + $translation = array_map('_locale_import_append_plural', $value['msgstr'], $entries); + $english = array_map('_locale_import_append_plural', $english, $entries); + foreach ($translation as $key => $trans) { + if ($key == 0) { + $plid = 0; + } + $plid = _locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english[$key], $trans, $group, $comments, $mode, $plid, $key); + } + } + + else { + // A simple string to import. + $english = $value['msgid']; + $translation = $value['msgstr']; + _locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english, $translation, $group, $comments, $mode); + } + } + } // end of db-store operation +} + +/** + * Import one string into the database. + * + * @param $report + * Report array summarizing the number of changes done in the form: + * array(inserts, updates, deletes). + * @param $langcode + * Language code to import string into. + * @param $context + * The context of this string. + * @param $source + * Source string. + * @param $translation + * Translation to language specified in $langcode. + * @param $textgroup + * Name of textgroup to store translation in. + * @param $location + * Location value to save with source string. + * @param $mode + * Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE. + * @param $plid + * Optional plural ID to use. + * @param $plural + * Optional plural value to use. + * + * @return + * The string ID of the existing string modified or the new string added. + */ +function _locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $textgroup, $location, $mode, $plid = 0, $plural = 0) { + $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = :textgroup", array(':source' => $source, ':context' => $context, ':textgroup' => $textgroup))->fetchField(); + + if (!empty($translation)) { + // Skip this string unless it passes a check for dangerous code. + // Text groups other than default still can contain HTML tags + // (i.e. translatable blocks). + if ($textgroup == "default" && !locale_string_is_safe($translation)) { + $report['skips']++; + $lid = 0; + } + elseif ($lid) { + // We have this source string saved already. + db_update('locales_source') + ->fields(array( + 'location' => $location, + )) + ->condition('lid', $lid) + ->execute(); + + $exists = db_query("SELECT COUNT(lid) FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchField(); + + if (!$exists) { + // No translation in this language. + db_insert('locales_target') + ->fields(array( + 'lid' => $lid, + 'language' => $langcode, + 'translation' => $translation, + 'plid' => $plid, + 'plural' => $plural, + )) + ->execute(); + + $report['additions']++; + } + elseif ($mode == LOCALE_IMPORT_OVERWRITE) { + // Translation exists, only overwrite if instructed. + db_update('locales_target') + ->fields(array( + 'translation' => $translation, + 'plid' => $plid, + 'plural' => $plural, + )) + ->condition('language', $langcode) + ->condition('lid', $lid) + ->execute(); + + $report['updates']++; + } + } + else { + // No such source string in the database yet. + $lid = db_insert('locales_source') + ->fields(array( + 'location' => $location, + 'source' => $source, + 'context' => (string) $context, + 'textgroup' => $textgroup, + )) + ->execute(); + + db_insert('locales_target') + ->fields(array( + 'lid' => $lid, + 'language' => $langcode, + 'translation' => $translation, + 'plid' => $plid, + 'plural' => $plural + )) + ->execute(); + + $report['additions']++; + } + } + elseif ($mode == LOCALE_IMPORT_OVERWRITE) { + // Empty translation, remove existing if instructed. + db_delete('locales_target') + ->condition('language', $langcode) + ->condition('lid', $lid) + ->condition('plid', $plid) + ->condition('plural', $plural) + ->execute(); + + $report['deletes']++; + } + + return $lid; +} + +/** + * Parses a Gettext Portable Object file header + * + * @param $header + * A string containing the complete header. + * + * @return + * An associative array of key-value pairs. + */ +function _locale_import_parse_header($header) { + $header_parsed = array(); + $lines = array_map('trim', explode("\n", $header)); + foreach ($lines as $line) { + if ($line) { + list($tag, $contents) = explode(":", $line, 2); + $header_parsed[trim($tag)] = trim($contents); + } + } + return $header_parsed; +} + +/** + * Parses a Plural-Forms entry from a Gettext Portable Object file header + * + * @param $pluralforms + * A string containing the Plural-Forms entry. + * @param $filepath + * A string containing the filepath. + * + * @return + * An array containing the number of plurals and a + * formula in PHP for computing the plural form. + */ +function _locale_import_parse_plural_forms($pluralforms, $filepath) { + // First, delete all whitespace + $pluralforms = strtr($pluralforms, array(" " => "", "\t" => "")); + + // Select the parts that define nplurals and plural + $nplurals = strstr($pluralforms, "nplurals="); + if (strpos($nplurals, ";")) { + $nplurals = substr($nplurals, 9, strpos($nplurals, ";") - 9); + } + else { + return FALSE; + } + $plural = strstr($pluralforms, "plural="); + if (strpos($plural, ";")) { + $plural = substr($plural, 7, strpos($plural, ";") - 7); + } + else { + return FALSE; + } + + // Get PHP version of the plural formula + $plural = _locale_import_parse_arithmetic($plural); + + if ($plural !== FALSE) { + return array($nplurals, $plural); + } + else { + drupal_set_message(t('The translation file %filepath contains an error: the plural formula could not be parsed.', array('%filepath' => $filepath)), 'error'); + return FALSE; + } +} + +/** + * Parses and sanitizes an arithmetic formula into a PHP expression + * + * While parsing, we ensure, that the operators have the right + * precedence and associativity. + * + * @param $string + * A string containing the arithmetic formula. + * + * @return + * The PHP version of the formula. + */ +function _locale_import_parse_arithmetic($string) { + // Operator precedence table + $precedence = array("(" => -1, ")" => -1, "?" => 1, ":" => 1, "||" => 3, "&&" => 4, "==" => 5, "!=" => 5, "<" => 6, ">" => 6, "<=" => 6, ">=" => 6, "+" => 7, "-" => 7, "*" => 8, "/" => 8, "%" => 8); + // Right associativity + $right_associativity = array("?" => 1, ":" => 1); + + $tokens = _locale_import_tokenize_formula($string); + + // Parse by converting into infix notation then back into postfix + // Operator stack - holds math operators and symbols + $operator_stack = array(); + // Element Stack - holds data to be operated on + $element_stack = array(); + + foreach ($tokens as $token) { + $current_token = $token; + + // Numbers and the $n variable are simply pushed into $element_stack + if (is_numeric($token)) { + $element_stack[] = $current_token; + } + elseif ($current_token == "n") { + $element_stack[] = '$n'; + } + elseif ($current_token == "(") { + $operator_stack[] = $current_token; + } + elseif ($current_token == ")") { + $topop = array_pop($operator_stack); + while (isset($topop) && ($topop != "(")) { + $element_stack[] = $topop; + $topop = array_pop($operator_stack); + } + } + elseif (!empty($precedence[$current_token])) { + // If it's an operator, then pop from $operator_stack into $element_stack until the + // precedence in $operator_stack is less than current, then push into $operator_stack + $topop = array_pop($operator_stack); + while (isset($topop) && ($precedence[$topop] >= $precedence[$current_token]) && !(($precedence[$topop] == $precedence[$current_token]) && !empty($right_associativity[$topop]) && !empty($right_associativity[$current_token]))) { + $element_stack[] = $topop; + $topop = array_pop($operator_stack); + } + if ($topop) { + $operator_stack[] = $topop; // Return element to top + } + $operator_stack[] = $current_token; // Parentheses are not needed + } + else { + return FALSE; + } + } + + // Flush operator stack + $topop = array_pop($operator_stack); + while ($topop != NULL) { + $element_stack[] = $topop; + $topop = array_pop($operator_stack); + } + + // Now extract formula from stack + $previous_size = count($element_stack) + 1; + while (count($element_stack) < $previous_size) { + $previous_size = count($element_stack); + for ($i = 2; $i < count($element_stack); $i++) { + $op = $element_stack[$i]; + if (!empty($precedence[$op])) { + $f = ""; + if ($op == ":") { + $f = $element_stack[$i - 2] . "):" . $element_stack[$i - 1] . ")"; + } + elseif ($op == "?") { + $f = "(" . $element_stack[$i - 2] . "?(" . $element_stack[$i - 1]; + } + else { + $f = "(" . $element_stack[$i - 2] . $op . $element_stack[$i - 1] . ")"; + } + array_splice($element_stack, $i - 2, 3, $f); + break; + } + } + } + + // If only one element is left, the number of operators is appropriate + if (count($element_stack) == 1) { + return $element_stack[0]; + } + else { + return FALSE; + } +} + +/** + * Backward compatible implementation of token_get_all() for formula parsing + * + * @param $string + * A string containing the arithmetic formula. + * + * @return + * The PHP version of the formula. + */ +function _locale_import_tokenize_formula($formula) { + $formula = str_replace(" ", "", $formula); + $tokens = array(); + for ($i = 0; $i < strlen($formula); $i++) { + if (is_numeric($formula[$i])) { + $num = $formula[$i]; + $j = $i + 1; + while ($j < strlen($formula) && is_numeric($formula[$j])) { + $num .= $formula[$j]; + $j++; + } + $i = $j - 1; + $tokens[] = $num; + } + elseif ($pos = strpos(" =<>!&|", $formula[$i])) { // We won't have a space + $next = $formula[$i + 1]; + switch ($pos) { + case 1: + case 2: + case 3: + case 4: + if ($next == '=') { + $tokens[] = $formula[$i] . '='; + $i++; + } + else { + $tokens[] = $formula[$i]; + } + break; + case 5: + if ($next == '&') { + $tokens[] = '&&'; + $i++; + } + else { + $tokens[] = $formula[$i]; + } + break; + case 6: + if ($next == '|') { + $tokens[] = '||'; + $i++; + } + else { + $tokens[] = $formula[$i]; + } + break; + } + } + else { + $tokens[] = $formula[$i]; + } + } + return $tokens; +} + +/** + * Modify a string to contain proper count indices + * + * This is a callback function used via array_map() + * + * @param $entry + * An array element. + * @param $key + * Index of the array element. + */ +function _locale_import_append_plural($entry, $key) { + // No modifications for 0, 1 + if ($key == 0 || $key == 1) { + return $entry; + } + + // First remove any possibly false indices, then add new ones + $entry = preg_replace('/(@count)\[[0-9]\]/', '\\1', $entry); + return preg_replace('/(@count)/', "\\1[$key]", $entry); +} + +/** + * Generate a short, one string version of the passed comment array + * + * @param $comment + * An array of strings containing a comment. + * + * @return + * Short one string version of the comment. + */ +function _locale_import_shorten_comments($comment) { + $comm = ''; + while (count($comment)) { + $test = $comm . substr(array_shift($comment), 1) . ', '; + if (strlen($comm) < 130) { + $comm = $test; + } + else { + break; + } + } + return trim(substr($comm, 0, -2)); +} + +/** + * Parses a string in quotes + * + * @param $string + * A string specified with enclosing quotes. + * + * @return + * The string parsed from inside the quotes. + */ +function _locale_import_parse_quoted($string) { + if (substr($string, 0, 1) != substr($string, -1, 1)) { + return FALSE; // Start and end quotes must be the same + } + $quote = substr($string, 0, 1); + $string = substr($string, 1, -1); + if ($quote == '"') { // Double quotes: strip slashes + return stripcslashes($string); + } + elseif ($quote == "'") { // Simple quote: return as-is + return $string; + } + else { + return FALSE; // Unrecognized quote + } +} +/** + * @} End of "locale-api-import-export" + */ + +/** + * Parses a JavaScript file, extracts strings wrapped in Drupal.t() and + * Drupal.formatPlural() and inserts them into the database. + */ +function _locale_parse_js_file($filepath) { + global $language; + + // The file path might contain a query string, so make sure we only use the + // actual file. + $parsed_url = drupal_parse_url($filepath); + $filepath = $parsed_url['path']; + // Load the JavaScript file. + $file = file_get_contents($filepath); + + // Match all calls to Drupal.t() in an array. + // Note: \s also matches newlines with the 's' modifier. + preg_match_all('~[^\w]Drupal\s*\.\s*t\s*\(\s*(' . LOCALE_JS_STRING . ')\s*[,\)]~s', $file, $t_matches); + + // Match all Drupal.formatPlural() calls in another array. + preg_match_all('~[^\w]Drupal\s*\.\s*formatPlural\s*\(\s*.+?\s*,\s*(' . LOCALE_JS_STRING . ')\s*,\s*((?:(?:\'(?:\\\\\'|[^\'])*@count(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*@count(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+)\s*[,\)]~s', $file, $plural_matches); + + // Loop through all matches and process them. + $all_matches = array_merge($plural_matches[1], $t_matches[1]); + foreach ($all_matches as $key => $string) { + $strings = array($string); + + // If there is also a plural version of this string, add it to the strings array. + if (isset($plural_matches[2][$key])) { + $strings[] = $plural_matches[2][$key]; + } + + foreach ($strings as $key => $string) { + // Remove the quotes and string concatenations from the string. + $string = implode('', preg_split('~(? $string))->fetchObject(); + if ($source) { + // We already have this source string and now have to add the location + // to the location column, if this file is not yet present in there. + $locations = preg_split('~\s*;\s*~', $source->location); + + if (!in_array($filepath, $locations)) { + $locations[] = $filepath; + $locations = implode('; ', $locations); + + // Save the new locations string to the database. + db_update('locales_source') + ->fields(array( + 'location' => $locations, + )) + ->condition('lid', $source->lid) + ->execute(); + } + } + else { + // We don't have the source string yet, thus we insert it into the database. + db_insert('locales_source') + ->fields(array( + 'location' => $filepath, + 'source' => $string, + 'context' => '', + 'textgroup' => 'default', + )) + ->execute(); + } + } + } +} + +/** + * @addtogroup locale-api-import-export + * @{ + */ + +/** + * Generates a structured array of all strings with translations in + * $language, if given. This array can be used to generate an export + * of the string in the database. + * + * @param $language + * Language object to generate the output for, or NULL if generating + * translation template. + * @param $group + * Text group to export PO file from (eg. 'default' for interface + * translations). + */ +function _locale_export_get_strings($language = NULL, $group = 'default') { + if (isset($language)) { + $result = db_query("SELECT s.lid, s.source, s.context, s.location, t.translation, t.plid, t.plural FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.textgroup = :textgroup ORDER BY t.plid, t.plural", array(':language' => $language->language, ':textgroup' => $group)); + } + else { + $result = db_query("SELECT s.lid, s.source, s.context, s.location, t.plid, t.plural FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid WHERE s.textgroup = :textgroup ORDER BY t.plid, t.plural", array(':textgroup' => $group)); + } + $strings = array(); + foreach ($result as $child) { + $string = array( + 'comment' => $child->location, + 'source' => $child->source, + 'context' => $child->context, + 'translation' => isset($child->translation) ? $child->translation : '', + ); + if ($child->plid) { + // Has a parent lid. Since we process in the order of plids, + // we already have the parent in the array, so we can add the + // lid to the next plural version to it. This builds a linked + // list of plurals. + $string['child'] = TRUE; + $strings[$child->plid]['plural'] = $child->lid; + } + $strings[$child->lid] = $string; + } + return $strings; +} + +/** + * Generates the PO(T) file contents for given strings. + * + * @param $language + * Language object to generate the output for, or NULL if generating + * translation template. + * @param $strings + * Array of strings to export. See _locale_export_get_strings() + * on how it should be formatted. + * @param $header + * The header portion to use for the output file. Defaults + * are provided for PO and POT files. + */ +function _locale_export_po_generate($language = NULL, $strings = array(), $header = NULL) { + global $user; + + if (!isset($header)) { + if (isset($language)) { + $header = '# ' . $language->name . ' translation of ' . variable_get('site_name', 'Drupal') . "\n"; + $header .= '# Generated by ' . $user->name . ' <' . $user->mail . ">\n"; + $header .= "#\n"; + $header .= "msgid \"\"\n"; + $header .= "msgstr \"\"\n"; + $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; + $header .= "\"POT-Creation-Date: " . date("Y-m-d H:iO") . "\\n\"\n"; + $header .= "\"PO-Revision-Date: " . date("Y-m-d H:iO") . "\\n\"\n"; + $header .= "\"Last-Translator: NAME \\n\"\n"; + $header .= "\"Language-Team: LANGUAGE \\n\"\n"; + $header .= "\"MIME-Version: 1.0\\n\"\n"; + $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; + $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; + if ($language->formula && $language->plurals) { + $header .= "\"Plural-Forms: nplurals=" . $language->plurals . "; plural=" . strtr($language->formula, array('$' => '')) . ";\\n\"\n"; + } + } + else { + $header = "# LANGUAGE translation of PROJECT\n"; + $header .= "# Copyright (c) YEAR NAME \n"; + $header .= "#\n"; + $header .= "msgid \"\"\n"; + $header .= "msgstr \"\"\n"; + $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; + $header .= "\"POT-Creation-Date: " . date("Y-m-d H:iO") . "\\n\"\n"; + $header .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n"; + $header .= "\"Last-Translator: NAME \\n\"\n"; + $header .= "\"Language-Team: LANGUAGE \\n\"\n"; + $header .= "\"MIME-Version: 1.0\\n\"\n"; + $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; + $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; + $header .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n"; + } + } + + $output = $header . "\n"; + + foreach ($strings as $lid => $string) { + // Only process non-children, children are output below their parent. + if (!isset($string['child'])) { + if ($string['comment']) { + $output .= '#: ' . $string['comment'] . "\n"; + } + if (!empty($string['context'])) { + $output .= 'msgctxt ' . _locale_export_string($string['context']); + } + $output .= 'msgid ' . _locale_export_string($string['source']); + if (!empty($string['plural'])) { + $plural = $string['plural']; + $output .= 'msgid_plural ' . _locale_export_string($strings[$plural]['source']); + if (isset($language)) { + $translation = $string['translation']; + for ($i = 0; $i < $language->plurals; $i++) { + $output .= 'msgstr[' . $i . '] ' . _locale_export_string($translation); + if ($plural) { + $translation = _locale_export_remove_plural($strings[$plural]['translation']); + $plural = isset($strings[$plural]['plural']) ? $strings[$plural]['plural'] : 0; + } + else { + $translation = ''; + } + } + } + else { + $output .= 'msgstr[0] ""' . "\n"; + $output .= 'msgstr[1] ""' . "\n"; + } + } + else { + $output .= 'msgstr ' . _locale_export_string($string['translation']); + } + $output .= "\n"; + } + } + return $output; +} + +/** + * Write a generated PO or POT file to the output. + * + * @param $language + * Language object to generate the output for, or NULL if generating + * translation template. + * @param $output + * The PO(T) file to output as a string. See _locale_export_generate_po() + * on how it can be generated. + */ +function _locale_export_po($language = NULL, $output = NULL) { + // Log the export event. + if (isset($language)) { + $filename = $language->language . '.po'; + watchdog('locale', 'Exported %locale translation file: %filename.', array('%locale' => $language->name, '%filename' => $filename)); + } + else { + $filename = 'drupal.pot'; + watchdog('locale', 'Exported translation file: %filename.', array('%filename' => $filename)); + } + // Download the file for the client. + header("Content-Disposition: attachment; filename=$filename"); + header("Content-Type: text/plain; charset=utf-8"); + print $output; + drupal_exit(); +} + +/** + * Print out a string on multiple lines + */ +function _locale_export_string($str) { + $stri = addcslashes($str, "\0..\37\\\""); + $parts = array(); + + // Cut text into several lines + while ($stri != "") { + $i = strpos($stri, "\\n"); + if ($i === FALSE) { + $curstr = $stri; + $stri = ""; + } + else { + $curstr = substr($stri, 0, $i + 2); + $stri = substr($stri, $i + 2); + } + $curparts = explode("\n", _locale_export_wrap($curstr, 70)); + $parts = array_merge($parts, $curparts); + } + + // Multiline string + if (count($parts) > 1) { + return "\"\"\n\"" . implode("\"\n\"", $parts) . "\"\n"; + } + // Single line string + elseif (count($parts) == 1) { + return "\"$parts[0]\"\n"; + } + // No translation + else { + return "\"\"\n"; + } +} + +/** + * Custom word wrapping for Portable Object (Template) files. + */ +function _locale_export_wrap($str, $len) { + $words = explode(' ', $str); + $return = array(); + + $cur = ""; + $nstr = 1; + while (count($words)) { + $word = array_shift($words); + if ($nstr) { + $cur = $word; + $nstr = 0; + } + elseif (strlen("$cur $word") > $len) { + $return[] = $cur . " "; + $cur = $word; + } + else { + $cur = "$cur $word"; + } + } + $return[] = $cur; + + return implode("\n", $return); +} + +/** + * Removes plural index information from a string + */ +function _locale_export_remove_plural($entry) { + return preg_replace('/(@count)\[[0-9]\]/', '\\1', $entry); +} +/** + * @} End of "locale-api-import-export" + */ + +/** + * @defgroup locale-api-seek Translation search API + * @{ + * Functions to search in translation files. + * + * These functions provide the functionality to search for specific + * translations. + */ + +/** + * Perform a string search and display results in a table + */ +function _locale_translate_seek() { + $output = ''; + + // We have at least one criterion to match + if (!($query = _locale_translate_seek_query())) { + $query = array( + 'translation' => 'all', + 'group' => 'all', + 'language' => 'all', + 'string' => '', + ); + } + + $sql_query = db_select('locales_source', 's'); + $sql_query->leftJoin('locales_target', 't', 't.lid = s.lid'); + $sql_query->fields('s', array('source', 'location', 'context', 'lid', 'textgroup')); + $sql_query->fields('t', array('translation', 'language')); + + // Compute LIKE section. + switch ($query['translation']) { + case 'translated': + $sql_query->condition('t.translation', '%' . db_like($query['string']) . '%', 'LIKE'); + $sql_query->orderBy('t.translation', 'DESC'); + break; + case 'untranslated': + $sql_query->condition(db_and() + ->condition('s.source', '%' . db_like($query['string']) . '%', 'LIKE') + ->isNull('t.translation') + ); + $sql_query->orderBy('s.source'); + break; + case 'all' : + default: + $condition = db_or() + ->condition('s.source', '%' . db_like($query['string']) . '%', 'LIKE'); + if ($query['language'] != 'en') { + // Only search in translations if the language is not forced to English. + $condition->condition('t.translation', '%' . db_like($query['string']) . '%', 'LIKE'); + } + $sql_query->condition($condition); + break; + } + + $limit_language = NULL; + if ($query['language'] != 'en' && $query['language'] != 'all') { + $sql_query->condition('language', $query['language']); + $limit_language = $query['language']; + } + + // Add a condition on the text group. + if (!empty($query['group']) && $query['group'] != 'all') { + $sql_query->condition('s.textgroup', $query['group']); + } + + $sql_query = $sql_query->extend('PagerDefault')->limit(50); + $locales = $sql_query->execute(); + + $groups = module_invoke_all('locale', 'groups'); + $header = array(t('Text group'), t('String'), t('Context'), ($limit_language) ? t('Language') : t('Languages'), array('data' => t('Operations'), 'colspan' => '2')); + + $strings = array(); + foreach ($locales as $locale) { + if (!isset($strings[$locale->lid])) { + $strings[$locale->lid] = array( + 'group' => $locale->textgroup, + 'languages' => array(), + 'location' => $locale->location, + 'source' => $locale->source, + 'context' => $locale->context, + ); + } + if (isset($locale->language)) { + $strings[$locale->lid]['languages'][$locale->language] = $locale->translation; + } + } + + $rows = array(); + foreach ($strings as $lid => $string) { + $rows[] = array( + $groups[$string['group']], + array('data' => check_plain(truncate_utf8($string['source'], 150, FALSE, TRUE)) . '
' . $string['location'] . ''), + $string['context'], + array('data' => _locale_translate_language_list($string['languages'], $limit_language), 'align' => 'center'), + array('data' => l(t('edit'), "admin/config/regional/translate/edit/$lid", array('query' => drupal_get_destination())), 'class' => array('nowrap')), + array('data' => l(t('delete'), "admin/config/regional/translate/delete/$lid", array('query' => drupal_get_destination())), 'class' => array('nowrap')), + ); + } + + $output .= theme('table', array('header' => $header, 'rows' => $rows, 'empty' => t('No strings available.'))); + $output .= theme('pager'); + + return $output; +} + +/** + * Build array out of search criteria specified in request variables + */ +function _locale_translate_seek_query() { + $query = &drupal_static(__FUNCTION__); + if (!isset($query)) { + $query = array(); + $fields = array('string', 'language', 'translation', 'group'); + foreach ($fields as $field) { + if (isset($_SESSION['locale_translation_filter'][$field])) { + $query[$field] = $_SESSION['locale_translation_filter'][$field]; + } + } + } + return $query; +} + +/** + * Force the JavaScript translation file(s) to be refreshed. + * + * This function sets a refresh flag for a specified language, or all + * languages except English, if none specified. JavaScript translation + * files are rebuilt (with locale_update_js_files()) the next time a + * request is served in that language. + * + * @param $langcode + * The language code for which the file needs to be refreshed. + * + * @return + * New content of the 'javascript_parsed' variable. + */ +function _locale_invalidate_js($langcode = NULL) { + $parsed = variable_get('javascript_parsed', array()); + + if (empty($langcode)) { + // Invalidate all languages. + $languages = language_list(); + unset($languages['en']); + foreach ($languages as $lcode => $data) { + $parsed['refresh:' . $lcode] = 'waiting'; + } + } + else { + // Invalidate single language. + $parsed['refresh:' . $langcode] = 'waiting'; + } + + variable_set('javascript_parsed', $parsed); + return $parsed; +} + +/** + * (Re-)Creates the JavaScript translation file for a language. + * + * @param $language + * The language, the translation file should be (re)created for. + */ +function _locale_rebuild_js($langcode = NULL) { + if (!isset($langcode)) { + global $language; + } + else { + // Get information about the locale. + $languages = language_list(); + $language = $languages[$langcode]; + } + + // Construct the array for JavaScript translations. + // Only add strings with a translation to the translations array. + $result = db_query("SELECT s.lid, s.source, t.translation FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.location LIKE '%.js%' AND s.textgroup = :textgroup", array(':language' => $language->language, ':textgroup' => 'default')); + + $translations = array(); + foreach ($result as $data) { + $translations[$data->source] = $data->translation; + } + + // Construct the JavaScript file, if there are translations. + $data_hash = NULL; + $data = $status = ''; + if (!empty($translations)) { + + $data = "Drupal.locale = { "; + + if (!empty($language->formula)) { + $data .= "'pluralFormula': function (\$n) { return Number({$language->formula}); }, "; + } + + $data .= "'strings': " . drupal_json_encode($translations) . " };"; + $data_hash = drupal_hash_base64($data); + } + + // Construct the filepath where JS translation files are stored. + // There is (on purpose) no front end to edit that variable. + $dir = 'public://' . variable_get('locale_js_directory', 'languages'); + + // Delete old file, if we have no translations anymore, or a different file to be saved. + $changed_hash = $language->javascript != $data_hash; + if (!empty($language->javascript) && (!$data || $changed_hash)) { + file_unmanaged_delete($dir . '/' . $language->language . '_' . $language->javascript . '.js'); + $language->javascript = ''; + $status = 'deleted'; + } + + // Only create a new file if the content has changed or the original file got + // lost. + $dest = $dir . '/' . $language->language . '_' . $data_hash . '.js'; + if ($data && ($changed_hash || !file_exists($dest))) { + // Ensure that the directory exists and is writable, if possible. + file_prepare_directory($dir, FILE_CREATE_DIRECTORY); + + // Save the file. + if (file_unmanaged_save_data($data, $dest)) { + $language->javascript = $data_hash; + // If we deleted a previous version of the file and we replace it with a + // new one we have an update. + if ($status == 'deleted') { + $status = 'updated'; + } + // If the file did not exist previously and the data has changed we have + // a fresh creation. + elseif ($changed_hash) { + $status = 'created'; + } + // If the data hash is unchanged the translation was lost and has to be + // rebuilt. + else { + $status = 'rebuilt'; + } + } + else { + $language->javascript = ''; + $status = 'error'; + } + } + + // Save the new JavaScript hash (or an empty value if the file just got + // deleted). Act only if some operation was executed that changed the hash + // code. + if ($status && $changed_hash) { + db_update('languages') + ->fields(array( + 'javascript' => $language->javascript, + )) + ->condition('language', $language->language) + ->execute(); + + // Update the default language variable if the default language has been altered. + // This is necessary to keep the variable consistent with the database + // version of the language and to prevent checking against an outdated hash. + $default_langcode = language_default('language'); + if ($default_langcode == $language->language) { + $default = db_query("SELECT * FROM {languages} WHERE language = :language", array(':language' => $default_langcode))->fetchObject(); + variable_set('language_default', $default); + } + } + + // Log the operation and return success flag. + switch ($status) { + case 'updated': + watchdog('locale', 'Updated JavaScript translation file for the language %language.', array('%language' => t($language->name))); + return TRUE; + case 'rebuilt': + watchdog('locale', 'JavaScript translation file %file.js was lost.', array('%file' => $language->javascript), LOG_WARNING); + // Proceed to the 'created' case as the JavaScript translation file has + // been created again. + case 'created': + watchdog('locale', 'Created JavaScript translation file for the language %language.', array('%language' => t($language->name))); + return TRUE; + case 'deleted': + watchdog('locale', 'Removed JavaScript translation file for the language %language, because no translations currently exist for that language.', array('%language' => t($language->name))); + return TRUE; + case 'error': + watchdog('locale', 'An error occurred during creation of the JavaScript translation file for the language %language.', array('%language' => t($language->name)), LOG_ERR); + return FALSE; + default: + // No operation needed. + return TRUE; + } +} + +/** + * List languages in search result table + */ +function _locale_translate_language_list($translation, $limit_language) { + // Add CSS. + drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css'); + + $languages = language_list(); + unset($languages['en']); + $output = ''; + foreach ($languages as $langcode => $language) { + if (!$limit_language || $limit_language == $langcode) { + $output .= (!empty($translation[$langcode])) ? $langcode . ' ' : "$langcode "; + } + } + + return $output; +} +/** + * @} End of "locale-api-seek" + */ + +/** + * @defgroup locale-api-predefined List of predefined languages + * @{ + * API to provide a list of predefined languages. + */ + +/** + * Prepares the language code list for a select form item with only the unsupported ones + */ +function _locale_prepare_predefined_list() { + include_once DRUPAL_ROOT . '/core/includes/iso.inc'; + $languages = language_list(); + $predefined = _locale_get_predefined_list(); + foreach ($predefined as $key => $value) { + if (isset($languages[$key])) { + unset($predefined[$key]); + continue; + } + // Include native name in output, if possible + if (count($value) > 1) { + $tname = t($value[0]); + $predefined[$key] = ($tname == $value[1]) ? $tname : "$tname ($value[1])"; + } + else { + $predefined[$key] = t($value[0]); + } + } + asort($predefined); + return $predefined; +} + +/** + * @} End of "locale-api-languages-predefined" + */ + +/** + * @defgroup locale-autoimport Automatic interface translation import + * @{ + * Functions to create batches for importing translations. + * + * These functions can be used to import translations for installed + * modules. + */ + +/** + * Prepare a batch to import translations for all enabled + * modules in a given language. + * + * @param $langcode + * Language code to import translations for. + * @param $finished + * Optional finished callback for the batch. + * @param $skip + * Array of component names to skip. Used in the installer for the + * second pass import, when most components are already imported. + * + * @return + * A batch structure or FALSE if no files found. + */ +function locale_batch_by_language($langcode, $finished = NULL, $skip = array()) { + // Collect all files to import for all enabled modules and themes. + $files = array(); + $components = array(); + $query = db_select('system', 's'); + $query->fields('s', array('name', 'filename')); + $query->condition('s.status', 1); + if (count($skip)) { + $query->condition('name', $skip, 'NOT IN'); + } + $result = $query->execute(); + foreach ($result as $component) { + // Collect all files for all components, names as $langcode.po or + // with names ending with $langcode.po. This allows for filenames + // like node-module.de.po to let translators use small files and + // be able to import in smaller chunks. + $files = array_merge($files, file_scan_directory(dirname($component->filename) . '/translations', '/(^|\.)' . $langcode . '\.po$/', array('recurse' => FALSE))); + $components[] = $component->name; + } + + return _locale_batch_build($files, $finished, $components); +} + +/** + * Prepare a batch to run when installing modules or enabling themes. + * + * This batch will import translations for the newly added components + * in all the languages already set up on the site. + * + * @param $components + * An array of component (theme and/or module) names to import + * translations for. + * @param $finished + * Optional finished callback for the batch. + */ +function locale_batch_by_component($components, $finished = '_locale_batch_system_finished') { + $files = array(); + $languages = language_list('enabled'); + unset($languages[1]['en']); + if (count($languages[1])) { + $language_list = join('|', array_keys($languages[1])); + // Collect all files to import for all $components. + $result = db_query("SELECT name, filename FROM {system} WHERE status = 1"); + foreach ($result as $component) { + if (in_array($component->name, $components)) { + // Collect all files for this component in all enabled languages, named + // as $langcode.po or with names ending with $langcode.po. This allows + // for filenames like node-module.de.po to let translators use small + // files and be able to import in smaller chunks. + $files = array_merge($files, file_scan_directory(dirname($component->filename) . '/translations', '/(^|\.)(' . $language_list . ')\.po$/', array('recurse' => FALSE))); + } + } + return _locale_batch_build($files, $finished); + } + return FALSE; +} + +/** + * Build a locale batch from an array of files. + * + * @param $files + * Array of files to import. + * @param $finished + * Optional finished callback for the batch. + * @param $components + * Optional list of component names the batch covers. Used in the installer. + * + * @return + * A batch structure. + */ +function _locale_batch_build($files, $finished = NULL, $components = array()) { + $t = get_t(); + if (count($files)) { + $operations = array(); + foreach ($files as $file) { + // We call _locale_batch_import for every batch operation. + $operations[] = array('_locale_batch_import', array($file->uri)); + } + $batch = array( + 'operations' => $operations, + 'title' => $t('Importing interface translations'), + 'init_message' => $t('Starting import'), + 'error_message' => $t('Error importing interface translations'), + 'file' => 'core/includes/locale.inc', + // This is not a batch API construct, but data passed along to the + // installer, so we know what did we import already. + '#components' => $components, + ); + if (isset($finished)) { + $batch['finished'] = $finished; + } + return $batch; + } + return FALSE; +} + +/** + * Perform interface translation import as a batch step. + * + * @param $filepath + * Path to a file to import. + * @param $results + * Contains a list of files imported. + */ +function _locale_batch_import($filepath, &$context) { + // The filename is either {langcode}.po or {prefix}.{langcode}.po, so + // we can extract the language code to use for the import from the end. + if (preg_match('!(/|\.)([^\./]+)\.po$!', $filepath, $langcode)) { + $file = (object) array('filename' => basename($filepath), 'uri' => $filepath); + _locale_import_read_po('db-store', $file, LOCALE_IMPORT_KEEP, $langcode[2]); + $context['results'][] = $filepath; + } +} + +/** + * Finished callback of system page locale import batch. + * Inform the user of translation files imported. + */ +function _locale_batch_system_finished($success, $results) { + if ($success) { + drupal_set_message(format_plural(count($results), 'One translation file imported for the newly installed modules.', '@count translation files imported for the newly installed modules.')); + } +} + +/** + * Finished callback of language addition locale import batch. + * Inform the user of translation files imported. + */ +function _locale_batch_language_finished($success, $results) { + if ($success) { + drupal_set_message(format_plural(count($results), 'One translation file imported for the enabled modules.', '@count translation files imported for the enabled modules.')); + } +} + +/** + * @} End of "locale-autoimport" + */ + +/** + * Get list of all predefined and custom countries. + * + * @return + * An array of all country code => country name pairs. + */ +function country_get_list() { + include_once DRUPAL_ROOT . '/core/includes/iso.inc'; + $countries = _country_get_predefined_list(); + // Allow other modules to modify the country list. + drupal_alter('countries', $countries); + return $countries; +} + +/** + * Save locale specific date formats to the database. + * + * @param $langcode + * Language code, can be 2 characters, e.g. 'en' or 5 characters, e.g. + * 'en-CA'. + * @param $type + * Date format type, e.g. 'short', 'medium'. + * @param $format + * The date format string. + */ +function locale_date_format_save($langcode, $type, $format) { + $locale_format = array(); + $locale_format['language'] = $langcode; + $locale_format['type'] = $type; + $locale_format['format'] = $format; + + $is_existing = (bool) db_query_range('SELECT 1 FROM {date_format_locale} WHERE language = :langcode AND type = :type', 0, 1, array(':langcode' => $langcode, ':type' => $type))->fetchField(); + if ($is_existing) { + $keys = array('type', 'language'); + drupal_write_record('date_format_locale', $locale_format, $keys); + } + else { + drupal_write_record('date_format_locale', $locale_format); + } +} + +/** + * Select locale date format details from database. + * + * @param $languages + * An array of language codes. + * + * @return + * An array of date formats. + */ +function locale_get_localized_date_format($languages) { + $formats = array(); + + // Get list of different format types. + $format_types = system_get_date_types(); + $short_default = variable_get('date_format_short', 'm/d/Y - H:i'); + + // Loop through each language until we find one with some date formats + // configured. + foreach ($languages as $language) { + $date_formats = system_date_format_locale($language); + if (!empty($date_formats)) { + // We have locale-specific date formats, so check for their types. If + // we're missing a type, use the default setting instead. + foreach ($format_types as $type => $type_info) { + // If format exists for this language, use it. + if (!empty($date_formats[$type])) { + $formats['date_format_' . $type] = $date_formats[$type]; + } + // Otherwise get default variable setting. If this is not set, default + // to the short format. + else { + $formats['date_format_' . $type] = variable_get('date_format_' . $type, $short_default); + } + } + + // Return on the first match. + return $formats; + } + } + + // No locale specific formats found, so use defaults. + $system_types = array('short', 'medium', 'long'); + // Handle system types separately as they have defaults if no variable exists. + $formats['date_format_short'] = $short_default; + $formats['date_format_medium'] = variable_get('date_format_medium', 'D, m/d/Y - H:i'); + $formats['date_format_long'] = variable_get('date_format_long', 'l, F j, Y - H:i'); + + // For non-system types, get the default setting, otherwise use the short + // format. + foreach ($format_types as $type => $type_info) { + if (!in_array($type, $system_types)) { + $formats['date_format_' . $type] = variable_get('date_format_' . $type, $short_default); + } + } + + return $formats; +} diff --git a/includes/lock.inc b/core/includes/lock.inc similarity index 100% rename from includes/lock.inc rename to core/includes/lock.inc diff --git a/includes/mail.inc b/core/includes/mail.inc similarity index 100% rename from includes/mail.inc rename to core/includes/mail.inc diff --git a/includes/menu.inc b/core/includes/menu.inc similarity index 100% rename from includes/menu.inc rename to core/includes/menu.inc diff --git a/core/includes/module.inc b/core/includes/module.inc new file mode 100644 index 0000000..e478cd9 --- /dev/null +++ b/core/includes/module.inc @@ -0,0 +1,1007 @@ + $module) { + drupal_get_filename('module', $name, $module['filename']); + $list[$name] = $name; + } + } + else { + if ($refresh) { + // For the $refresh case, make sure that system_list() returns fresh + // data. + drupal_static_reset('system_list'); + } + if ($bootstrap_refresh) { + $list = system_list('bootstrap'); + } + else { + // Not using drupal_map_assoc() here as that requires common.inc. + $list = array_keys(system_list('module_enabled')); + $list = (!empty($list) ? array_combine($list, $list) : array()); + } + } + } + if ($sort) { + if (!isset($sorted_list)) { + $sorted_list = $list; + ksort($sorted_list); + } + return $sorted_list; + } + return $list; +} + +/** + * Build a list of bootstrap modules and enabled modules and themes. + * + * @param $type + * The type of list to return: + * - module_enabled: All enabled modules. + * - bootstrap: All enabled modules required for bootstrap. + * - theme: All themes. + * + * @return + * An associative array of modules or themes, keyed by name. For $type + * 'bootstrap', the array values equal the keys. For $type 'module_enabled' + * or 'theme', the array values are objects representing the respective + * database row, with the 'info' property already unserialized. + * + * @see module_list() + * @see list_themes() + */ +function system_list($type) { + $lists = &drupal_static(__FUNCTION__); + + // For bootstrap modules, attempt to fetch the list from cache if possible. + // if not fetch only the required information to fire bootstrap hooks + // in case we are going to serve the page from cache. + if ($type == 'bootstrap') { + if (isset($lists['bootstrap'])) { + return $lists['bootstrap']; + } + if ($cached = cache_get('bootstrap_modules', 'cache_bootstrap')) { + $bootstrap_list = $cached->data; + } + else { + $bootstrap_list = db_query("SELECT name, filename FROM {system} WHERE status = 1 AND bootstrap = 1 AND type = 'module' ORDER BY weight ASC, name ASC")->fetchAllAssoc('name'); + cache_set('bootstrap_modules', $bootstrap_list, 'cache_bootstrap'); + } + // To avoid a separate database lookup for the filepath, prime the + // drupal_get_filename() static cache for bootstrap modules only. + // The rest is stored separately to keep the bootstrap module cache small. + foreach ($bootstrap_list as $module) { + drupal_get_filename('module', $module->name, $module->filename); + } + // We only return the module names here since module_list() doesn't need + // the filename itself. + $lists['bootstrap'] = array_keys($bootstrap_list); + } + // Otherwise build the list for enabled modules and themes. + elseif (!isset($lists['module_enabled'])) { + if ($cached = cache_get('system_list', 'cache_bootstrap')) { + $lists = $cached->data; + } + else { + $lists = array( + 'module_enabled' => array(), + 'theme' => array(), + 'filepaths' => array(), + ); + // The module name (rather than the filename) is used as the fallback + // weighting in order to guarantee consistent behavior across different + // Drupal installations, which might have modules installed in different + // locations in the file system. The ordering here must also be + // consistent with the one used in module_implements(). + $result = db_query("SELECT * FROM {system} WHERE type = 'theme' OR (type = 'module' AND status = 1) ORDER BY weight ASC, name ASC"); + foreach ($result as $record) { + $record->info = unserialize($record->info); + // Build a list of all enabled modules. + if ($record->type == 'module') { + $lists['module_enabled'][$record->name] = $record; + } + // Build a list of themes. + if ($record->type == 'theme') { + $lists['theme'][$record->name] = $record; + } + // Build a list of filenames so drupal_get_filename can use it. + if ($record->status) { + $lists['filepaths'][] = array('type' => $record->type, 'name' => $record->name, 'filepath' => $record->filename); + } + } + cache_set('system_list', $lists, 'cache_bootstrap'); + } + // To avoid a separate database lookup for the filepath, prime the + // drupal_get_filename() static cache with all enabled modules and themes. + foreach ($lists['filepaths'] as $item) { + drupal_get_filename($item['type'], $item['name'], $item['filepath']); + } + } + + return $lists[$type]; +} + +/** + * Reset all system_list() caches. + */ +function system_list_reset() { + drupal_static_reset('system_list'); + drupal_static_reset('system_rebuild_module_data'); + drupal_static_reset('list_themes'); + cache_clear_all('bootstrap_modules', 'cache_bootstrap'); + cache_clear_all('system_list', 'cache_bootstrap'); +} + +/** + * Find dependencies any level deep and fill in required by information too. + * + * @param $files + * The array of filesystem objects used to rebuild the cache. + * + * @return + * The same array with the new keys for each module: + * - requires: An array with the keys being the modules that this module + * requires. + * - required_by: An array with the keys being the modules that will not work + * without this module. + */ +function _module_build_dependencies($files) { + require_once DRUPAL_ROOT . '/core/includes/graph.inc'; + foreach ($files as $filename => $file) { + $graph[$file->name]['edges'] = array(); + if (isset($file->info['dependencies']) && is_array($file->info['dependencies'])) { + foreach ($file->info['dependencies'] as $dependency) { + $dependency_data = drupal_parse_dependency($dependency); + $graph[$file->name]['edges'][$dependency_data['name']] = $dependency_data; + } + } + } + drupal_depth_first_search($graph); + foreach ($graph as $module => $data) { + $files[$module]->required_by = isset($data['reverse_paths']) ? $data['reverse_paths'] : array(); + $files[$module]->requires = isset($data['paths']) ? $data['paths'] : array(); + $files[$module]->sort = $data['weight']; + } + return $files; +} + +/** + * Determine whether a given module exists. + * + * @param $module + * The name of the module (without the .module extension). + * + * @return + * TRUE if the module is both installed and enabled. + */ +function module_exists($module) { + $list = module_list(); + return isset($list[$module]); +} + +/** + * Load a module's installation hooks. + * + * @param $module + * The name of the module (without the .module extension). + * + * @return + * The name of the module's install file, if successful; FALSE otherwise. + */ +function module_load_install($module) { + // Make sure the installation API is available + include_once DRUPAL_ROOT . '/core/includes/install.inc'; + + return module_load_include('install', $module); +} + +/** + * Load a module include file. + * + * Examples: + * @code + * // Load node.admin.inc from the node module. + * module_load_include('inc', 'node', 'node.admin'); + * // Load content_types.inc from the node module. + * module_load_include('inc', 'node', 'content_types'); + * @endcode + * + * Do not use this function to load an install file, use module_load_install() + * instead. Do not use this function in a global context since it requires + * Drupal to be fully bootstrapped, use require_once DRUPAL_ROOT . '/path/file' + * instead. + * + * @param $type + * The include file's type (file extension). + * @param $module + * The module to which the include file belongs. + * @param $name + * (optional) The base file name (without the $type extension). If omitted, + * $module is used; i.e., resulting in "$module.$type" by default. + * + * @return + * The name of the included file, if successful; FALSE otherwise. + */ +function module_load_include($type, $module, $name = NULL) { + if (!isset($name)) { + $name = $module; + } + + if (function_exists('drupal_get_path')) { + $file = DRUPAL_ROOT . '/' . drupal_get_path('module', $module) . "/$name.$type"; + if (is_file($file)) { + require_once $file; + return $file; + } + } + return FALSE; +} + +/** + * Load an include file for each of the modules that have been enabled in + * the system table. + */ +function module_load_all_includes($type, $name = NULL) { + $modules = module_list(); + foreach ($modules as $module) { + module_load_include($type, $module, $name); + } +} + +/** + * Enables or installs a given list of modules. + * + * Definitions: + * - "Enabling" is the process of activating a module for use by Drupal. + * - "Disabling" is the process of deactivating a module. + * - "Installing" is the process of enabling it for the first time or after it + * has been uninstalled. + * - "Uninstalling" is the process of removing all traces of a module. + * + * Order of events: + * - Gather and add module dependencies to $module_list (if applicable). + * - For each module that is being enabled: + * - Install module schema and update system registries and caches. + * - If the module is being enabled for the first time or had been + * uninstalled, invoke hook_install() and add it to the list of installed + * modules. + * - Invoke hook_enable(). + * - Invoke hook_modules_installed(). + * - Invoke hook_modules_enabled(). + * + * @param $module_list + * An array of module names. + * @param $enable_dependencies + * If TRUE, dependencies will automatically be added and enabled in the + * correct order. This incurs a significant performance cost, so use FALSE + * if you know $module_list is already complete and in the correct order. + * + * @return + * FALSE if one or more dependencies are missing, TRUE otherwise. + * + * @see hook_install() + * @see hook_enable() + * @see hook_modules_installed() + * @see hook_modules_enabled() + */ +function module_enable($module_list, $enable_dependencies = TRUE) { + if ($enable_dependencies) { + // Get all module data so we can find dependencies and sort. + $module_data = system_rebuild_module_data(); + // Create an associative array with weights as values. + $module_list = array_flip(array_values($module_list)); + + while (list($module) = each($module_list)) { + if (!isset($module_data[$module])) { + // This module is not found in the filesystem, abort. + return FALSE; + } + if ($module_data[$module]->status) { + // Skip already enabled modules. + unset($module_list[$module]); + continue; + } + $module_list[$module] = $module_data[$module]->sort; + + // Add dependencies to the list, with a placeholder weight. + // The new modules will be processed as the while loop continues. + foreach (array_keys($module_data[$module]->requires) as $dependency) { + if (!isset($module_list[$dependency])) { + $module_list[$dependency] = 0; + } + } + } + + if (!$module_list) { + // Nothing to do. All modules already enabled. + return TRUE; + } + + // Sort the module list by pre-calculated weights. + arsort($module_list); + $module_list = array_keys($module_list); + } + + // Required for module installation checks. + include_once DRUPAL_ROOT . '/core/includes/install.inc'; + + $modules_installed = array(); + $modules_enabled = array(); + foreach ($module_list as $module) { + // Only process modules that are not already enabled. + $existing = db_query("SELECT status FROM {system} WHERE type = :type AND name = :name", array( + ':type' => 'module', + ':name' => $module)) + ->fetchObject(); + if ($existing->status == 0) { + // Load the module's code. + drupal_load('module', $module); + module_load_install($module); + + // Update the database and module list to reflect the new module. This + // needs to be done first so that the module's hook implementations, + // hook_schema() in particular, can be called while it is being + // installed. + db_update('system') + ->fields(array('status' => 1)) + ->condition('type', 'module') + ->condition('name', $module) + ->execute(); + // Refresh the module list to include it. + system_list_reset(); + module_list(TRUE); + module_implements('', FALSE, TRUE); + _system_update_bootstrap_status(); + // Update the registry to include it. + registry_update(); + // Refresh the schema to include it. + drupal_get_schema(NULL, TRUE); + // Clear entity cache. + entity_info_cache_clear(); + + // Now install the module if necessary. + if (drupal_get_installed_schema_version($module, TRUE) == SCHEMA_UNINSTALLED) { + drupal_install_schema($module); + + // Set the schema version to the number of the last update provided + // by the module. + $versions = drupal_get_schema_versions($module); + $version = $versions ? max($versions) : SCHEMA_INSTALLED; + + // If the module has no current updates, but has some that were + // previously removed, set the version to the value of + // hook_update_last_removed(). + if ($last_removed = module_invoke($module, 'update_last_removed')) { + $version = max($version, $last_removed); + } + drupal_set_installed_schema_version($module, $version); + // Allow the module to perform install tasks. + module_invoke($module, 'install'); + // Record the fact that it was installed. + $modules_installed[] = $module; + watchdog('system', '%module module installed.', array('%module' => $module), LOG_INFO); + } + + // Enable the module. + module_invoke($module, 'enable'); + + // Record the fact that it was enabled. + $modules_enabled[] = $module; + watchdog('system', '%module module enabled.', array('%module' => $module), LOG_INFO); + } + } + + // If any modules were newly installed, invoke hook_modules_installed(). + if (!empty($modules_installed)) { + module_invoke_all('modules_installed', $modules_installed); + } + + // If any modules were newly enabled, invoke hook_modules_enabled(). + if (!empty($modules_enabled)) { + module_invoke_all('modules_enabled', $modules_enabled); + } + + return TRUE; +} + +/** + * Disable a given set of modules. + * + * @param $module_list + * An array of module names. + * @param $disable_dependents + * If TRUE, dependent modules will automatically be added and disabled in the + * correct order. This incurs a significant performance cost, so use FALSE + * if you know $module_list is already complete and in the correct order. + */ +function module_disable($module_list, $disable_dependents = TRUE) { + if ($disable_dependents) { + // Get all module data so we can find dependents and sort. + $module_data = system_rebuild_module_data(); + // Create an associative array with weights as values. + $module_list = array_flip(array_values($module_list)); + + $profile = drupal_get_profile(); + while (list($module) = each($module_list)) { + if (!isset($module_data[$module]) || !$module_data[$module]->status) { + // This module doesn't exist or is already disabled, skip it. + unset($module_list[$module]); + continue; + } + $module_list[$module] = $module_data[$module]->sort; + + // Add dependent modules to the list, with a placeholder weight. + // The new modules will be processed as the while loop continues. + foreach ($module_data[$module]->required_by as $dependent => $dependent_data) { + if (!isset($module_list[$dependent]) && $dependent != $profile) { + $module_list[$dependent] = 0; + } + } + } + + // Sort the module list by pre-calculated weights. + asort($module_list); + $module_list = array_keys($module_list); + } + + $invoke_modules = array(); + + foreach ($module_list as $module) { + if (module_exists($module)) { + // Check if node_access table needs rebuilding. + if (!node_access_needs_rebuild() && module_hook($module, 'node_grants')) { + node_access_needs_rebuild(TRUE); + } + + module_load_install($module); + module_invoke($module, 'disable'); + db_update('system') + ->fields(array('status' => 0)) + ->condition('type', 'module') + ->condition('name', $module) + ->execute(); + $invoke_modules[] = $module; + watchdog('system', '%module module disabled.', array('%module' => $module), LOG_INFO); + } + } + + if (!empty($invoke_modules)) { + // Refresh the module list to exclude the disabled modules. + system_list_reset(); + module_list(TRUE); + module_implements('', FALSE, TRUE); + // Invoke hook_modules_disabled before disabling modules, + // so we can still call module hooks to get information. + module_invoke_all('modules_disabled', $invoke_modules); + // Update the registry to remove the newly-disabled module. + registry_update(); + _system_update_bootstrap_status(); + } + + // If there remains no more node_access module, rebuilding will be + // straightforward, we can do it right now. + if (node_access_needs_rebuild() && count(module_implements('node_grants')) == 0) { + node_access_rebuild(); + } +} + +/** + * @defgroup hooks Hooks + * @{ + * Allow modules to interact with the Drupal core. + * + * Drupal's module system is based on the concept of "hooks". A hook is a PHP + * function that is named foo_bar(), where "foo" is the name of the module + * (whose filename is thus foo.module) and "bar" is the name of the hook. Each + * hook has a defined set of parameters and a specified result type. + * + * To extend Drupal, a module need simply implement a hook. When Drupal wishes + * to allow intervention from modules, it determines which modules implement a + * hook and calls that hook in all enabled modules that implement it. + * + * The available hooks to implement are explained here in the Hooks section of + * the developer documentation. The string "hook" is used as a placeholder for + * the module name in the hook definitions. For example, if the module file is + * called example.module, then hook_help() as implemented by that module would + * be defined as example_help(). + * + * The example functions included are not part of the Drupal core, they are + * just models that you can modify. Only the hooks implemented within modules + * are executed when running Drupal. + * + * See also @link themeable the themeable group page. @endlink + */ + +/** + * Determine whether a module implements a hook. + * + * @param $module + * The name of the module (without the .module extension). + * @param $hook + * The name of the hook (e.g. "help" or "menu"). + * + * @return + * TRUE if the module is both installed and enabled, and the hook is + * implemented in that module. + */ +function module_hook($module, $hook) { + $function = $module . '_' . $hook; + if (function_exists($function)) { + return TRUE; + } + // If the hook implementation does not exist, check whether it may live in an + // optional include file registered via hook_hook_info(). + $hook_info = module_hook_info(); + if (isset($hook_info[$hook]['group'])) { + module_load_include('inc', $module, $module . '.' . $hook_info[$hook]['group']); + if (function_exists($function)) { + return TRUE; + } + } + return FALSE; +} + +/** + * Determine which modules are implementing a hook. + * + * @param $hook + * The name of the hook (e.g. "help" or "menu"). + * @param $sort + * By default, modules are ordered by weight and filename, settings this option + * to TRUE, module list will be ordered by module name. + * @param $reset + * For internal use only: Whether to force the stored list of hook + * implementations to be regenerated (such as after enabling a new module, + * before processing hook_enable). + * + * @return + * An array with the names of the modules which are implementing this hook. + * + * @see module_implements_write_cache() + */ +function module_implements($hook, $sort = FALSE, $reset = FALSE) { + // Use the advanced drupal_static() pattern, since this is called very often. + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['implementations'] = &drupal_static(__FUNCTION__); + } + $implementations = &$drupal_static_fast['implementations']; + + // We maintain a persistent cache of hook implementations in addition to the + // static cache to avoid looping through every module and every hook on each + // request. Benchmarks show that the benefit of this caching outweighs the + // additional database hit even when using the default database caching + // backend and only a small number of modules are enabled. The cost of the + // cache_get() is more or less constant and reduced further when non-database + // caching backends are used, so there will be more significant gains when a + // large number of modules are installed or hooks invoked, since this can + // quickly lead to module_hook() being called several thousand times + // per request. + if ($reset) { + $implementations = array(); + cache_set('module_implements', array(), 'cache_bootstrap'); + drupal_static_reset('module_hook_info'); + drupal_static_reset('drupal_alter'); + cache_clear_all('hook_info', 'cache_bootstrap'); + return; + } + + // Fetch implementations from cache. + if (empty($implementations)) { + $implementations = cache_get('module_implements', 'cache_bootstrap'); + if ($implementations === FALSE) { + $implementations = array(); + } + else { + $implementations = $implementations->data; + } + } + + if (!isset($implementations[$hook])) { + // The hook is not cached, so ensure that whether or not it has + // implementations, that the cache is updated at the end of the request. + $implementations['#write_cache'] = TRUE; + $hook_info = module_hook_info(); + $implementations[$hook] = array(); + $list = module_list(FALSE, FALSE, $sort); + foreach ($list as $module) { + $include_file = isset($hook_info[$hook]['group']) && module_load_include('inc', $module, $module . '.' . $hook_info[$hook]['group']); + // Since module_hook() may needlessly try to load the include file again, + // function_exists() is used directly here. + if (function_exists($module . '_' . $hook)) { + $implementations[$hook][$module] = $include_file ? $hook_info[$hook]['group'] : FALSE; + } + } + // Allow modules to change the weight of specific implementations but avoid + // an infinite loop. + if ($hook != 'module_implements_alter') { + drupal_alter('module_implements', $implementations[$hook], $hook); + } + } + else { + foreach ($implementations[$hook] as $module => $group) { + // If this hook implementation is stored in a lazy-loaded file, so include + // that file first. + if ($group) { + module_load_include('inc', $module, "$module.$group"); + } + // It is possible that a module removed a hook implementation without the + // implementations cache being rebuilt yet, so we check whether the + // function exists on each request to avoid undefined function errors. + // Since module_hook() may needlessly try to load the include file again, + // function_exists() is used directly here. + if (!function_exists($module . '_' . $hook)) { + // Clear out the stale implementation from the cache and force a cache + // refresh to forget about no longer existing hook implementations. + unset($implementations[$hook][$module]); + $implementations['#write_cache'] = TRUE; + } + } + } + + return array_keys($implementations[$hook]); +} + +/** + * Retrieve a list of what hooks are explicitly declared. + */ +function module_hook_info() { + // This function is indirectly invoked from bootstrap_invoke_all(), in which + // case common.inc, subsystems, and modules are not loaded yet, so it does not + // make sense to support hook groups resp. lazy-loaded include files prior to + // full bootstrap. + if (drupal_bootstrap(NULL, FALSE) != DRUPAL_BOOTSTRAP_FULL) { + return array(); + } + $hook_info = &drupal_static(__FUNCTION__); + + if (!isset($hook_info)) { + $hook_info = array(); + $cache = cache_get('hook_info', 'cache_bootstrap'); + if ($cache === FALSE) { + // Rebuild the cache and save it. + // We can't use module_invoke_all() here or it would cause an infinite + // loop. + foreach (module_list() as $module) { + $function = $module . '_hook_info'; + if (function_exists($function)) { + $result = $function(); + if (isset($result) && is_array($result)) { + $hook_info = array_merge_recursive($hook_info, $result); + } + } + } + // We can't use drupal_alter() for the same reason as above. + foreach (module_list() as $module) { + $function = $module . '_hook_info_alter'; + if (function_exists($function)) { + $function($hook_info); + } + } + cache_set('hook_info', $hook_info, 'cache_bootstrap'); + } + else { + $hook_info = $cache->data; + } + } + + return $hook_info; +} + +/** + * Writes the hook implementation cache. + * + * @see module_implements() + */ +function module_implements_write_cache() { + $implementations = &drupal_static('module_implements'); + // Check whether we need to write the cache. We do not want to cache hooks + // which are only invoked on HTTP POST requests since these do not need to be + // optimized as tightly, and not doing so keeps the cache entry smaller. + if (isset($implementations['#write_cache']) && ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD')) { + unset($implementations['#write_cache']); + cache_set('module_implements', $implementations, 'cache_bootstrap'); + } +} + +/** + * Invoke a hook in a particular module. + * + * @param $module + * The name of the module (without the .module extension). + * @param $hook + * The name of the hook to invoke. + * @param ... + * Arguments to pass to the hook implementation. + * + * @return + * The return value of the hook implementation. + */ +function module_invoke() { + $args = func_get_args(); + $module = $args[0]; + $hook = $args[1]; + unset($args[0], $args[1]); + if (module_hook($module, $hook)) { + return call_user_func_array($module . '_' . $hook, $args); + } +} + +/** + * Invoke a hook in all enabled modules that implement it. + * + * @param $hook + * The name of the hook to invoke. + * @param ... + * Arguments to pass to the hook. + * + * @return + * An array of return values of the hook implementations. If modules return + * arrays from their implementations, those are merged into one array. + */ +function module_invoke_all() { + $args = func_get_args(); + $hook = $args[0]; + unset($args[0]); + $return = array(); + foreach (module_implements($hook) as $module) { + $function = $module . '_' . $hook; + if (function_exists($function)) { + $result = call_user_func_array($function, $args); + if (isset($result) && is_array($result)) { + $return = array_merge_recursive($return, $result); + } + elseif (isset($result)) { + $return[] = $result; + } + } + } + + return $return; +} + +/** + * @} End of "defgroup hooks". + */ + +/** + * Array of modules required by core. + */ +function drupal_required_modules() { + $files = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info$/', 'modules', 'name', 0); + $required = array(); + + // An install profile is required and one must always be loaded. + $required[] = drupal_get_profile(); + + foreach ($files as $name => $file) { + $info = drupal_parse_info_file($file->uri); + if (!empty($info) && !empty($info['required']) && $info['required']) { + $required[] = $name; + } + } + + return $required; +} + +/** + * Hands off alterable variables to type-specific *_alter implementations. + * + * This dispatch function hands off the passed-in variables to type-specific + * hook_TYPE_alter() implementations in modules. It ensures a consistent + * interface for all altering operations. + * + * A maximum of 2 alterable arguments is supported. In case more arguments need + * to be passed and alterable, modules provide additional variables assigned by + * reference in the last $context argument: + * @code + * $context = array( + * 'alterable' => &$alterable, + * 'unalterable' => $unalterable, + * 'foo' => 'bar', + * ); + * drupal_alter('mymodule_data', $alterable1, $alterable2, $context); + * @endcode + * + * Note that objects are always passed by reference in PHP5. If it is absolutely + * required that no implementation alters a passed object in $context, then an + * object needs to be cloned: + * @code + * $context = array( + * 'unalterable_object' => clone $object, + * ); + * drupal_alter('mymodule_data', $data, $context); + * @endcode + * + * @param $type + * A string describing the type of the alterable $data. 'form', 'links', + * 'node_content', and so on are several examples. Alternatively can be an + * array, in which case hook_TYPE_alter() is invoked for each value in the + * array, ordered first by module, and then for each module, in the order of + * values in $type. For example, when Form API is using drupal_alter() to + * execute both hook_form_alter() and hook_form_FORM_ID_alter() + * implementations, it passes array('form', 'form_' . $form_id) for $type. + * @param $data + * The variable that will be passed to hook_TYPE_alter() implementations to be + * altered. The type of this variable depends on the value of the $type + * argument. For example, when altering a 'form', $data will be a structured + * array. When altering a 'profile', $data will be an object. + * @param $context1 + * (optional) An additional variable that is passed by reference. + * @param $context2 + * (optional) An additional variable that is passed by reference. If more + * context needs to be provided to implementations, then this should be an + * associative array as described above. + */ +function drupal_alter($type, &$data, &$context1 = NULL, &$context2 = 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['functions'] = &drupal_static(__FUNCTION__); + } + $functions = &$drupal_static_fast['functions']; + + // Most of the time, $type is passed as a string, so for performance, + // normalize it to that. When passed as an array, usually the first item in + // the array is a generic type, and additional items in the array are more + // specific variants of it, as in the case of array('form', 'form_FORM_ID'). + if (is_array($type)) { + $cid = implode(',', $type); + $extra_types = $type; + $type = array_shift($extra_types); + // Allow if statements in this function to use the faster isset() rather + // than !empty() both when $type is passed as a string, or as an array with + // one item. + if (empty($extra_types)) { + unset($extra_types); + } + } + else { + $cid = $type; + } + + // Some alter hooks are invoked many times per page request, so statically + // cache the list of functions to call, and on subsequent calls, iterate + // through them quickly. + if (!isset($functions[$cid])) { + $functions[$cid] = array(); + $hook = $type . '_alter'; + $modules = module_implements($hook); + if (!isset($extra_types)) { + // For the more common case of a single hook, we do not need to call + // function_exists(), since module_implements() returns only modules with + // implementations. + foreach ($modules as $module) { + $functions[$cid][] = $module . '_' . $hook; + } + } + else { + // For multiple hooks, we need $modules to contain every module that + // implements at least one of them. + $extra_modules = array(); + foreach ($extra_types as $extra_type) { + $extra_modules = array_merge($extra_modules, module_implements($extra_type . '_alter')); + } + // If any modules implement one of the extra hooks that do not implement + // the primary hook, we need to add them to the $modules array in their + // appropriate order. + if (array_diff($extra_modules, $modules)) { + // Order the modules by the order returned by module_list(). + $modules = array_intersect(module_list(), array_merge($modules, $extra_modules)); + } + foreach ($modules as $module) { + // Since $modules is a merged array, for any given module, we do not + // know whether it has any particular implementation, so we need a + // function_exists(). + $function = $module . '_' . $hook; + if (function_exists($function)) { + $functions[$cid][] = $function; + } + foreach ($extra_types as $extra_type) { + $function = $module . '_' . $extra_type . '_alter'; + if (function_exists($function)) { + $functions[$cid][] = $function; + } + } + } + } + // Allow the theme to alter variables after the theme system has been + // initialized. + global $theme, $base_theme_info; + if (isset($theme)) { + $theme_keys = array(); + foreach ($base_theme_info as $base) { + $theme_keys[] = $base->name; + } + $theme_keys[] = $theme; + foreach ($theme_keys as $theme_key) { + $function = $theme_key . '_' . $hook; + if (function_exists($function)) { + $functions[$cid][] = $function; + } + if (isset($extra_types)) { + foreach ($extra_types as $extra_type) { + $function = $theme_key . '_' . $extra_type . '_alter'; + if (function_exists($function)) { + $functions[$cid][] = $function; + } + } + } + } + } + } + + foreach ($functions[$cid] as $function) { + $function($data, $context1, $context2); + } +} + diff --git a/includes/pager.inc b/core/includes/pager.inc similarity index 100% rename from includes/pager.inc rename to core/includes/pager.inc diff --git a/includes/password.inc b/core/includes/password.inc similarity index 100% rename from includes/password.inc rename to core/includes/password.inc diff --git a/includes/path.inc b/core/includes/path.inc similarity index 100% rename from includes/path.inc rename to core/includes/path.inc diff --git a/core/includes/registry.inc b/core/includes/registry.inc new file mode 100644 index 0000000..4bfacb2 --- /dev/null +++ b/core/includes/registry.inc @@ -0,0 +1,186 @@ +fetchAll(); + // Get the list of files we are going to parse. + $files = array(); + foreach ($modules as &$module) { + $module->info = unserialize($module->info); + $dir = dirname($module->filename); + + // Store the module directory for use in hook_registry_files_alter(). + $module->dir = $dir; + + if ($module->status) { + // Add files for enabled modules to the registry. + foreach ($module->info['files'] as $file) { + $files["$dir/$file"] = array('module' => $module->name, 'weight' => $module->weight); + } + } + } + foreach (file_scan_directory('core/includes', '/\.inc$/') as $filename => $file) { + $files["$filename"] = array('module' => '', 'weight' => 0); + } + + $transaction = db_transaction(); + try { + // Allow modules to manually modify the list of files before the registry + // parses them. The $modules array provides the .info file information, which + // includes the list of files registered to each module. Any files in the + // list can then be added to the list of files that the registry will parse, + // or modify attributes of a file. + drupal_alter('registry_files', $files, $modules); + foreach (registry_get_parsed_files() as $filename => $file) { + // Add the hash for those files we have already parsed. + if (isset($files[$filename])) { + $files[$filename]['hash'] = $file['hash']; + } + else { + // Flush the registry of resources in files that are no longer on disc + // or are in files that no installed modules require to be parsed. + db_delete('registry') + ->condition('filename', $filename) + ->execute(); + db_delete('registry_file') + ->condition('filename', $filename) + ->execute(); + } + } + $parsed_files = _registry_parse_files($files); + + $unchanged_resources = array(); + $lookup_cache = array(); + if ($cache = cache_get('lookup_cache', 'cache_bootstrap')) { + $lookup_cache = $cache->data; + } + foreach ($lookup_cache as $key => $file) { + // If the file for this cached resource is carried over unchanged from + // the last registry build, then we can safely re-cache it. + if ($file && in_array($file, array_keys($files)) && !in_array($file, $parsed_files)) { + $unchanged_resources[$key] = $file; + } + } + module_implements('', FALSE, TRUE); + _registry_check_code(REGISTRY_RESET_LOOKUP_CACHE); + } + catch (Exception $e) { + $transaction->rollback(); + watchdog_exception('registry', $e); + throw $e; + } + + // We have some unchanged resources, warm up the cache - no need to pay + // for looking them up again. + if (count($unchanged_resources) > 0) { + cache_set('lookup_cache', $unchanged_resources, 'cache_bootstrap'); + } +} + +/** + * Return the list of files in registry_file + */ +function registry_get_parsed_files() { + $files = array(); + // We want the result as a keyed array. + $files = db_query("SELECT * FROM {registry_file}")->fetchAllAssoc('filename', PDO::FETCH_ASSOC); + return $files; +} + +/** + * Parse all files that have changed since the registry was last built, and save their function and class listings. + * + * @param $files + * The list of files to check and parse. + */ +function _registry_parse_files($files) { + $parsed_files = array(); + foreach ($files as $filename => $file) { + if (file_exists($filename)) { + $hash = hash_file('sha256', $filename); + if (empty($file['hash']) || $file['hash'] != $hash) { + // Delete registry entries for this file, so we can insert the new resources. + db_delete('registry') + ->condition('filename', $filename) + ->execute(); + $file['hash'] = $hash; + $parsed_files[$filename] = $file; + } + } + } + foreach ($parsed_files as $filename => $file) { + _registry_parse_file($filename, file_get_contents($filename), $file['module'], $file['weight']); + db_merge('registry_file') + ->key(array('filename' => $filename)) + ->fields(array( + 'hash' => $file['hash'], + )) + ->execute(); + } + return array_keys($parsed_files); +} + +/** + * Parse a file and save its function and class listings. + * + * @param $filename + * Name of the file we are going to parse. + * @param $contents + * Contents of the file we are going to parse as a string. + * @param $module + * (optional) Name of the module this file belongs to. + * @param $weight + * (optional) Weight of the module. + */ +function _registry_parse_file($filename, $contents, $module = '', $weight = 0) { + if (preg_match_all('/^\s*(?:abstract|final)?\s*(class|interface)\s+([a-zA-Z0-9_]+)/m', $contents, $matches)) { + $query = db_insert('registry')->fields(array('name', 'type', 'filename', 'module', 'weight')); + foreach ($matches[2] as $key => $name) { + $query->values(array( + 'name' => $name, + 'type' => $matches[1][$key], + 'filename' => $filename, + 'module' => $module, + 'weight' => $weight, + )); + } + $query->execute(); + } +} + +/** + * @} End of "defgroup registry". + */ + diff --git a/core/includes/session.inc b/core/includes/session.inc new file mode 100644 index 0000000..541ca67 --- /dev/null +++ b/core/includes/session.inc @@ -0,0 +1,490 @@ + $sid))->fetchObject(); + if (!$user) { + if (isset($_COOKIE[$insecure_session_name])) { + $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid AND s.uid = 0", array( + ':sid' => $_COOKIE[$insecure_session_name])) + ->fetchObject(); + } + } + } + else { + $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => $sid))->fetchObject(); + } + + // We found the client's session record and they are an authenticated, + // active user. + if ($user && $user->uid > 0 && $user->status == 1) { + // This is done to unserialize the data member of $user. + $user->data = unserialize($user->data); + + // Add roles element to $user. + $user->roles = array(); + $user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user'; + $user->roles += db_query("SELECT r.rid, r.name FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid = :uid", array(':uid' => $user->uid))->fetchAllKeyed(0, 1); + } + elseif ($user) { + // The user is anonymous or blocked. Only preserve two fields from the + // {sessions} table. + $account = drupal_anonymous_user(); + $account->session = $user->session; + $account->timestamp = $user->timestamp; + $user = $account; + } + else { + // The session has expired. + $user = drupal_anonymous_user(); + $user->session = ''; + } + + // Store the session that was read for comparison in _drupal_session_write(). + $last_read = &drupal_static('drupal_session_last_read'); + $last_read = array( + 'sid' => $sid, + 'value' => $user->session, + ); + + return $user->session; +} + +/** + * Session handler assigned by session_set_save_handler(). + * + * This function will be called by PHP to store the current user's + * session, which Drupal saves to the database. + * + * This function should not be called directly. Session data should + * instead be accessed via the $_SESSION superglobal. + * + * @param $sid + * Session ID. + * @param $value + * Serialized array of the session data. + * + * @return + * This function will always return TRUE. + */ +function _drupal_session_write($sid, $value) { + global $user, $is_https; + + // The exception handler is not active at this point, so we need to do it + // manually. + try { + if (!drupal_save_session()) { + // We don't have anything to do if we are not allowed to save the session. + return; + } + + // Check whether $_SESSION has been changed in this request. + $last_read = &drupal_static('drupal_session_last_read'); + $is_changed = !isset($last_read) || $last_read['sid'] != $sid || $last_read['value'] !== $value; + + // For performance reasons, do not update the sessions table, unless + // $_SESSION has changed or more than 180 has passed since the last update. + if ($is_changed || REQUEST_TIME - $user->timestamp > variable_get('session_write_interval', 180)) { + // Either ssid or sid or both will be added from $key below. + $fields = array( + 'uid' => $user->uid, + 'cache' => isset($user->cache) ? $user->cache : 0, + 'hostname' => ip_address(), + 'session' => $value, + 'timestamp' => REQUEST_TIME, + ); + + // Use the session ID as 'sid' and an empty string as 'ssid' by default. + // _drupal_session_read() does not allow empty strings so that's a safe + // default. + $key = array('sid' => $sid, 'ssid' => ''); + // On HTTPS connections, use the session ID as both 'sid' and 'ssid'. + if ($is_https) { + $key['ssid'] = $sid; + // The "secure pages" setting allows a site to simultaneously use both + // secure and insecure session cookies. If enabled and both cookies are + // presented then use both keys. + if (variable_get('https', FALSE)) { + $insecure_session_name = substr(session_name(), 1); + if (isset($_COOKIE[$insecure_session_name])) { + $key['sid'] = $_COOKIE[$insecure_session_name]; + } + } + } + + db_merge('sessions') + ->key($key) + ->fields($fields) + ->execute(); + } + + // Likewise, do not update access time more than once per 180 seconds. + if ($user->uid && REQUEST_TIME - $user->access > variable_get('session_write_interval', 180)) { + db_update('users') + ->fields(array( + 'access' => REQUEST_TIME + )) + ->condition('uid', $user->uid) + ->execute(); + } + + return TRUE; + } + catch (Exception $exception) { + require_once DRUPAL_ROOT . '/core/includes/errors.inc'; + // If we are displaying errors, then do so with no possibility of a further + // uncaught exception being thrown. + if (error_displayable()) { + print '

Uncaught exception thrown in session handler.

'; + print '

' . _drupal_render_exception_safe($exception) . '


'; + } + return FALSE; + } +} + +/** + * Initializes the session handler, starting a session if needed. + */ +function drupal_session_initialize() { + global $user, $is_https; + + session_set_save_handler('_drupal_session_open', '_drupal_session_close', '_drupal_session_read', '_drupal_session_write', '_drupal_session_destroy', '_drupal_session_garbage_collection'); + + // We use !empty() in the following check to ensure that blank session IDs + // are not valid. + if (!empty($_COOKIE[session_name()]) || ($is_https && variable_get('https', FALSE) && !empty($_COOKIE[substr(session_name(), 1)]))) { + // If a session cookie exists, initialize the session. Otherwise the + // session is only started on demand in drupal_session_commit(), making + // anonymous users not use a session cookie unless something is stored in + // $_SESSION. This allows HTTP proxies to cache anonymous pageviews. + drupal_session_start(); + if (!empty($user->uid) || !empty($_SESSION)) { + drupal_page_is_cacheable(FALSE); + } + } + else { + // Set a session identifier for this request. This is necessary because + // we lazily start sessions at the end of this request, and some + // processes (like drupal_get_token()) needs to know the future + // session ID in advance. + $user = drupal_anonymous_user(); + // Less random sessions (which are much faster to generate) are used for + // anonymous users than are generated in drupal_session_regenerate() when + // a user becomes authenticated. + session_id(drupal_hash_base64(uniqid(mt_rand(), TRUE))); + } + date_default_timezone_set(drupal_get_user_timezone()); +} + +/** + * Forcefully starts a session, preserving already set session data. + * + * @ingroup php_wrappers + */ +function drupal_session_start() { + // Command line clients do not support cookies nor sessions. + if (!drupal_session_started() && !drupal_is_cli()) { + // Save current session data before starting it, as PHP will destroy it. + $session_data = isset($_SESSION) ? $_SESSION : NULL; + + session_start(); + drupal_session_started(TRUE); + + // Restore session data. + if (!empty($session_data)) { + $_SESSION += $session_data; + } + } +} + +/** + * Commits the current session, if necessary. + * + * If an anonymous user already have an empty session, destroy it. + */ +function drupal_session_commit() { + global $user; + + if (!drupal_save_session()) { + // We don't have anything to do if we are not allowed to save the session. + return; + } + + if (empty($user->uid) && empty($_SESSION)) { + // There is no session data to store, destroy the session if it was + // previously started. + if (drupal_session_started()) { + session_destroy(); + } + } + else { + // There is session data to store. Start the session if it is not already + // started. + if (!drupal_session_started()) { + drupal_session_start(); + } + // Write the session data. + session_write_close(); + } +} + +/** + * Returns whether a session has been started. + */ +function drupal_session_started($set = NULL) { + static $session_started = FALSE; + if (isset($set)) { + $session_started = $set; + } + return $session_started && session_id(); +} + +/** + * Called when an anonymous user becomes authenticated or vice-versa. + * + * @ingroup php_wrappers + */ +function drupal_session_regenerate() { + global $user, $is_https; + if ($is_https && variable_get('https', FALSE)) { + $insecure_session_name = substr(session_name(), 1); + if (isset($_COOKIE[$insecure_session_name])) { + $old_insecure_session_id = $_COOKIE[$insecure_session_name]; + } + $params = session_get_cookie_params(); + $session_id = drupal_hash_base64(uniqid(mt_rand(), TRUE) . drupal_random_bytes(55)); + // If a session cookie lifetime is set, the session will expire + // $params['lifetime'] seconds from the current request. If it is not set, + // it will expire when the browser is closed. + $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; + setcookie($insecure_session_name, $session_id, $expire, $params['path'], $params['domain'], FALSE, $params['httponly']); + $_COOKIE[$insecure_session_name] = $session_id; + } + + if (drupal_session_started()) { + $old_session_id = session_id(); + } + session_id(drupal_hash_base64(uniqid(mt_rand(), TRUE) . drupal_random_bytes(55))); + + if (isset($old_session_id)) { + $params = session_get_cookie_params(); + $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; + setcookie(session_name(), session_id(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']); + $fields = array('sid' => session_id()); + if ($is_https) { + $fields['ssid'] = session_id(); + // If the "secure pages" setting is enabled, use the newly-created + // insecure session identifier as the regenerated sid. + if (variable_get('https', FALSE)) { + $fields['sid'] = $session_id; + } + } + db_update('sessions') + ->fields($fields) + ->condition($is_https ? 'ssid' : 'sid', $old_session_id) + ->execute(); + } + elseif (isset($old_insecure_session_id)) { + // If logging in to the secure site, and there was no active session on the + // secure site but a session was active on the insecure site, update the + // insecure session with the new session identifiers. + db_update('sessions') + ->fields(array('sid' => $session_id, 'ssid' => session_id())) + ->condition('sid', $old_insecure_session_id) + ->execute(); + } + else { + // Start the session when it doesn't exist yet. + // Preserve the logged in user, as it will be reset to anonymous + // by _drupal_session_read. + $account = $user; + drupal_session_start(); + $user = $account; + } + date_default_timezone_set(drupal_get_user_timezone()); +} + +/** + * Session handler assigned by session_set_save_handler(). + * + * Cleans up a specific session. + * + * @param $sid + * Session ID. + */ +function _drupal_session_destroy($sid) { + global $user, $is_https; + + // Delete session data. + db_delete('sessions') + ->condition($is_https ? 'ssid' : 'sid', $sid) + ->execute(); + + // Reset $_SESSION and $user to prevent a new session from being started + // in drupal_session_commit(). + $_SESSION = array(); + $user = drupal_anonymous_user(); + + // Unset the session cookies. + _drupal_session_delete_cookie(session_name()); + if ($is_https) { + _drupal_session_delete_cookie(substr(session_name(), 1), TRUE); + } +} + +/** + * Deletes the session cookie. + * + * @param $name + * Name of session cookie to delete. + * @param $force_insecure + * Force cookie to be insecure. + */ +function _drupal_session_delete_cookie($name, $force_insecure = FALSE) { + if (isset($_COOKIE[$name])) { + $params = session_get_cookie_params(); + setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], !$force_insecure && $params['secure'], $params['httponly']); + unset($_COOKIE[$name]); + } +} + +/** + * Ends a specific user's session(s). + * + * @param $uid + * User ID. + */ +function drupal_session_destroy_uid($uid) { + db_delete('sessions') + ->condition('uid', $uid) + ->execute(); +} + +/** + * Session handler assigned by session_set_save_handler(). + * + * Cleans up stalled sessions. + * + * @param $lifetime + * The value of session.gc_maxlifetime, passed by PHP. + * Sessions not updated for more than $lifetime seconds will be removed. + */ +function _drupal_session_garbage_collection($lifetime) { + // Be sure to adjust 'php_value session.gc_maxlifetime' to a large enough + // value. For example, if you want user sessions to stay in your database + // for three weeks before deleting them, you need to set gc_maxlifetime + // to '1814400'. At that value, only after a user doesn't log in after + // three weeks (1814400 seconds) will his/her session be removed. + db_delete('sessions') + ->condition('timestamp', REQUEST_TIME - $lifetime, '<') + ->execute(); + return TRUE; +} + +/** + * Determines whether to save session data of the current request. + * + * This function allows the caller to temporarily disable writing of + * session data, should the request end while performing potentially + * dangerous operations, such as manipulating the global $user object. + * See http://drupal.org/node/218104 for usage. + * + * @param $status + * Disables writing of session data when FALSE, (re-)enables + * writing when TRUE. + * + * @return + * FALSE if writing session data has been disabled. Otherwise, TRUE. + */ +function drupal_save_session($status = NULL) { + $save_session = &drupal_static(__FUNCTION__, TRUE); + if (isset($status)) { + $save_session = $status; + } + return $save_session; +} diff --git a/core/includes/stream_wrappers.inc b/core/includes/stream_wrappers.inc new file mode 100644 index 0000000..890bc3d --- /dev/null +++ b/core/includes/stream_wrappers.inc @@ -0,0 +1,824 @@ +uri = $uri; + } + + /** + * Base implementation of getUri(). + */ + function getUri() { + return $this->uri; + } + + /** + * Returns the local writable target of the resource within the stream. + * + * This function should be used in place of calls to realpath() or similar + * functions when attempting to determine the location of a file. While + * functions like realpath() may return the location of a read-only file, this + * method may return a URI or path suitable for writing that is completely + * separate from the URI used for reading. + * + * @param $uri + * Optional URI. + * + * @return + * Returns a string representing a location suitable for writing of a file, + * or FALSE if unable to write to the file such as with read-only streams. + */ + protected function getTarget($uri = NULL) { + if (!isset($uri)) { + $uri = $this->uri; + } + + list($scheme, $target) = explode('://', $uri, 2); + + // Remove erroneous leading or trailing, forward-slashes and backslashes. + return trim($target, '\/'); + } + + /** + * Base implementation of getMimeType(). + */ + static function getMimeType($uri, $mapping = NULL) { + if (!isset($mapping)) { + // The default file map, defined in file.mimetypes.inc is quite big. + // We only load it when necessary. + include_once DRUPAL_ROOT . '/core/includes/file.mimetypes.inc'; + $mapping = file_mimetype_mapping(); + } + + $extension = ''; + $file_parts = explode('.', basename($uri)); + + // Remove the first part: a full filename should not match an extension. + array_shift($file_parts); + + // Iterate over the file parts, trying to find a match. + // For my.awesome.image.jpeg, we try: + // - jpeg + // - image.jpeg, and + // - awesome.image.jpeg + while ($additional_part = array_pop($file_parts)) { + $extension = strtolower($additional_part . ($extension ? '.' . $extension : '')); + if (isset($mapping['extensions'][$extension])) { + return $mapping['mimetypes'][$mapping['extensions'][$extension]]; + } + } + + return 'application/octet-stream'; + } + + /** + * Base implementation of chmod(). + */ + function chmod($mode) { + return @chmod($this->getLocalPath(), $mode); + } + + /** + * Base implementation of realpath(). + */ + function realpath() { + return $this->getLocalPath(); + } + + /** + * Return the local filesystem path. + * + * @param $uri + * Optional URI, supplied when doing a move or rename. + */ + protected function getLocalPath($uri = NULL) { + if (!isset($uri)) { + $uri = $this->uri; + } + $path = $this->getDirectoryPath() . '/' . $this->getTarget($uri); + $realpath = realpath($path); + if (!$realpath) { + // This file does not yet exist. + $realpath = realpath(dirname($path)) . '/' . basename($path); + } + $directory = realpath($this->getDirectoryPath()); + if (!$realpath || !$directory || strpos($realpath, $directory) !== 0) { + return FALSE; + } + return $realpath; + } + + /** + * Support for fopen(), file_get_contents(), file_put_contents() etc. + * + * @param $uri + * A string containing the URI to the file to open. + * @param $mode + * The file mode ("r", "wb" etc.). + * @param $options + * A bit mask of STREAM_USE_PATH and STREAM_REPORT_ERRORS. + * @param $opened_path + * A string containing the path actually opened. + * + * @return + * Returns TRUE if file was opened successfully. + * + * @see http://php.net/manual/en/streamwrapper.stream-open.php + */ + public function stream_open($uri, $mode, $options, &$opened_path) { + $this->uri = $uri; + $path = $this->getLocalPath(); + $this->handle = ($options & STREAM_REPORT_ERRORS) ? fopen($path, $mode) : @fopen($path, $mode); + + if ((bool) $this->handle && $options & STREAM_USE_PATH) { + $opened_url = $path; + } + + return (bool) $this->handle; + } + + /** + * Support for flock(). + * + * @param $operation + * One of the following: + * - LOCK_SH to acquire a shared lock (reader). + * - LOCK_EX to acquire an exclusive lock (writer). + * - LOCK_UN to release a lock (shared or exclusive). + * - LOCK_NB if you don't want flock() to block while locking (not + * supported on Windows). + * + * @return + * Always returns TRUE at the present time. + * + * @see http://php.net/manual/en/streamwrapper.stream-lock.php + */ + public function stream_lock($operation) { + if (in_array($operation, array(LOCK_SH, LOCK_EX, LOCK_UN, LOCK_NB))) { + return flock($this->handle, $operation); + } + + return TRUE; + } + + /** + * Support for fread(), file_get_contents() etc. + * + * @param $count + * Maximum number of bytes to be read. + * + * @return + * The string that was read, or FALSE in case of an error. + * + * @see http://php.net/manual/en/streamwrapper.stream-read.php + */ + public function stream_read($count) { + return fread($this->handle, $count); + } + + /** + * Support for fwrite(), file_put_contents() etc. + * + * @param $data + * The string to be written. + * + * @return + * The number of bytes written (integer). + * + * @see http://php.net/manual/en/streamwrapper.stream-write.php + */ + public function stream_write($data) { + return fwrite($this->handle, $data); + } + + /** + * Support for feof(). + * + * @return + * TRUE if end-of-file has been reached. + * + * @see http://php.net/manual/en/streamwrapper.stream-eof.php + */ + public function stream_eof() { + return feof($this->handle); + } + + /** + * Support for fseek(). + * + * @param $offset + * The byte offset to got to. + * @param $whence + * SEEK_SET, SEEK_CUR, or SEEK_END. + * + * @return + * TRUE on success. + * + * @see http://php.net/manual/en/streamwrapper.stream-seek.php + */ + public function stream_seek($offset, $whence) { + // fseek returns 0 on success and -1 on a failure. + // stream_seek 1 on success and 0 on a failure. + return !fseek($this->handle, $offset, $whence); + } + + /** + * Support for fflush(). + * + * @return + * TRUE if data was successfully stored (or there was no data to store). + * + * @see http://php.net/manual/en/streamwrapper.stream-flush.php + */ + public function stream_flush() { + return fflush($this->handle); + } + + /** + * Support for ftell(). + * + * @return + * The current offset in bytes from the beginning of file. + * + * @see http://php.net/manual/en/streamwrapper.stream-tell.php + */ + public function stream_tell() { + return ftell($this->handle); + } + + /** + * Support for fstat(). + * + * @return + * An array with file status, or FALSE in case of an error - see fstat() + * for a description of this array. + * + * @see http://php.net/manual/en/streamwrapper.stream-stat.php + */ + public function stream_stat() { + return fstat($this->handle); + } + + /** + * Support for fclose(). + * + * @return + * TRUE if stream was successfully closed. + * + * @see http://php.net/manual/en/streamwrapper.stream-close.php + */ + public function stream_close() { + return fclose($this->handle); + } + + /** + * Support for unlink(). + * + * @param $uri + * A string containing the uri to the resource to delete. + * + * @return + * TRUE if resource was successfully deleted. + * + * @see http://php.net/manual/en/streamwrapper.unlink.php + */ + public function unlink($uri) { + $this->uri = $uri; + return drupal_unlink($this->getLocalPath()); + } + + /** + * Support for rename(). + * + * @param $from_uri, + * The uri to the file to rename. + * @param $to_uri + * The new uri for file. + * + * @return + * TRUE if file was successfully renamed. + * + * @see http://php.net/manual/en/streamwrapper.rename.php + */ + public function rename($from_uri, $to_uri) { + return rename($this->getLocalPath($from_uri), $this->getLocalPath($to_uri)); + } + + /** + * Gets the name of the directory from a given path. + * + * This method is usually accessed through drupal_dirname(), which wraps + * around the PHP dirname() function because it does not support stream + * wrappers. + * + * @param $uri + * A URI or path. + * + * @return + * A string containing the directory name. + * + * @see drupal_dirname() + */ + public function dirname($uri = NULL) { + list($scheme, $target) = explode('://', $uri, 2); + $target = $this->getTarget($uri); + $dirname = dirname($target); + + if ($dirname == '.') { + $dirname = ''; + } + + return $scheme . '://' . $dirname; + } + + /** + * Support for mkdir(). + * + * @param $uri + * A string containing the URI to the directory to create. + * @param $mode + * Permission flags - see mkdir(). + * @param $options + * A bit mask of STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE. + * + * @return + * TRUE if directory was successfully created. + * + * @see http://php.net/manual/en/streamwrapper.mkdir.php + */ + public function mkdir($uri, $mode, $options) { + $this->uri = $uri; + $recursive = (bool) ($options & STREAM_MKDIR_RECURSIVE); + if ($recursive) { + // $this->getLocalPath() fails if $uri has multiple levels of directories + // that do not yet exist. + $localpath = $this->getDirectoryPath() . '/' . $this->getTarget($uri); + } + else { + $localpath = $this->getLocalPath($uri); + } + if ($options & STREAM_REPORT_ERRORS) { + return mkdir($localpath, $mode, $recursive); + } + else { + return @mkdir($localpath, $mode, $recursive); + } + } + + /** + * Support for rmdir(). + * + * @param $uri + * A string containing the URI to the directory to delete. + * @param $options + * A bit mask of STREAM_REPORT_ERRORS. + * + * @return + * TRUE if directory was successfully removed. + * + * @see http://php.net/manual/en/streamwrapper.rmdir.php + */ + public function rmdir($uri, $options) { + $this->uri = $uri; + if ($options & STREAM_REPORT_ERRORS) { + return drupal_rmdir($this->getLocalPath()); + } + else { + return @drupal_rmdir($this->getLocalPath()); + } + } + + /** + * Support for stat(). + * + * @param $uri + * A string containing the URI to get information about. + * @param $flags + * A bit mask of STREAM_URL_STAT_LINK and STREAM_URL_STAT_QUIET. + * + * @return + * An array with file status, or FALSE in case of an error - see fstat() + * for a description of this array. + * + * @see http://php.net/manual/en/streamwrapper.url-stat.php + */ + public function url_stat($uri, $flags) { + $this->uri = $uri; + $path = $this->getLocalPath(); + // Suppress warnings if requested or if the file or directory does not + // exist. This is consistent with PHP's plain filesystem stream wrapper. + if ($flags & STREAM_URL_STAT_QUIET || !file_exists($path)) { + return @stat($path); + } + else { + return stat($path); + } + } + + /** + * Support for opendir(). + * + * @param $uri + * A string containing the URI to the directory to open. + * @param $options + * Unknown (parameter is not documented in PHP Manual). + * + * @return + * TRUE on success. + * + * @see http://php.net/manual/en/streamwrapper.dir-opendir.php + */ + public function dir_opendir($uri, $options) { + $this->uri = $uri; + $this->handle = opendir($this->getLocalPath()); + + return (bool) $this->handle; + } + + /** + * Support for readdir(). + * + * @return + * The next filename, or FALSE if there are no more files in the directory. + * + * @see http://php.net/manual/en/streamwrapper.dir-readdir.php + */ + public function dir_readdir() { + return readdir($this->handle); + } + + /** + * Support for rewinddir(). + * + * @return + * TRUE on success. + * + * @see http://php.net/manual/en/streamwrapper.dir-rewinddir.php + */ + public function dir_rewinddir() { + rewinddir($this->handle); + // We do not really have a way to signal a failure as rewinddir() does not + // have a return value and there is no way to read a directory handler + // without advancing to the next file. + return TRUE; + } + + /** + * Support for closedir(). + * + * @return + * TRUE on success. + * + * @see http://php.net/manual/en/streamwrapper.dir-closedir.php + */ + public function dir_closedir() { + closedir($this->handle); + // We do not really have a way to signal a failure as closedir() does not + // have a return value. + return TRUE; + } +} + +/** + * Drupal public (public://) stream wrapper class. + * + * Provides support for storing publicly accessible files with the Drupal file + * interface. + */ +class DrupalPublicStreamWrapper extends DrupalLocalStreamWrapper { + /** + * Implements abstract public function getDirectoryPath() + */ + public function getDirectoryPath() { + return variable_get('file_public_path', conf_path() . '/files'); + } + + /** + * Overrides getExternalUrl(). + * + * Return the HTML URI of a public file. + */ + function getExternalUrl() { + $path = str_replace('\\', '/', $this->getTarget()); + return $GLOBALS['base_url'] . '/' . self::getDirectoryPath() . '/' . drupal_encode_path($path); + } +} + + +/** + * Drupal private (private://) stream wrapper class. + * + * Provides support for storing privately accessible files with the Drupal file + * interface. + * + * Extends DrupalPublicStreamWrapper. + */ +class DrupalPrivateStreamWrapper extends DrupalLocalStreamWrapper { + /** + * Implements abstract public function getDirectoryPath() + */ + public function getDirectoryPath() { + return variable_get('file_private_path', ''); + } + + /** + * Overrides getExternalUrl(). + * + * Return the HTML URI of a private file. + */ + function getExternalUrl() { + $path = str_replace('\\', '/', $this->getTarget()); + return url('system/files/' . $path, array('absolute' => TRUE)); + } +} + +/** + * Drupal temporary (temporary://) stream wrapper class. + * + * Provides support for storing temporarily accessible files with the Drupal + * file interface. + * + * Extends DrupalPublicStreamWrapper. + */ +class DrupalTemporaryStreamWrapper extends DrupalLocalStreamWrapper { + /** + * Implements abstract public function getDirectoryPath() + */ + public function getDirectoryPath() { + return variable_get('file_temporary_path', file_directory_temp()); + } + + /** + * Overrides getExternalUrl(). + */ + public function getExternalUrl() { + $path = str_replace('\\', '/', $this->getTarget()); + return url('system/temporary/' . $path, array('absolute' => TRUE)); + } +} diff --git a/includes/tablesort.inc b/core/includes/tablesort.inc similarity index 100% rename from includes/tablesort.inc rename to core/includes/tablesort.inc diff --git a/core/includes/theme.inc b/core/includes/theme.inc new file mode 100644 index 0000000..5c7c5ae --- /dev/null +++ b/core/includes/theme.inc @@ -0,0 +1,2616 @@ +status) || ($admin_theme && $theme->name == $admin_theme); +} + +/** + * Initialize the theme system by loading the theme. + */ +function drupal_theme_initialize() { + global $theme, $user, $theme_key; + + // If $theme is already set, assume the others are set, too, and do nothing + if (isset($theme)) { + return; + } + + drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE); + $themes = list_themes(); + + // Only select the user selected theme if it is available in the + // list of themes that can be accessed. + $theme = !empty($user->theme) && drupal_theme_access($user->theme) ? $user->theme : variable_get('theme_default', 'bartik'); + + // Allow modules to override the theme. Validation has already been performed + // inside menu_get_custom_theme(), so we do not need to check it again here. + $custom_theme = menu_get_custom_theme(); + $theme = !empty($custom_theme) ? $custom_theme : $theme; + + // Store the identifier for retrieving theme settings with. + $theme_key = $theme; + + // Find all our ancestor themes and put them in an array. + $base_theme = array(); + $ancestor = $theme; + while ($ancestor && isset($themes[$ancestor]->base_theme)) { + $ancestor = $themes[$ancestor]->base_theme; + $base_theme[] = $themes[$ancestor]; + } + _drupal_theme_initialize($themes[$theme], array_reverse($base_theme)); + + // Themes can have alter functions, so reset the drupal_alter() cache. + drupal_static_reset('drupal_alter'); + + // Provide the page with information about the theme that's used, so that a + // later Ajax request can be rendered using the same theme. + // @see ajax_base_page_theme() + $setting['ajaxPageState'] = array( + 'theme' => $theme_key, + 'theme_token' => drupal_get_token($theme_key), + ); + drupal_add_js($setting, 'setting'); +} + +/** + * Initialize the theme system given already loaded information. This + * function is useful to initialize a theme when no database is present. + * + * @param $theme + * An object with the following information: + * filename + * The .info file for this theme. The 'path' to + * the theme will be in this file's directory. (Required) + * owner + * The path to the .theme file or the .engine file to load for + * the theme. (Required) + * stylesheet + * The primary stylesheet for the theme. (Optional) + * engine + * The name of theme engine to use. (Optional) + * @param $base_theme + * An optional array of objects that represent the 'base theme' if the + * theme is meant to be derivative of another theme. It requires + * the same information as the $theme object. It should be in + * 'oldest first' order, meaning the top level of the chain will + * be first. + * @param $registry_callback + * The callback to invoke to set the theme registry. + */ +function _drupal_theme_initialize($theme, $base_theme = array(), $registry_callback = '_theme_load_registry') { + global $theme_info, $base_theme_info, $theme_engine, $theme_path; + $theme_info = $theme; + $base_theme_info = $base_theme; + + $theme_path = dirname($theme->filename); + + // Prepare stylesheets from this theme as well as all ancestor themes. + // We work it this way so that we can have child themes override parent + // theme stylesheets easily. + $final_stylesheets = array(); + + // Grab stylesheets from base theme + foreach ($base_theme as $base) { + if (!empty($base->stylesheets)) { + foreach ($base->stylesheets as $media => $stylesheets) { + foreach ($stylesheets as $name => $stylesheet) { + $final_stylesheets[$media][$name] = $stylesheet; + } + } + } + } + + // Add stylesheets used by this theme. + if (!empty($theme->stylesheets)) { + foreach ($theme->stylesheets as $media => $stylesheets) { + foreach ($stylesheets as $name => $stylesheet) { + $final_stylesheets[$media][$name] = $stylesheet; + } + } + } + + // And now add the stylesheets properly + foreach ($final_stylesheets as $media => $stylesheets) { + foreach ($stylesheets as $stylesheet) { + drupal_add_css($stylesheet, array('group' => CSS_THEME, 'every_page' => TRUE, 'media' => $media)); + } + } + + // Do basically the same as the above for scripts + $final_scripts = array(); + + // Grab scripts from base theme + foreach ($base_theme as $base) { + if (!empty($base->scripts)) { + foreach ($base->scripts as $name => $script) { + $final_scripts[$name] = $script; + } + } + } + + // Add scripts used by this theme. + if (!empty($theme->scripts)) { + foreach ($theme->scripts as $name => $script) { + $final_scripts[$name] = $script; + } + } + + // Add scripts used by this theme. + foreach ($final_scripts as $script) { + drupal_add_js($script, array('group' => JS_THEME, 'every_page' => TRUE)); + } + + $theme_engine = NULL; + + // Initialize the theme. + if (isset($theme->engine)) { + // Include the engine. + include_once DRUPAL_ROOT . '/' . $theme->owner; + + $theme_engine = $theme->engine; + if (function_exists($theme_engine . '_init')) { + foreach ($base_theme as $base) { + call_user_func($theme_engine . '_init', $base); + } + call_user_func($theme_engine . '_init', $theme); + } + } + else { + // include non-engine theme files + foreach ($base_theme as $base) { + // Include the theme file or the engine. + if (!empty($base->owner)) { + include_once DRUPAL_ROOT . '/' . $base->owner; + } + } + // and our theme gets one too. + if (!empty($theme->owner)) { + include_once DRUPAL_ROOT . '/' . $theme->owner; + } + } + + if (isset($registry_callback)) { + _theme_registry_callback($registry_callback, array($theme, $base_theme, $theme_engine)); + } +} + +/** + * Get the theme registry. + * + * @return + * The theme registry array if it has been stored in memory, NULL otherwise. + */ +function theme_get_registry() { + static $theme_registry = NULL; + + if (!isset($theme_registry)) { + list($callback, $arguments) = _theme_registry_callback(); + $theme_registry = call_user_func_array($callback, $arguments); + } + + return $theme_registry; +} + +/** + * Set the callback that will be used by theme_get_registry() to fetch the registry. + * + * @param $callback + * The name of the callback function. + * @param $arguments + * The arguments to pass to the function. + */ +function _theme_registry_callback($callback = NULL, array $arguments = array()) { + static $stored; + if (isset($callback)) { + $stored = array($callback, $arguments); + } + return $stored; +} + +/** + * Get the theme_registry cache from the database; if it doesn't exist, build it. + * + * @param $theme + * The loaded $theme object as returned by list_themes(). + * @param $base_theme + * An array of loaded $theme objects representing the ancestor themes in + * oldest first order. + * @param theme_engine + * The name of the theme engine. + */ +function _theme_load_registry($theme, $base_theme = NULL, $theme_engine = NULL) { + // Check the theme registry cache; if it exists, use it. + $cache = cache_get("theme_registry:$theme->name", 'cache'); + if (isset($cache->data)) { + $registry = $cache->data; + } + else { + // If not, build one and cache it. + $registry = _theme_build_registry($theme, $base_theme, $theme_engine); + // Only persist this registry if all modules are loaded. This assures a + // complete set of theme hooks. + if (module_load_all(NULL)) { + _theme_save_registry($theme, $registry); + } + } + return $registry; +} + +/** + * Write the theme_registry cache into the database. + */ +function _theme_save_registry($theme, $registry) { + cache_set("theme_registry:$theme->name", $registry); +} + +/** + * Force the system to rebuild the theme registry; this should be called + * when modules are added to the system, or when a dynamic system needs + * to add more theme hooks. + */ +function drupal_theme_rebuild() { + cache_clear_all('theme_registry', 'cache', TRUE); +} + +/** + * Process a single implementation of hook_theme(). + * + * @param $cache + * The theme registry that will eventually be cached; It is an associative + * array keyed by theme hooks, whose values are associative arrays describing + * the hook: + * - 'type': The passed-in $type. + * - 'theme path': The passed-in $path. + * - 'function': The name of the function generating output for this theme + * hook. Either defined explicitly in hook_theme() or, if neither 'function' + * nor 'template' is defined, then the default theme function name is used. + * The default theme function name is the theme hook prefixed by either + * 'theme_' for modules or '$name_' for everything else. If 'function' is + * defined, 'template' is not used. + * - 'template': The filename of the template generating output for this + * theme hook. The template is in the directory defined by the 'path' key of + * hook_theme() or defaults to $path. + * - 'variables': The variables for this theme hook as defined in + * hook_theme(). If there is more than one implementation and 'variables' is + * not specified in a later one, then the previous definition is kept. + * - 'render element': The renderable element for this theme hook as defined + * in hook_theme(). If there is more than one implementation and + * 'render element' is not specified in a later one, then the previous + * definition is kept. + * - 'preprocess functions': See theme() for detailed documentation. + * - 'process functions': See theme() for detailed documentation. + * @param $name + * The name of the module, theme engine, base theme engine, theme or base + * theme implementing hook_theme(). + * @param $type + * One of 'module', 'theme_engine', 'base_theme_engine', 'theme', or + * 'base_theme'. Unlike regular hooks that can only be implemented by modules, + * each of these can implement hook_theme(). _theme_process_registry() is + * called in aforementioned order and new entries override older ones. For + * example, if a theme hook is both defined by a module and a theme, then the + * definition in the theme will be used. + * @param $theme + * The loaded $theme object as returned from list_themes(). + * @param $path + * The directory where $name is. For example, modules/system or + * themes/bartik. + * + * @see theme() + * @see _theme_process_registry() + * @see hook_theme() + * @see list_themes() + */ +function _theme_process_registry(&$cache, $name, $type, $theme, $path) { + $result = array(); + + // Processor functions work in two distinct phases with the process + // functions always being executed after the preprocess functions. + $variable_process_phases = array( + 'preprocess functions' => 'preprocess', + 'process functions' => 'process', + ); + + $hook_defaults = array( + 'variables' => TRUE, + 'render element' => TRUE, + 'pattern' => TRUE, + 'base hook' => TRUE, + ); + + // Invoke the hook_theme() implementation, process what is returned, and + // merge it into $cache. + $function = $name . '_theme'; + if (function_exists($function)) { + $result = $function($cache, $type, $theme, $path); + foreach ($result as $hook => $info) { + // When a theme or engine overrides a module's theme function + // $result[$hook] will only contain key/value pairs for information being + // overridden. Pull the rest of the information from what was defined by + // an earlier hook. + + // Fill in the type and path of the module, theme, or engine that + // implements this theme function. + $result[$hook]['type'] = $type; + $result[$hook]['theme path'] = $path; + + // If function and file are omitted, default to standard naming + // conventions. + if (!isset($info['template']) && !isset($info['function'])) { + $result[$hook]['function'] = ($type == 'module' ? 'theme_' : $name . '_') . $hook; + } + + if (isset($cache[$hook]['includes'])) { + $result[$hook]['includes'] = $cache[$hook]['includes']; + } + + // If the theme implementation defines a file, then also use the path + // that it defined. Otherwise use the default path. This allows + // system.module to declare theme functions on behalf of core .include + // files. + if (isset($info['file'])) { + $include_file = isset($info['path']) ? $info['path'] : $path; + $include_file .= '/' . $info['file']; + include_once DRUPAL_ROOT . '/' . $include_file; + $result[$hook]['includes'][] = $include_file; + } + + // If the default keys are not set, use the default values registered + // by the module. + if (isset($cache[$hook])) { + $result[$hook] += array_intersect_key($cache[$hook], $hook_defaults); + } + + // The following apply only to theming hooks implemented as templates. + if (isset($info['template'])) { + // Prepend the current theming path when none is set. + if (!isset($info['path'])) { + $result[$hook]['template'] = $path . '/' . $info['template']; + } + } + + // Allow variable processors for all theming hooks, whether the hook is + // implemented as a template or as a function. + foreach ($variable_process_phases as $phase_key => $phase) { + // Check for existing variable processors. Ensure arrayness. + if (!isset($info[$phase_key]) || !is_array($info[$phase_key])) { + $info[$phase_key] = array(); + $prefixes = array(); + if ($type == 'module') { + // Default variable processor prefix. + $prefixes[] = 'template'; + // Add all modules so they can intervene with their own variable + // processors. This allows them to provide variable processors even + // if they are not the owner of the current hook. + $prefixes += module_list(); + } + elseif ($type == 'theme_engine' || $type == 'base_theme_engine') { + // Theme engines get an extra set that come before the normally + // named variable processors. + $prefixes[] = $name . '_engine'; + // The theme engine registers on behalf of the theme using the + // theme's name. + $prefixes[] = $theme; + } + else { + // This applies when the theme manually registers their own variable + // processors. + $prefixes[] = $name; + } + foreach ($prefixes as $prefix) { + // Only use non-hook-specific variable processors for theming hooks + // implemented as templates. See theme(). + if (isset($info['template']) && function_exists($prefix . '_' . $phase)) { + $info[$phase_key][] = $prefix . '_' . $phase; + } + if (function_exists($prefix . '_' . $phase . '_' . $hook)) { + $info[$phase_key][] = $prefix . '_' . $phase . '_' . $hook; + } + } + } + // Check for the override flag and prevent the cached variable + // processors from being used. This allows themes or theme engines to + // remove variable processors set earlier in the registry build. + if (!empty($info['override ' . $phase_key])) { + // Flag not needed inside the registry. + unset($result[$hook]['override ' . $phase_key]); + } + elseif (isset($cache[$hook][$phase_key]) && is_array($cache[$hook][$phase_key])) { + $info[$phase_key] = array_merge($cache[$hook][$phase_key], $info[$phase_key]); + } + $result[$hook][$phase_key] = $info[$phase_key]; + } + } + + // Merge the newly created theme hooks into the existing cache. + $cache = $result + $cache; + } + + // Let themes have variable processors even if they didn't register a template. + if ($type == 'theme' || $type == 'base_theme') { + foreach ($cache as $hook => $info) { + // Check only if not registered by the theme or engine. + if (empty($result[$hook])) { + foreach ($variable_process_phases as $phase_key => $phase) { + if (!isset($info[$phase_key])) { + $cache[$hook][$phase_key] = array(); + } + // Only use non-hook-specific variable processors for theming hooks + // implemented as templates. See theme(). + if (isset($info['template']) && function_exists($name . '_' . $phase)) { + $cache[$hook][$phase_key][] = $name . '_' . $phase; + } + if (function_exists($name . '_' . $phase . '_' . $hook)) { + $cache[$hook][$phase_key][] = $name . '_' . $phase . '_' . $hook; + $cache[$hook]['theme path'] = $path; + } + // Ensure uniqueness. + $cache[$hook][$phase_key] = array_unique($cache[$hook][$phase_key]); + } + } + } + } +} + +/** + * Rebuild the theme registry cache. + * + * @param $theme + * The loaded $theme object as returned by list_themes(). + * @param $base_theme + * An array of loaded $theme objects representing the ancestor themes in + * oldest first order. + * @param theme_engine + * The name of the theme engine. + */ +function _theme_build_registry($theme, $base_theme, $theme_engine) { + $cache = array(); + // First, process the theme hooks advertised by modules. This will + // serve as the basic registry. + foreach (module_implements('theme') as $module) { + _theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module)); + } + + // Process each base theme. + foreach ($base_theme as $base) { + // If the base theme uses a theme engine, process its hooks. + $base_path = dirname($base->filename); + if ($theme_engine) { + _theme_process_registry($cache, $theme_engine, 'base_theme_engine', $base->name, $base_path); + } + _theme_process_registry($cache, $base->name, 'base_theme', $base->name, $base_path); + } + + // And then the same thing, but for the theme. + if ($theme_engine) { + _theme_process_registry($cache, $theme_engine, 'theme_engine', $theme->name, dirname($theme->filename)); + } + + // Finally, hooks provided by the theme itself. + _theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename)); + + // Let modules alter the registry. + drupal_alter('theme_registry', $cache); + + // Optimize the registry to not have empty arrays for functions. + foreach ($cache as $hook => $info) { + foreach (array('preprocess functions', 'process functions') as $phase) { + if (empty($info[$phase])) { + unset($cache[$hook][$phase]); + } + } + } + return $cache; +} + +/** + * Return a list of all currently available themes. + * + * Retrieved from the database, if available and the site is not in maintenance + * mode; otherwise compiled freshly from the filesystem. + * + * @param $refresh + * Whether to reload the list of themes from the database. Defaults to FALSE. + * + * @return + * An associative array of the currently available themes. The keys are the + * names of the themes and the values are objects having the following + * properties: + * - 'filename': The name of the .info file. + * - 'name': The name of the theme. + * - 'status': 1 for enabled, 0 for disabled themes. + * - 'info': The contents of the .info file. + * - 'stylesheets': A two dimensional array, using the first key for the + * 'media' attribute (e.g. 'all'), the second for the name of the file + * (e.g. style.css). The value is a complete filepath + * (e.g. themes/bartik/style.css). + * - 'scripts': An associative array of JavaScripts, using the filename as key + * and the complete filepath as value. + * - 'engine': The name of the theme engine. + * - 'base theme': The name of the base theme. + */ +function list_themes($refresh = FALSE) { + $list = &drupal_static(__FUNCTION__, array()); + + if ($refresh) { + $list = array(); + system_list_reset(); + } + + if (empty($list)) { + $list = array(); + $themes = array(); + // Extract from the database only when it is available. + // Also check that the site is not in the middle of an install or update. + if (!defined('MAINTENANCE_MODE')) { + try { + $themes = system_list('theme'); + } + catch (Exception $e) { + // If the database is not available, rebuild the theme data. + $themes = _system_rebuild_theme_data(); + } + } + else { + // Scan the installation when the database should not be read. + $themes = _system_rebuild_theme_data(); + } + + foreach ($themes as $theme) { + foreach ($theme->info['stylesheets'] as $media => $stylesheets) { + foreach ($stylesheets as $stylesheet => $path) { + $theme->stylesheets[$media][$stylesheet] = $path; + } + } + foreach ($theme->info['scripts'] as $script => $path) { + $theme->scripts[$script] = $path; + } + if (isset($theme->info['engine'])) { + $theme->engine = $theme->info['engine']; + } + if (isset($theme->info['base theme'])) { + $theme->base_theme = $theme->info['base theme']; + } + // Status is normally retrieved from the database. Add zero values when + // read from the installation directory to prevent notices. + if (!isset($theme->status)) { + $theme->status = 0; + } + $list[$theme->name] = $theme; + } + } + + return $list; +} + +/** + * Generates themed output. + * + * All requests for themed output must go through this function. It examines + * the request and routes it to the appropriate theme function or template, by + * checking the theme registry. + * + * The first argument to this function is the name of the theme hook. For + * instance, to theme a table, the theme hook name is 'table'. By default, this + * theme hook could be implemented by a function called 'theme_table' or a + * template file called 'table.tpl.php', but hook_theme() can override the + * default function or template name. + * + * If the implementation is a template file, several functions are called + * before the template file is invoked, to modify the $variables array. These + * fall into the "preprocessing" phase and the "processing" phase, and are + * executed (if they exist), in the following order (note that in the following + * list, HOOK indicates the theme hook name, MODULE indicates a module name, + * THEME indicates a theme name, and ENGINE indicates a theme engine name): + * - template_preprocess(&$variables, $hook): Creates a default set of variables + * for all theme hooks. + * - template_preprocess_HOOK(&$variables): Should be implemented by + * the module that registers the theme hook, to set up default variables. + * - MODULE_preprocess(&$variables, $hook): hook_preprocess() is invoked on all + * implementing modules. + * - MODULE_preprocess_HOOK(&$variables): hook_preprocess_HOOK() is invoked on + * all implementing modules, so that modules that didn't define the theme hook + * can alter the variables. + * - ENGINE_engine_preprocess(&$variables, $hook): Allows the theme engine to + * set necessary variables for all theme hooks. + * - ENGINE_engine_preprocess_HOOK(&$variables): Allows the theme engine to set + * necessary variables for the particular theme hook. + * - THEME_preprocess(&$variables, $hook): Allows the theme to set necessary + * variables for all theme hooks. + * - THEME_preprocess_HOOK(&$variables): Allows the theme to set necessary + * variables specific to the particular theme hook. + * - template_process(&$variables, $hook): Creates a default set of variables + * for all theme hooks. + * - template_process_HOOK(&$variables): This is the first processor specific + * to the theme hook; it should be implemented by the module that registers + * it. + * - MODULE_process(&$variables, $hook): hook_process() is invoked on all + * implementing modules. + * - MODULE_process_HOOK(&$variables): hook_process_HOOK() is invoked on + * on all implementing modules, so that modules that didn't define the theme + * hook can alter the variables. + * - ENGINE_engine_process(&$variables, $hook): Allows the theme engine to set + * necessary variables for all theme hooks. + * - ENGINE_engine_process_HOOK(&$variables): Allows the theme engine to set + * necessary variables for the particular theme hook. + * - ENGINE_process(&$variables, $hook): Allows the theme engine to process the + * variables. + * - ENGINE_process_HOOK(&$variables): Allows the theme engine to process the + * variables specific to the theme hook. + * - THEME_process(&$variables, $hook): Allows the theme to process the + * variables. + * - THEME_process_HOOK(&$variables): Allows the theme to process the + * variables specific to the theme hook. + * + * If the implementation is a function, only the theme-hook-specific preprocess + * and process functions (the ones ending in _HOOK) are called from the + * list above. This is because theme hooks with function implementations + * need to be fast, and calling the non-theme-hook-specific preprocess and + * process functions for them would incur a noticeable performance penalty. + * + * There are two special variables that these preprocess and process functions + * can set: 'theme_hook_suggestion' and 'theme_hook_suggestions'. These will be + * merged together to form a list of 'suggested' alternate theme hooks to use, + * in reverse order of priority. theme_hook_suggestion will always be a higher + * priority than items in theme_hook_suggestions. theme() will use the + * highest priority implementation that exists. If none exists, theme() will + * use the implementation for the theme hook it was called with. These + * suggestions are similar to and are used for similar reasons as calling + * theme() with an array as the $hook parameter (see below). The difference + * is whether the suggestions are determined by the code that calls theme() or + * by a preprocess or process function. + * + * @param $hook + * The name of the theme hook to call. If the name contains a + * double-underscore ('__') and there isn't an implementation for the full + * name, the part before the '__' is checked. This allows a fallback to a more + * generic implementation. For example, if theme('links__node', ...) is + * called, but there is no implementation of that theme hook, then the 'links' + * implementation is used. This process is iterative, so if + * theme('links__contextual__node', ...) is called, theme() checks for the + * following implementations, and uses the first one that exists: + * - links__contextual__node + * - links__contextual + * - links + * This allows themes to create specific theme implementations for named + * objects and contexts of otherwise generic theme hooks. The $hook parameter + * may also be an array, in which case the first theme hook that has an + * implementation is used. This allows for the code that calls theme() to + * explicitly specify the fallback order in a situation where using the '__' + * convention is not desired or is insufficient. + * @param $variables + * An associative array of variables to merge with defaults from the theme + * registry, pass to preprocess and process functions for modification, and + * finally, pass to the function or template implementing the theme hook. + * Alternatively, this can be a renderable array, in which case, its + * properties are mapped to variables expected by the theme hook + * implementations. + * + * @return + * An HTML string representing the themed output. + */ +function theme($hook, $variables = array()) { + static $hooks = NULL; + + // If called before all modules are loaded, we do not necessarily have a full + // theme registry to work with, and therefore cannot process the theme + // request properly. See also _theme_load_registry(). + if (!module_load_all(NULL) && !defined('MAINTENANCE_MODE')) { + throw new Exception(t('theme() may not be called until all modules are loaded.')); + } + + if (!isset($hooks)) { + drupal_theme_initialize(); + $hooks = theme_get_registry(); + } + + // If an array of hook candidates were passed, use the first one that has an + // implementation. + if (is_array($hook)) { + foreach ($hook as $candidate) { + if (isset($hooks[$candidate])) { + break; + } + } + $hook = $candidate; + } + + // If there's no implementation, check for more generic fallbacks. If there's + // still no implementation, log an error and return an empty string. + if (!isset($hooks[$hook])) { + // Iteratively strip everything after the last '__' delimiter, until an + // implementation is found. + while ($pos = strrpos($hook, '__')) { + $hook = substr($hook, 0, $pos); + if (isset($hooks[$hook])) { + break; + } + } + if (!isset($hooks[$hook])) { + // Only log a message when not trying theme suggestions ($hook being an + // array). + if (!isset($candidate)) { + watchdog('theme', 'Theme key "@key" not found.', array('@key' => $hook), LOG_WARNING); + } + return ''; + } + } + + $info = $hooks[$hook]; + global $theme_path; + $temp = $theme_path; + // point path_to_theme() to the currently used theme path: + $theme_path = $info['theme path']; + + // Include a file if the theme function or variable processor is held elsewhere. + if (!empty($info['includes'])) { + foreach ($info['includes'] as $include_file) { + include_once DRUPAL_ROOT . '/' . $include_file; + } + } + + // If a renderable array is passed as $variables, then set $variables to + // the arguments expected by the theme function. + if (isset($variables['#theme']) || isset($variables['#theme_wrappers'])) { + $element = $variables; + $variables = array(); + if (isset($info['variables'])) { + foreach (array_keys($info['variables']) as $name) { + if (isset($element["#$name"])) { + $variables[$name] = $element["#$name"]; + } + } + } + else { + $variables[$info['render element']] = $element; + } + } + + // Merge in argument defaults. + if (!empty($info['variables'])) { + $variables += $info['variables']; + } + elseif (!empty($info['render element'])) { + $variables += array($info['render element'] => array()); + } + + // Invoke the variable processors, if any. The processors may specify + // alternate suggestions for which hook's template/function to use. If the + // hook is a suggestion of a base hook, invoke the variable processors of + // the base hook, but retain the suggestion as a high priority suggestion to + // be used unless overridden by a variable processor function. + if (isset($info['base hook'])) { + $base_hook = $info['base hook']; + $base_hook_info = $hooks[$base_hook]; + if (isset($base_hook_info['preprocess functions']) || isset($base_hook_info['process functions'])) { + $variables['theme_hook_suggestion'] = $hook; + $hook = $base_hook; + $info = $base_hook_info; + } + } + if (isset($info['preprocess functions']) || isset($info['process functions'])) { + $variables['theme_hook_suggestions'] = array(); + foreach (array('preprocess functions', 'process functions') as $phase) { + if (!empty($info[$phase])) { + foreach ($info[$phase] as $processor_function) { + if (function_exists($processor_function)) { + // We don't want a poorly behaved process function changing $hook. + $hook_clone = $hook; + $processor_function($variables, $hook_clone); + } + } + } + } + // If the preprocess/process functions specified hook suggestions, and the + // suggestion exists in the theme registry, use it instead of the hook that + // theme() was called with. This allows the preprocess/process step to + // route to a more specific theme hook. For example, a function may call + // theme('node', ...), but a preprocess function can add 'node__article' as + // a suggestion, enabling a theme to have an alternate template file for + // article nodes. Suggestions are checked in the following order: + // - The 'theme_hook_suggestion' variable is checked first. It overrides + // all others. + // - The 'theme_hook_suggestions' variable is checked in FILO order, so the + // last suggestion added to the array takes precedence over suggestions + // added earlier. + $suggestions = array(); + if (!empty($variables['theme_hook_suggestions'])) { + $suggestions = $variables['theme_hook_suggestions']; + } + if (!empty($variables['theme_hook_suggestion'])) { + $suggestions[] = $variables['theme_hook_suggestion']; + } + foreach (array_reverse($suggestions) as $suggestion) { + if (isset($hooks[$suggestion])) { + $info = $hooks[$suggestion]; + break; + } + } + } + + // Generate the output using either a function or a template. + $output = ''; + if (isset($info['function'])) { + if (function_exists($info['function'])) { + $output = $info['function']($variables); + } + } + else { + // Default render function and extension. + $render_function = 'theme_render_template'; + $extension = '.tpl.php'; + + // The theme engine may use a different extension and a different renderer. + global $theme_engine; + if (isset($theme_engine)) { + if ($info['type'] != 'module') { + if (function_exists($theme_engine . '_render_template')) { + $render_function = $theme_engine . '_render_template'; + } + $extension_function = $theme_engine . '_extension'; + if (function_exists($extension_function)) { + $extension = $extension_function(); + } + } + } + + // In some cases, a template implementation may not have had + // template_preprocess() run (for example, if the default implementation is + // a function, but a template overrides that default implementation). In + // these cases, a template should still be able to expect to have access to + // the variables provided by template_preprocess(), so we add them here if + // they don't already exist. We don't want to run template_preprocess() + // twice (it would be inefficient and mess up zebra striping), so we use the + // 'directory' variable to determine if it has already run, which while not + // completely intuitive, is reasonably safe, and allows us to save on the + // overhead of adding some new variable to track that. + if (!isset($variables['directory'])) { + $default_template_variables = array(); + template_preprocess($default_template_variables, $hook); + $variables += $default_template_variables; + } + + // Render the output using the template file. + $template_file = $info['template'] . $extension; + if (isset($info['path'])) { + $template_file = $info['path'] . '/' . $template_file; + } + $output = $render_function($template_file, $variables); + } + + // restore path_to_theme() + $theme_path = $temp; + return $output; +} + +/** + * Return the path to the current themed element. + * + * It can point to the active theme or the module handling a themed implementation. + * For example, when invoked within the scope of a theming call it will depend + * on where the theming function is handled. If implemented from a module, it + * will point to the module. If implemented from the active theme, it will point + * to the active theme. When called outside the scope of a theming call, it will + * always point to the active theme. + */ +function path_to_theme() { + global $theme_path; + + if (!isset($theme_path)) { + drupal_theme_initialize(); + } + + return $theme_path; +} + +/** + * Allow themes and/or theme engines to easily discover overridden theme functions. + * + * @param $cache + * The existing cache of theme hooks to test against. + * @param $prefixes + * An array of prefixes to test, in reverse order of importance. + * + * @return $implementations + * The functions found, suitable for returning from hook_theme; + */ +function drupal_find_theme_functions($cache, $prefixes) { + $implementations = array(); + $functions = get_defined_functions(); + + foreach ($cache as $hook => $info) { + foreach ($prefixes as $prefix) { + // Find theme functions that implement possible "suggestion" variants of + // registered theme hooks and add those as new registered theme hooks. + // The 'pattern' key defines a common prefix that all suggestions must + // start with. The default is the name of the hook followed by '__'. An + // 'base hook' key is added to each entry made for a found suggestion, + // so that common functionality can be implemented for all suggestions of + // the same base hook. To keep things simple, deep hierarchy of + // suggestions is not supported: each suggestion's 'base hook' key + // refers to a base hook, not to another suggestion, and all suggestions + // are found using the base hook's pattern, not a pattern from an + // intermediary suggestion. + $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__'); + if (!isset($info['base hook']) && !empty($pattern)) { + $matches = preg_grep('/^' . $prefix . '_' . $pattern . '/', $functions['user']); + if ($matches) { + foreach ($matches as $match) { + $new_hook = substr($match, strlen($prefix) + 1); + $arg_name = isset($info['variables']) ? 'variables' : 'render element'; + $implementations[$new_hook] = array( + 'function' => $match, + $arg_name => $info[$arg_name], + 'base hook' => $hook, + ); + } + } + } + // Find theme functions that implement registered theme hooks and include + // that in what is returned so that the registry knows that the theme has + // this implementation. + if (function_exists($prefix . '_' . $hook)) { + $implementations[$hook] = array( + 'function' => $prefix . '_' . $hook, + ); + } + } + } + + return $implementations; +} + +/** + * Allow themes and/or theme engines to easily discover overridden templates. + * + * @param $cache + * The existing cache of theme hooks to test against. + * @param $extension + * The extension that these templates will have. + * @param $path + * The path to search. + */ +function drupal_find_theme_templates($cache, $extension, $path) { + $implementations = array(); + + // Collect paths to all sub-themes grouped by base themes. These will be + // used for filtering. This allows base themes to have sub-themes in its + // folder hierarchy without affecting the base themes template discovery. + $theme_paths = array(); + foreach (list_themes() as $theme_info) { + if (!empty($theme_info->base_theme)) { + $theme_paths[$theme_info->base_theme][$theme_info->name] = dirname($theme_info->filename); + } + } + foreach ($theme_paths as $basetheme => $subthemes) { + foreach ($subthemes as $subtheme => $subtheme_path) { + if (isset($theme_paths[$subtheme])) { + $theme_paths[$basetheme] = array_merge($theme_paths[$basetheme], $theme_paths[$subtheme]); + } + } + } + global $theme; + $subtheme_paths = isset($theme_paths[$theme]) ? $theme_paths[$theme] : array(); + + // Escape the periods in the extension. + $regex = '/' . str_replace('.', '\.', $extension) . '$/'; + // Get a listing of all template files in the path to search. + $files = file_scan_directory($path, $regex, array('key' => 'name')); + + // Find templates that implement registered theme hooks and include that in + // what is returned so that the registry knows that the theme has this + // implementation. + foreach ($files as $template => $file) { + // Ignore sub-theme templates for the current theme. + if (strpos($file->uri, str_replace($subtheme_paths, '', $file->uri)) !== 0) { + continue; + } + // Chop off the remaining extensions if there are any. $template already + // has the rightmost extension removed, but there might still be more, + // such as with .tpl.php, which still has .tpl in $template at this point. + if (($pos = strpos($template, '.')) !== FALSE) { + $template = substr($template, 0, $pos); + } + // Transform - in filenames to _ to match function naming scheme + // for the purposes of searching. + $hook = strtr($template, '-', '_'); + if (isset($cache[$hook])) { + $implementations[$hook] = array( + 'template' => $template, + 'path' => dirname($file->uri), + ); + } + } + + // Find templates that implement possible "suggestion" variants of registered + // theme hooks and add those as new registered theme hooks. See + // drupal_find_theme_functions() for more information about suggestions and + // the use of 'pattern' and 'base hook'. + $patterns = array_keys($files); + foreach ($cache as $hook => $info) { + $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__'); + if (!isset($info['base hook']) && !empty($pattern)) { + // Transform _ in pattern to - to match file naming scheme + // for the purposes of searching. + $pattern = strtr($pattern, '_', '-'); + + $matches = preg_grep('/^' . $pattern . '/', $patterns); + if ($matches) { + foreach ($matches as $match) { + $file = substr($match, 0, strpos($match, '.')); + // Put the underscores back in for the hook name and register this pattern. + $arg_name = isset($info['variables']) ? 'variables' : 'render element'; + $implementations[strtr($file, '-', '_')] = array( + 'template' => $file, + 'path' => dirname($files[$match]->uri), + $arg_name => $info[$arg_name], + 'base hook' => $hook, + ); + } + } + } + } + return $implementations; +} + +/** + * Retrieve a setting for the current theme or for a given theme. + * + * The final setting is obtained from the last value found in the following + * sources: + * - the default global settings specified in this function + * - the default theme-specific settings defined in any base theme's .info file + * - the default theme-specific settings defined in the theme's .info file + * - the saved values from the global theme settings form + * - the saved values from the theme's settings form + * To only retrieve the default global theme setting, an empty string should be + * given for $theme. + * + * @param $setting_name + * The name of the setting to be retrieved. + * @param $theme + * The name of a given theme; defaults to the current theme. + * + * @return + * The value of the requested setting, NULL if the setting does not exist. + */ +function theme_get_setting($setting_name, $theme = NULL) { + $cache = &drupal_static(__FUNCTION__, array()); + + // If no key is given, use the current theme if we can determine it. + if (!isset($theme)) { + $theme = !empty($GLOBALS['theme_key']) ? $GLOBALS['theme_key'] : ''; + } + + if (empty($cache[$theme])) { + // Set the default values for each global setting. + // To add new global settings, add their default values below, and then + // add form elements to system_theme_settings() in system.admin.inc. + $cache[$theme] = array( + 'default_logo' => 1, + 'logo_path' => '', + 'default_favicon' => 1, + 'favicon_path' => '', + // Use the IANA-registered MIME type for ICO files as default. + 'favicon_mimetype' => 'image/vnd.microsoft.icon', + ); + // Turn on all default features. + $features = _system_default_theme_features(); + foreach ($features as $feature) { + $cache[$theme]['toggle_' . $feature] = 1; + } + + // Get the values for the theme-specific settings from the .info files of + // the theme and all its base themes. + if ($theme) { + $themes = list_themes(); + $theme_object = $themes[$theme]; + + // Create a list which includes the current theme and all its base themes. + if (isset($theme_object->base_themes)) { + $theme_keys = array_keys($theme_object->base_themes); + $theme_keys[] = $theme; + } + else { + $theme_keys = array($theme); + } + foreach ($theme_keys as $theme_key) { + if (!empty($themes[$theme_key]->info['settings'])) { + $cache[$theme] = array_merge($cache[$theme], $themes[$theme_key]->info['settings']); + } + } + } + + // Get the saved global settings from the database. + $cache[$theme] = array_merge($cache[$theme], variable_get('theme_settings', array())); + + if ($theme) { + // Get the saved theme-specific settings from the database. + $cache[$theme] = array_merge($cache[$theme], variable_get('theme_' . $theme . '_settings', array())); + + // If the theme does not support a particular feature, override the global + // setting and set the value to NULL. + if (!empty($theme_object->info['features'])) { + foreach ($features as $feature) { + if (!in_array($feature, $theme_object->info['features'])) { + $cache[$theme]['toggle_' . $feature] = NULL; + } + } + } + + // Generate the path to the logo image. + if ($cache[$theme]['toggle_logo']) { + if ($cache[$theme]['default_logo']) { + $cache[$theme]['logo'] = file_create_url(dirname($theme_object->filename) . '/logo.png'); + } + elseif ($cache[$theme]['logo_path']) { + $cache[$theme]['logo'] = file_create_url($cache[$theme]['logo_path']); + } + } + + // Generate the path to the favicon. + if ($cache[$theme]['toggle_favicon']) { + if ($cache[$theme]['default_favicon']) { + if (file_exists($favicon = dirname($theme_object->filename) . '/favicon.ico')) { + $cache[$theme]['favicon'] = file_create_url($favicon); + } + else { + $cache[$theme]['favicon'] = file_create_url('core/misc/favicon.ico'); + } + } + elseif ($cache[$theme]['favicon_path']) { + $cache[$theme]['favicon'] = file_create_url($cache[$theme]['favicon_path']); + } + else { + $cache[$theme]['toggle_favicon'] = FALSE; + } + } + } + } + + return isset($cache[$theme][$setting_name]) ? $cache[$theme][$setting_name] : NULL; +} + +/** + * Render a system default template, which is essentially a PHP template. + * + * @param $template_file + * The filename of the template to render. + * @param $variables + * A keyed array of variables that will appear in the output. + * + * @return + * The output generated by the template. + */ +function theme_render_template($template_file, $variables) { + extract($variables, EXTR_SKIP); // Extract the variables to a local namespace + ob_start(); // Start output buffering + include DRUPAL_ROOT . '/' . $template_file; // Include the template file + return ob_get_clean(); // End buffering and return its contents +} + +/** + * Enable a given list of themes. + * + * @param $theme_list + * An array of theme names. + */ +function theme_enable($theme_list) { + drupal_clear_css_cache(); + + foreach ($theme_list as $key) { + db_update('system') + ->fields(array('status' => 1)) + ->condition('type', 'theme') + ->condition('name', $key) + ->execute(); + } + + list_themes(TRUE); + menu_rebuild(); + drupal_theme_rebuild(); + + // Invoke hook_themes_enabled() after the themes have been enabled. + module_invoke_all('themes_enabled', $theme_list); +} + +/** + * Disable a given list of themes. + * + * @param $theme_list + * An array of theme names. + */ +function theme_disable($theme_list) { + // Don't disable the default theme. + if ($pos = array_search(variable_get('theme_default', 'bartik'), $theme_list) !== FALSE) { + unset($theme_list[$pos]); + if (empty($theme_list)) { + return; + } + } + + drupal_clear_css_cache(); + + foreach ($theme_list as $key) { + db_update('system') + ->fields(array('status' => 0)) + ->condition('type', 'theme') + ->condition('name', $key) + ->execute(); + } + + list_themes(TRUE); + menu_rebuild(); + drupal_theme_rebuild(); + + // Invoke hook_themes_disabled after the themes have been disabled. + module_invoke_all('themes_disabled', $theme_list); +} + +/** + * @ingroup themeable + * @{ + */ + +/** + * Returns HTML for status and/or error messages, grouped by type. + * + * An invisible heading identifies the messages for assistive technology. + * Sighted users see a colored box. See http://www.w3.org/TR/WCAG-TECHS/H69.html + * for info. + * + * @param $variables + * An associative array containing: + * - display: (optional) Set to 'status' or 'error' to display only messages + * of that type. + */ +function theme_status_messages($variables) { + $display = $variables['display']; + $output = ''; + + $status_heading = array( + 'status' => t('Status message'), + 'error' => t('Error message'), + 'warning' => t('Warning message'), + ); + foreach (drupal_get_messages($display) as $type => $messages) { + $output .= "
\n"; + if (!empty($status_heading[$type])) { + $output .= '

' . $status_heading[$type] . "

\n"; + } + if (count($messages) > 1) { + $output .= "
    \n"; + foreach ($messages as $message) { + $output .= '
  • ' . $message . "
  • \n"; + } + $output .= "
\n"; + } + else { + $output .= $messages[0]; + } + $output .= "
\n"; + } + return $output; +} + +/** + * Returns HTML for a link. + * + * All Drupal code that outputs a link should call the l() function. That + * function performs some initial preprocessing, and then, if necessary, calls + * theme('link') for rendering the anchor tag. + * + * To optimize performance for sites that don't need custom theming of links, + * the l() function includes an inline copy of this function, and uses that copy + * if none of the enabled modules or the active theme implement any preprocess + * or process functions or override this theme implementation. + * + * @param $variables + * An associative array containing the keys 'text', 'path', and 'options'. See + * the l() function for information about these variables. + * + * @see l() + */ +function theme_link($variables) { + return '' . ($variables['options']['html'] ? $variables['text'] : check_plain($variables['text'])) . ''; +} + +/** + * Returns HTML for a set of links. + * + * @param $variables + * An associative array containing: + * - links: An associative array of links to be themed. The key for each link + * is used as its css class. Each link should be itself an array, with the + * following elements: + * - title: The link text. + * - href: The link URL. If omitted, the 'title' is shown as a plain text + * item in the links list. + * - html: (optional) Whether or not 'title' is HTML. If set, the title + * will not be passed through check_plain(). + * - attributes: (optional) Attributes for the anchor, or for the tag + * used in its place if no 'href' is supplied. + * If the 'href' element is supplied, the entire link array is passed to l() + * as its $options parameter. + * - attributes: A keyed array of attributes for the UL containing the + * list of links. + * - heading: (optional) A heading to precede the links. May be an associative + * array or a string. If it's an array, it can have the following elements: + * - text: The heading text. + * - level: The heading level (e.g. 'h2', 'h3'). + * - class: (optional) An array of the CSS classes for the heading. + * When using a string it will be used as the text of the heading and the + * level will default to 'h2'. Headings should be used on navigation menus + * and any list of links that consistently appears on multiple pages. To + * make the heading invisible use the 'element-invisible' CSS class. Do not + * use 'display:none', which removes it from screen-readers and assistive + * technology. Headings allow screen-reader and keyboard only users to + * navigate to or skip the links. See + * http://juicystudio.com/article/screen-readers-display-none.php and + * http://www.w3.org/TR/WCAG-TECHS/H42.html for more information. + */ +function theme_links($variables) { + $links = $variables['links']; + $attributes = $variables['attributes']; + $heading = $variables['heading']; + global $language_url; + $output = ''; + + if (count($links) > 0) { + $output = ''; + + // Treat the heading first if it is present to prepend it to the + // list of links. + if (!empty($heading)) { + if (is_string($heading)) { + // Prepare the array that will be used when the passed heading + // is a string. + $heading = array( + 'text' => $heading, + // Set the default level of the heading. + 'level' => 'h2', + ); + } + $output .= '<' . $heading['level']; + if (!empty($heading['class'])) { + $output .= drupal_attributes(array('class' => $heading['class'])); + } + $output .= '>' . check_plain($heading['text']) . ''; + } + + $output .= ''; + + $num_links = count($links); + $i = 1; + + foreach ($links as $key => $link) { + $class = array($key); + + // Add first, last and active classes to the list of links to help out themers. + if ($i == 1) { + $class[] = 'first'; + } + if ($i == $num_links) { + $class[] = 'last'; + } + if (isset($link['href']) && ($link['href'] == $_GET['q'] || ($link['href'] == '' && drupal_is_front_page())) + && (empty($link['language']) || $link['language']->language == $language_url->language)) { + $class[] = 'active'; + } + $output .= ' $class)) . '>'; + + if (isset($link['href'])) { + // Pass in $link as $options, they share the same keys. + $output .= l($link['title'], $link['href'], $link); + } + elseif (!empty($link['title'])) { + // Some links are actually not links, but we wrap these in for adding title and class attributes. + if (empty($link['html'])) { + $link['title'] = check_plain($link['title']); + } + $span_attributes = ''; + if (isset($link['attributes'])) { + $span_attributes = drupal_attributes($link['attributes']); + } + $output .= '' . $link['title'] . ''; + } + + $i++; + $output .= "\n"; + } + + $output .= ''; + } + + return $output; +} + +/** + * Returns HTML for an image. + * + * @param $variables + * An associative array containing: + * - path: Either the path of the image file (relative to base_path()) or a + * full URL. + * - width: The width of the image (if known). + * - height: The height of the image (if known). + * - alt: The alternative text for text-based browsers. HTML 4 and XHTML 1.0 + * always require an alt attribute. The HTML 5 draft allows the alt + * attribute to be omitted in some cases. Therefore, this variable defaults + * to an empty string, but can be set to NULL for the attribute to be + * omitted. Usually, neither omission nor an empty string satisfies + * accessibility requirements, so it is strongly encouraged for code calling + * theme('image') to pass a meaningful value for this variable. + * - http://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8 + * - http://www.w3.org/TR/xhtml1/dtds.html + * - http://dev.w3.org/html5/spec/Overview.html#alt + * - title: The title text is displayed when the image is hovered in some + * popular browsers. + * - attributes: Associative array of attributes to be placed in the img tag. + */ +function theme_image($variables) { + $attributes = $variables['attributes']; + $attributes['src'] = file_create_url($variables['path']); + + foreach (array('width', 'height', 'alt', 'title') as $key) { + + if (isset($variables[$key])) { + $attributes[$key] = $variables[$key]; + } + } + + return ''; +} + +/** + * Returns HTML for a breadcrumb trail. + * + * @param $variables + * An associative array containing: + * - breadcrumb: An array containing the breadcrumb links. + */ +function theme_breadcrumb($variables) { + $breadcrumb = $variables['breadcrumb']; + + if (!empty($breadcrumb)) { + // Provide a navigational heading to give context for breadcrumb links to + // screen-reader users. Make the heading invisible with .element-invisible. + $output = '

' . t('You are here') . '

'; + + $output .= ''; + return $output; + } +} + +/** + * Returns HTML for a table. + * + * @param $variables + * An associative array containing: + * - header: An array containing the table headers. Each element of the array + * can be either a localized string or an associative array with the + * following keys: + * - "data": The localized title of the table column. + * - "field": The database field represented in the table column (required + * if user is to be able to sort on this column). + * - "sort": A default sort order for this column ("asc" or "desc"). + * - Any HTML attributes, such as "colspan", to apply to the column header + * cell. + * - rows: An array of table rows. Every row is an array of cells, or an + * associative array with the following keys: + * - "data": an array of cells + * - Any HTML attributes, such as "class", to apply to the table row. + * - "no_striping": a boolean indicating that the row should receive no + * 'even / odd' styling. Defaults to FALSE. + * Each cell can be either a string or an associative array with the + * following keys: + * - "data": The string to display in the table cell. + * - "header": Indicates this cell is a header. + * - Any HTML attributes, such as "colspan", to apply to the table cell. + * Here's an example for $rows: + * @code + * $rows = array( + * // Simple row + * array( + * 'Cell 1', 'Cell 2', 'Cell 3' + * ), + * // Row with attributes on the row and some of its cells. + * array( + * 'data' => array('Cell 1', array('data' => 'Cell 2', 'colspan' => 2)), 'class' => array('funky') + * ) + * ); + * @endcode + * - attributes: An array of HTML attributes to apply to the table tag. + * - caption: A localized string to use for the
\n"; + } + + // Format the table columns: + if (count($colgroups)) { + foreach ($colgroups as $number => $colgroup) { + $attributes = array(); + + // Check if we're dealing with a simple or complex column + if (isset($colgroup['data'])) { + foreach ($colgroup as $key => $value) { + if ($key == 'data') { + $cols = $value; + } + else { + $attributes[$key] = $value; + } + } + } + else { + $cols = $colgroup; + } + + // Build colgroup + if (is_array($cols) && count($cols)) { + $output .= ' '; + $i = 0; + foreach ($cols as $col) { + $output .= ' '; + } + $output .= " \n"; + } + else { + $output .= ' \n"; + } + } + } + + // Add the 'empty' row message if available. + if (!count($rows) && $empty) { + $header_count = 0; + foreach ($header as $header_cell) { + if (is_array($header_cell)) { + $header_count += isset($header_cell['colspan']) ? $header_cell['colspan'] : 1; + } + else { + $header_count++; + } + } + $rows[] = array(array('data' => $empty, 'colspan' => $header_count, 'class' => array('empty', 'message'))); + } + + // Format the table header: + if (count($header)) { + $ts = tablesort_init($header); + // HTML requires that the thead tag has tr tags in it followed by tbody + // tags. Using ternary operator to check and see if we have any rows. + $output .= (count($rows) ? ' ' : ' '); + foreach ($header as $cell) { + $cell = tablesort_header($cell, $header, $ts); + $output .= _theme_table_cell($cell, TRUE); + } + // Using ternary operator to close the tags based on whether or not there are rows + $output .= (count($rows) ? " \n" : "\n"); + } + else { + $ts = array(); + } + + // Format the table rows: + if (count($rows)) { + $output .= "\n"; + $flip = array('even' => 'odd', 'odd' => 'even'); + $class = 'even'; + foreach ($rows as $number => $row) { + $attributes = array(); + + // Check if we're dealing with a simple or complex row + if (isset($row['data'])) { + foreach ($row as $key => $value) { + if ($key == 'data') { + $cells = $value; + } + else { + $attributes[$key] = $value; + } + } + } + else { + $cells = $row; + } + if (count($cells)) { + // Add odd/even class + if (empty($row['no_striping'])) { + $class = $flip[$class]; + $attributes['class'][] = $class; + } + + // Build row + $output .= ' '; + $i = 0; + foreach ($cells as $cell) { + $cell = tablesort_cell($cell, $header, $ts, $i++); + $output .= _theme_table_cell($cell); + } + $output .= " \n"; + } + } + $output .= "\n"; + } + + $output .= "
tag. + * - colgroups: An array of column groups. Each element of the array can be + * either: + * - An array of columns, each of which is an associative array of HTML + * attributes applied to the COL element. + * - An array of attributes applied to the COLGROUP element, which must + * include a "data" attribute. To add attributes to COL elements, set the + * "data" attribute with an array of columns, each of which is an + * associative array of HTML attributes. + * Here's an example for $colgroup: + * @code + * $colgroup = array( + * // COLGROUP with one COL element. + * array( + * array( + * 'class' => array('funky'), // Attribute for the COL element. + * ), + * ), + * // Colgroup with attributes and inner COL elements. + * array( + * 'data' => array( + * array( + * 'class' => array('funky'), // Attribute for the COL element. + * ), + * ), + * 'class' => array('jazzy'), // Attribute for the COLGROUP element. + * ), + * ); + * @endcode + * These optional tags are used to group and set properties on columns + * within a table. For example, one may easily group three columns and + * apply same background style to all. + * - sticky: Use a "sticky" table header. + * - empty: The message to display in an extra row if table does not have any + * rows. + */ +function theme_table($variables) { + $header = $variables['header']; + $rows = $variables['rows']; + $attributes = $variables['attributes']; + $caption = $variables['caption']; + $colgroups = $variables['colgroups']; + $sticky = $variables['sticky']; + $empty = $variables['empty']; + + // Add sticky headers, if applicable. + if (count($header) && $sticky) { + drupal_add_js('core/misc/tableheader.js'); + // Add 'sticky-enabled' class to the table to identify it for JS. + // This is needed to target tables constructed by this function. + $attributes['class'][] = 'sticky-enabled'; + } + + $output = '\n"; + + if (isset($caption)) { + $output .= '' . $caption . "
\n"; + return $output; +} + +/** + * Returns HTML for a sort icon. + * + * @param $variables + * An associative array containing: + * - style: Set to either 'asc' or 'desc', this determines which icon to show. + */ +function theme_tablesort_indicator($variables) { + if ($variables['style'] == "asc") { + return theme('image', array('path' => 'core/misc/arrow-asc.png', 'width' => 13, 'height' => 13, 'alt' => t('sort ascending'), 'title' => t('sort ascending'))); + } + else { + return theme('image', array('path' => 'core/misc/arrow-desc.png', 'width' => 13, 'height' => 13, 'alt' => t('sort descending'), 'title' => t('sort descending'))); + } +} + +/** + * Returns HTML for a marker for new or updated content. + * + * @param $variables + * An associative array containing: + * - type: Number representing the marker type to display. See MARK_NEW, + * MARK_UPDATED, MARK_READ. + */ +function theme_mark($variables) { + $type = $variables['type']; + global $user; + if ($user->uid) { + if ($type == MARK_NEW) { + return ' ' . t('new') . ''; + } + elseif ($type == MARK_UPDATED) { + return ' ' . t('updated') . ''; + } + } +} + +/** + * Returns HTML for a list or nested list of items. + * + * @param $variables + * An associative array containing: + * - items: An array of items to be displayed in the list. If an item is a + * string, then it is used as is. If an item is an array, then the "data" + * element of the array is used as the contents of the list item. If an item + * is an array with a "children" element, those children are displayed in a + * nested list. All other elements are treated as attributes of the list + * item element. + * - title: The title of the list. + * - type: The type of list to return (e.g. "ul", "ol"). + * - attributes: The attributes applied to the list element. + */ +function theme_item_list($variables) { + $items = $variables['items']; + $title = $variables['title']; + $type = $variables['type']; + $attributes = $variables['attributes']; + + $output = '
'; + if (isset($title)) { + $output .= '

' . $title . '

'; + } + + if (!empty($items)) { + $output .= "<$type" . drupal_attributes($attributes) . '>'; + $num_items = count($items); + foreach ($items as $i => $item) { + $attributes = array(); + $children = array(); + if (is_array($item)) { + foreach ($item as $key => $value) { + if ($key == 'data') { + $data = $value; + } + elseif ($key == 'children') { + $children = $value; + } + else { + $attributes[$key] = $value; + } + } + } + else { + $data = $item; + } + if (count($children) > 0) { + // Render nested list. + $data .= theme_item_list(array('items' => $children, 'title' => NULL, 'type' => $type, 'attributes' => $attributes)); + } + if ($i == 0) { + $attributes['class'][] = 'first'; + } + if ($i == $num_items - 1) { + $attributes['class'][] = 'last'; + } + $output .= '' . $data . "\n"; + } + $output .= ""; + } + $output .= '
'; + return $output; +} + +/** + * Returns HTML for a "more help" link. + * + * @param $variables + * An associative array containing: + * - url: The url for the link. + */ +function theme_more_help_link($variables) { + return ''; +} + +/** + * Returns HTML for a feed icon. + * + * @param $variables + * An associative array containing: + * - url: An internal system path or a fully qualified external URL of the + * feed. + * - title: A descriptive title of the feed. + */ +function theme_feed_icon($variables) { + $text = t('Subscribe to @feed-title', array('@feed-title' => $variables['title'])); + if ($image = theme('image', array('path' => 'core/misc/feed.png', 'width' => 16, 'height' => 16, 'alt' => $text))) { + return l($image, $variables['url'], array('html' => TRUE, 'attributes' => array('class' => array('feed-icon'), 'title' => $text))); + } +} + +/** + * Returns HTML for a generic HTML tag with attributes. + * + * @param $variables + * An associative array containing: + * - element: An associative array describing the tag: + * - #tag: The tag name to output. Typical tags added to the HTML HEAD: + * - meta: To provide meta information, such as a page refresh. + * - link: To refer to stylesheets and other contextual information. + * - script: To load JavaScript. + * - #attributes: (optional) An array of HTML attributes to apply to the + * tag. + * - #value: (optional) A string containing tag content, such as inline CSS. + * - #value_prefix: (optional) A string to prepend to #value, e.g. a CDATA + * wrapper prefix. + * - #value_suffix: (optional) A string to append to #value, e.g. a CDATA + * wrapper suffix. + */ +function theme_html_tag($variables) { + $element = $variables['element']; + if (!isset($element['#value'])) { + return '<' . $element['#tag'] . drupal_attributes($element['#attributes']) . " />\n"; + } + else { + $output = '<' . $element['#tag'] . drupal_attributes($element['#attributes']) . '>'; + if (isset($element['#value_prefix'])) { + $output .= $element['#value_prefix']; + } + $output .= $element['#value']; + if (isset($element['#value_suffix'])) { + $output .= $element['#value_suffix']; + } + $output .= '\n"; + return $output; + } +} + +/** + * Returns HTML for a "more" link, like those used in blocks. + * + * @param $variables + * An associative array containing: + * - url: The url of the main page. + * - title: A descriptive verb for the link, like 'Read more'. + */ +function theme_more_link($variables) { + return ''; +} + +/** + * Returns HTML for a username, potentially linked to the user's page. + * + * @param $variables + * An associative array containing: + * - account: The user object to format. + * - name: The user's name, sanitized. + * - extra: Additional text to append to the user's name, sanitized. + * - link_path: The path or URL of the user's profile page, home page, or + * other desired page to link to for more information about the user. + * - link_options: An array of options to pass to the l() function's $options + * parameter if linking the user's name to the user's page. + * - attributes_array: An array of attributes to pass to the + * drupal_attributes() function if not linking to the user's page. + * + * @see template_preprocess_username() + * @see template_process_username() + */ +function theme_username($variables) { + if (isset($variables['link_path'])) { + // We have a link path, so we should generate a link using l(). + // Additional classes may be added as array elements like + // $variables['link_options']['attributes']['class'][] = 'myclass'; + $output = l($variables['name'] . $variables['extra'], $variables['link_path'], $variables['link_options']); + } + else { + // Modules may have added important attributes so they must be included + // in the output. Additional classes may be added as array elements like + // $variables['attributes_array']['class'][] = 'myclass'; + $output = '' . $variables['name'] . $variables['extra'] . ''; + } + return $output; +} + +/** + * Returns HTML for a progress bar. + * + * @param $variables + * An associative array containing: + * - percent: The percentage of the progress. + * - message: A string containing information to be displayed. + */ +function theme_progress_bar($variables) { + $output = '
'; + $output .= '
'; + $output .= '
' . $variables['percent'] . '%
'; + $output .= '
' . $variables['message'] . '
'; + $output .= '
'; + + return $output; +} + +/** + * Returns HTML for an indentation div; used for drag and drop tables. + * + * @param $variables + * An associative array containing: + * - size: Optional. The number of indentations to create. + */ +function theme_indentation($variables) { + $output = ''; + for ($n = 0; $n < $variables['size']; $n++) { + $output .= '
 
'; + } + return $output; +} + +/** + * @} End of "ingroup themeable". + */ + +/** + * Returns HTML output for a single table cell for theme_table(). + * + * @param $cell + * Array of cell information, or string to display in cell. + * @param bool $header + * TRUE if this cell is a table header cell, FALSE if it is an ordinary + * table cell. If $cell is an array with element 'header' set to TRUE, that + * will override the $header parameter. + * + * @return + * HTML for the cell. + */ +function _theme_table_cell($cell, $header = FALSE) { + $attributes = ''; + + if (is_array($cell)) { + $data = isset($cell['data']) ? $cell['data'] : ''; + // Cell's data property can be a string or a renderable array. + if (is_array($data)) { + $data = drupal_render($data); + } + $header |= isset($cell['header']); + unset($cell['data']); + unset($cell['header']); + $attributes = drupal_attributes($cell); + } + else { + $data = $cell; + } + + if ($header) { + $output = "$data"; + } + else { + $output = "$data"; + } + + return $output; +} + +/** + * Adds a default set of helper variables for variable processors and templates. + * This comes in before any other preprocess function which makes it possible to + * be used in default theme implementations (non-overridden theme functions). + * + * For more detailed information, see theme(). + * + */ +function template_preprocess(&$variables, $hook) { + global $user; + static $count = array(); + + // Track run count for each hook to provide zebra striping. + // See "template_preprocess_block()" which provides the same feature specific to blocks. + $count[$hook] = isset($count[$hook]) && is_int($count[$hook]) ? $count[$hook] : 1; + $variables['zebra'] = ($count[$hook] % 2) ? 'odd' : 'even'; + $variables['id'] = $count[$hook]++; + + // Tell all templates where they are located. + $variables['directory'] = path_to_theme(); + + // Initialize html class attribute for the current hook. + $variables['classes_array'] = array(drupal_html_class($hook)); + + // Merge in variables that don't depend on hook and don't change during a + // single page request. + // Use the advanced drupal_static() pattern, since this is called very often. + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['default_variables'] = &drupal_static(__FUNCTION__); + } + $default_variables = &$drupal_static_fast['default_variables']; + // Global $user object shouldn't change during a page request once rendering + // has started, but if there's an edge case where it does, re-fetch the + // variables appropriate for the new user. + if (!isset($default_variables) || ($user !== $default_variables['user'])) { + $default_variables = _template_preprocess_default_variables(); + } + $variables += $default_variables; +} + +/** + * Returns hook-independant variables to template_preprocess(). + */ +function _template_preprocess_default_variables() { + global $user; + + // Variables that don't depend on a database connection. + $variables = array( + 'attributes_array' => array(), + 'title_attributes_array' => array(), + 'content_attributes_array' => array(), + 'title_prefix' => array(), + 'title_suffix' => array(), + 'user' => $user, + 'db_is_active' => !defined('MAINTENANCE_MODE'), + 'is_admin' => FALSE, + 'logged_in' => FALSE, + ); + + // The user object has no uid property when the database does not exist during + // install. The user_access() check deals with issues when in maintenance mode + // as uid is set but the user.module has not been included. + if (isset($user->uid) && function_exists('user_access')) { + $variables['is_admin'] = user_access('access administration pages'); + $variables['logged_in'] = ($user->uid > 0); + } + + // drupal_is_front_page() might throw an exception. + try { + $variables['is_front'] = drupal_is_front_page(); + } + catch (Exception $e) { + // If the database is not yet available, set default values for these + // variables. + $variables['is_front'] = FALSE; + $variables['db_is_active'] = FALSE; + } + + return $variables; +} + +/** + * A default process function used to alter variables as late as possible. + * + * For more detailed information, see theme(). + * + */ +function template_process(&$variables, $hook) { + // Flatten out classes. + $variables['classes'] = implode(' ', $variables['classes_array']); + + // Flatten out attributes, title_attributes, and content_attributes. + // Because this function can be called very often, and often with empty + // attributes, optimize performance by only calling drupal_attributes() if + // necessary. + $variables['attributes'] = $variables['attributes_array'] ? drupal_attributes($variables['attributes_array']) : ''; + $variables['title_attributes'] = $variables['title_attributes_array'] ? drupal_attributes($variables['title_attributes_array']) : ''; + $variables['content_attributes'] = $variables['content_attributes_array'] ? drupal_attributes($variables['content_attributes_array']) : ''; +} + +/** + * Preprocess variables for html.tpl.php + * + * @see system_elements() + * @see html.tpl.php + */ +function template_preprocess_html(&$variables) { + // Compile a list of classes that are going to be applied to the body element. + // This allows advanced theming based on context (home page, node of certain type, etc.). + // Add a class that tells us whether we're on the front page or not. + $variables['classes_array'][] = $variables['is_front'] ? 'front' : 'not-front'; + // Add a class that tells us whether the page is viewed by an authenticated user or not. + $variables['classes_array'][] = $variables['logged_in'] ? 'logged-in' : 'not-logged-in'; + + // Add information about the number of sidebars. + if (!empty($variables['page']['sidebar_first']) && !empty($variables['page']['sidebar_second'])) { + $variables['classes_array'][] = 'two-sidebars'; + } + elseif (!empty($variables['page']['sidebar_first'])) { + $variables['classes_array'][] = 'one-sidebar sidebar-first'; + } + elseif (!empty($variables['page']['sidebar_second'])) { + $variables['classes_array'][] = 'one-sidebar sidebar-second'; + } + else { + $variables['classes_array'][] = 'no-sidebars'; + } + + // Populate the body classes. + if ($suggestions = theme_get_suggestions(arg(), 'page', '-')) { + foreach ($suggestions as $suggestion) { + if ($suggestion != 'page-front') { + // Add current suggestion to page classes to make it possible to theme + // the page depending on the current page type (e.g. node, admin, user, + // etc.) as well as more specific data like node-12 or node-edit. + $variables['classes_array'][] = drupal_html_class($suggestion); + } + } + } + + // If on an individual node page, add the node type to body classes. + if ($node = menu_get_object()) { + $variables['classes_array'][] = drupal_html_class('node-type-' . $node->type); + } + + // RDFa allows annotation of XHTML pages with RDF data, while GRDDL provides + // mechanisms for extraction of this RDF content via XSLT transformation + // using an associated GRDDL profile. + $variables['rdf_namespaces'] = drupal_get_rdf_namespaces(); + $variables['grddl_profile'] = 'http://www.w3.org/1999/xhtml/vocab'; + $variables['language'] = $GLOBALS['language']; + $variables['language']->dir = $GLOBALS['language']->direction ? 'rtl' : 'ltr'; + + // Add favicon. + if (theme_get_setting('toggle_favicon')) { + $favicon = theme_get_setting('favicon'); + $type = theme_get_setting('favicon_mimetype'); + drupal_add_html_head_link(array('rel' => 'shortcut icon', 'href' => drupal_strip_dangerous_protocols($favicon), 'type' => $type)); + } + + // Construct page title. + if (drupal_get_title()) { + $head_title = array( + 'title' => strip_tags(drupal_get_title()), + 'name' => check_plain(variable_get('site_name', 'Drupal')), + ); + } + else { + $head_title = array('name' => check_plain(variable_get('site_name', 'Drupal'))); + if (variable_get('site_slogan', '')) { + $head_title['slogan'] = filter_xss_admin(variable_get('site_slogan', '')); + } + } + $variables['head_title_array'] = $head_title; + $variables['head_title'] = implode(' | ', $head_title); + + // Populate the page template suggestions. + if ($suggestions = theme_get_suggestions(arg(), 'html')) { + $variables['theme_hook_suggestions'] = $suggestions; + } +} + +/** + * Preprocess variables for page.tpl.php + * + * Most themes utilize their own copy of page.tpl.php. The default is located + * inside "modules/system/page.tpl.php". Look in there for the full list of + * variables. + * + * Uses the arg() function to generate a series of page template suggestions + * based on the current path. + * + * Any changes to variables in this preprocessor should also be changed inside + * template_preprocess_maintenance_page() to keep all of them consistent. + * + * @see drupal_render_page() + * @see template_process_page() + * @see page.tpl.php + */ +function template_preprocess_page(&$variables) { + // Move some variables to the top level for themer convenience and template cleanliness. + $variables['show_messages'] = $variables['page']['#show_messages']; + + foreach (system_region_list($GLOBALS['theme']) as $region_key => $region_name) { + if (!isset($variables['page'][$region_key])) { + $variables['page'][$region_key] = array(); + } + } + + // Set up layout variable. + $variables['layout'] = 'none'; + if (!empty($variables['page']['sidebar_first'])) { + $variables['layout'] = 'first'; + } + if (!empty($variables['page']['sidebar_second'])) { + $variables['layout'] = ($variables['layout'] == 'first') ? 'both' : 'second'; + } + + $variables['base_path'] = base_path(); + $variables['front_page'] = url(); + $variables['feed_icons'] = drupal_get_feeds(); + $variables['language'] = $GLOBALS['language']; + $variables['language']->dir = $GLOBALS['language']->direction ? 'rtl' : 'ltr'; + $variables['logo'] = theme_get_setting('logo'); + $variables['main_menu'] = theme_get_setting('toggle_main_menu') ? menu_main_menu() : array(); + $variables['secondary_menu'] = theme_get_setting('toggle_secondary_menu') ? menu_secondary_menu() : array(); + $variables['action_links'] = menu_local_actions(); + $variables['site_name'] = (theme_get_setting('toggle_name') ? filter_xss_admin(variable_get('site_name', 'Drupal')) : ''); + $variables['site_slogan'] = (theme_get_setting('toggle_slogan') ? filter_xss_admin(variable_get('site_slogan', '')) : ''); + $variables['tabs'] = menu_local_tabs(); + + if ($node = menu_get_object()) { + $variables['node'] = $node; + } + + // Populate the page template suggestions. + if ($suggestions = theme_get_suggestions(arg(), 'page')) { + $variables['theme_hook_suggestions'] = $suggestions; + } +} + +/** + * Process variables for page.tpl.php + * + * Perform final addition of variables before passing them into the template. + * To customize these variables, simply set them in an earlier step. + * + * @see template_preprocess_page() + * @see page.tpl.php + */ +function template_process_page(&$variables) { + if (!isset($variables['breadcrumb'])) { + // Build the breadcrumb last, so as to increase the chance of being able to + // re-use the cache of an already rendered menu containing the active link + // for the current page. + // @see menu_tree_page_data() + $variables['breadcrumb'] = theme('breadcrumb', array('breadcrumb' => drupal_get_breadcrumb())); + } + if (!isset($variables['title'])) { + $variables['title'] = drupal_get_title(); + } + + // Generate messages last in order to capture as many as possible for the + // current page. + if (!isset($variables['messages'])) { + $variables['messages'] = $variables['show_messages'] ? theme('status_messages') : ''; + } +} + +/** + * Process variables for html.tpl.php + * + * Perform final addition and modification of variables before passing into + * the template. To customize these variables, call drupal_render() on elements + * in $variables['page'] during THEME_preprocess_page(). + * + * @see template_preprocess_html() + * @see html.tpl.php + */ +function template_process_html(&$variables) { + // Render page_top and page_bottom into top level variables. + $variables['page_top'] = drupal_render($variables['page']['page_top']); + $variables['page_bottom'] = drupal_render($variables['page']['page_bottom']); + // Place the rendered HTML for the page body into a top level variable. + $variables['page'] = $variables['page']['#children']; + $variables['page_bottom'] .= drupal_get_js('footer'); + + $variables['head'] = drupal_get_html_head(); + $variables['css'] = drupal_add_css(); + $variables['styles'] = drupal_get_css(); + $variables['scripts'] = drupal_get_js(); +} + +/** + * Generate an array of suggestions from path arguments. + * + * This is typically called for adding to the 'theme_hook_suggestions' or + * 'classes_array' variables from within preprocess functions, when wanting to + * base the additional suggestions on the path of the current page. + * + * @param $args + * An array of path arguments, such as from function arg(). + * @param $base + * A string identifying the base 'thing' from which more specific suggestions + * are derived. For example, 'page' or 'html'. + * @param $delimiter + * The string used to delimit increasingly specific information. The default + * of '__' is appropriate for theme hook suggestions. '-' is appropriate for + * extra classes. + * + * @return + * An array of suggestions, suitable for adding to + * $variables['theme_hook_suggestions'] within a preprocess function or to + * $variables['classes_array'] if the suggestions represent extra CSS classes. + */ +function theme_get_suggestions($args, $base, $delimiter = '__') { + + // Build a list of suggested theme hooks or body classes in order of + // specificity. One suggestion is made for every element of the current path, + // though numeric elements are not carried to subsequent suggestions. For + // example, for $base='page', http://www.example.com/node/1/edit would result + // in the following suggestions and body classes: + // + // page__node page-node + // page__node__% page-node-% + // page__node__1 page-node-1 + // page__node__edit page-node-edit + + $suggestions = array(); + $prefix = $base; + foreach ($args as $arg) { + // Remove slashes or null per SA-CORE-2009-003 and change - (hyphen) to _ + // (underscore). + // + // When we discover templates in @see drupal_find_theme_templates, + // hyphens (-) are converted to underscores (_) before the theme hook + // is registered. We do this because the hyphens used for delimiters + // in hook suggestions cannot be used in the function names of the + // associated preprocess functions. Any page templates designed to be used + // on paths that contain a hyphen are also registered with these hyphens + // converted to underscores so here we must convert any hyphens in path + // arguments to underscores here before fetching theme hook suggestions + // to ensure the templates are appropriately recognized. + $arg = str_replace(array("/", "\\", "\0", '-'), array('', '', '', '_'), $arg); + // The percent acts as a wildcard for numeric arguments since + // asterisks are not valid filename characters on many filesystems. + if (is_numeric($arg)) { + $suggestions[] = $prefix . $delimiter . '%'; + } + $suggestions[] = $prefix . $delimiter . $arg; + if (!is_numeric($arg)) { + $prefix .= $delimiter . $arg; + } + } + if (drupal_is_front_page()) { + // Front templates should be based on root only, not prefixed arguments. + $suggestions[] = $base . $delimiter . 'front'; + } + + return $suggestions; +} + +/** + * The variables array generated here is a mirror of template_preprocess_page(). + * This preprocessor will run its course when theme_maintenance_page() is + * invoked. + * + * An alternate template file of "maintenance-page--offline.tpl.php" can be + * used when the database is offline to hide errors and completely replace the + * content. + * + * The $variables array contains the following arguments: + * - $content + * + * @see maintenance-page.tpl.php + */ +function template_preprocess_maintenance_page(&$variables) { + // Add favicon + if (theme_get_setting('toggle_favicon')) { + $favicon = theme_get_setting('favicon'); + $type = theme_get_setting('favicon_mimetype'); + drupal_add_html_head_link(array('rel' => 'shortcut icon', 'href' => drupal_strip_dangerous_protocols($favicon), 'type' => $type)); + } + + global $theme; + // Retrieve the theme data to list all available regions. + $theme_data = list_themes(); + $regions = $theme_data[$theme]->info['regions']; + + // Get all region content set with drupal_add_region_content(). + foreach (array_keys($regions) as $region) { + // Assign region to a region variable. + $region_content = drupal_get_region_content($region); + isset($variables[$region]) ? $variables[$region] .= $region_content : $variables[$region] = $region_content; + } + + // Setup layout variable. + $variables['layout'] = 'none'; + if (!empty($variables['sidebar_first'])) { + $variables['layout'] = 'first'; + } + if (!empty($variables['sidebar_second'])) { + $variables['layout'] = ($variables['layout'] == 'first') ? 'both' : 'second'; + } + + // Construct page title + if (drupal_get_title()) { + $head_title = array( + 'title' => strip_tags(drupal_get_title()), + 'name' => variable_get('site_name', 'Drupal'), + ); + } + else { + $head_title = array('name' => variable_get('site_name', 'Drupal')); + if (variable_get('site_slogan', '')) { + $head_title['slogan'] = variable_get('site_slogan', ''); + } + } + + // set the default language if necessary + $language = isset($GLOBALS['language']) ? $GLOBALS['language'] : language_default(); + + $variables['head_title_array'] = $head_title; + $variables['head_title'] = implode(' | ', $head_title); + $variables['base_path'] = base_path(); + $variables['front_page'] = url(); + $variables['breadcrumb'] = ''; + $variables['feed_icons'] = ''; + $variables['help'] = ''; + $variables['language'] = $language; + $variables['language']->dir = $language->direction ? 'rtl' : 'ltr'; + $variables['logo'] = theme_get_setting('logo'); + $variables['messages'] = $variables['show_messages'] ? theme('status_messages') : ''; + $variables['main_menu'] = array(); + $variables['secondary_menu'] = array(); + $variables['site_name'] = (theme_get_setting('toggle_name') ? variable_get('site_name', 'Drupal') : ''); + $variables['site_slogan'] = (theme_get_setting('toggle_slogan') ? variable_get('site_slogan', '') : ''); + $variables['tabs'] = ''; + $variables['title'] = drupal_get_title(); + $variables['closure'] = ''; + + // Compile a list of classes that are going to be applied to the body element. + $variables['classes_array'][] = 'in-maintenance'; + if (isset($variables['db_is_active']) && !$variables['db_is_active']) { + $variables['classes_array'][] = 'db-offline'; + } + if ($variables['layout'] == 'both') { + $variables['classes_array'][] = 'two-sidebars'; + } + elseif ($variables['layout'] == 'none') { + $variables['classes_array'][] = 'no-sidebars'; + } + else { + $variables['classes_array'][] = 'one-sidebar sidebar-' . $variables['layout']; + } + + // Dead databases will show error messages so supplying this template will + // allow themers to override the page and the content completely. + if (isset($variables['db_is_active']) && !$variables['db_is_active']) { + $variables['theme_hook_suggestion'] = 'maintenance_page__offline'; + } +} + +/** + * The variables array generated here is a mirror of template_process_html(). + * This processor will run its course when theme_maintenance_page() is invoked. + * + * @see maintenance-page.tpl.php + */ +function template_process_maintenance_page(&$variables) { + $variables['head'] = drupal_get_html_head(); + $variables['css'] = drupal_add_css(); + $variables['styles'] = drupal_get_css(); + $variables['scripts'] = drupal_get_js(); +} + +/** + * Preprocess variables for region.tpl.php + * + * Prepare the values passed to the theme_region function to be passed into a + * pluggable template engine. Uses the region name to generate a template file + * suggestions. If none are found, the default region.tpl.php is used. + * + * @see drupal_region_class() + * @see region.tpl.php + */ +function template_preprocess_region(&$variables) { + // Create the $content variable that templates expect. + $variables['content'] = $variables['elements']['#children']; + $variables['region'] = $variables['elements']['#region']; + + $variables['classes_array'][] = drupal_region_class($variables['region']); + $variables['theme_hook_suggestions'][] = 'region__' . $variables['region']; +} + +/** + * Preprocesses variables for theme_username(). + * + * Modules that make any changes to variables like 'name' or 'extra' must insure + * that the final string is safe to include directly in the output by using + * check_plain() or filter_xss(). + * + * @see template_process_username() + */ +function template_preprocess_username(&$variables) { + $account = $variables['account']; + + $variables['extra'] = ''; + if (empty($account->uid)) { + $variables['uid'] = 0; + if (theme_get_setting('toggle_comment_user_verification')) { + $variables['extra'] = ' (' . t('not verified') . ')'; + } + } + else { + $variables['uid'] = (int) $account->uid; + } + + // Set the name to a formatted name that is safe for printing and + // that won't break tables by being too long. Keep an unshortened, + // unsanitized version, in case other preprocess functions want to implement + // their own shortening logic or add markup. If they do so, they must ensure + // that $variables['name'] is safe for printing. + $name = $variables['name_raw'] = format_username($account); + if (drupal_strlen($name) > 20) { + $name = drupal_substr($name, 0, 15) . '...'; + } + $variables['name'] = check_plain($name); + + $variables['profile_access'] = user_access('access user profiles'); + $variables['link_attributes'] = array(); + // Populate link path and attributes if appropriate. + if ($variables['uid'] && $variables['profile_access']) { + // We are linking to a local user. + $variables['link_attributes'] = array('title' => t('View user profile.')); + $variables['link_path'] = 'user/' . $variables['uid']; + } + elseif (!empty($account->homepage)) { + // Like the 'class' attribute, the 'rel' attribute can hold a + // space-separated set of values, so initialize it as an array to make it + // easier for other preprocess functions to append to it. + $variables['link_attributes'] = array('rel' => array('nofollow')); + $variables['link_path'] = $account->homepage; + $variables['homepage'] = $account->homepage; + } + // We do not want the l() function to check_plain() a second time. + $variables['link_options']['html'] = TRUE; + // Set a default class. + $variables['attributes_array'] = array('class' => array('username')); +} + +/** + * Processes variables for theme_username(). + * + * @see template_preprocess_username() + */ +function template_process_username(&$variables) { + // Finalize the link_options array for passing to the l() function. + // This is done in the process phase so that attributes may be added by + // modules or the theme during the preprocess phase. + if (isset($variables['link_path'])) { + // $variables['attributes_array'] contains attributes that should be applied + // regardless of whether a link is being rendered or not. + // $variables['link_attributes'] contains attributes that should only be + // applied if a link is being rendered. Preprocess functions are encouraged + // to use the former unless they want to add attributes on the link only. + // If a link is being rendered, these need to be merged. Some attributes are + // themselves arrays, so the merging needs to be recursive. + $variables['link_options']['attributes'] = array_merge_recursive($variables['link_attributes'], $variables['attributes_array']); + } +} diff --git a/core/includes/theme.maintenance.inc b/core/includes/theme.maintenance.inc new file mode 100644 index 0000000..fcd8703 --- /dev/null +++ b/core/includes/theme.maintenance.inc @@ -0,0 +1,211 @@ +base_theme)) { + $base_theme[] = $new_base_theme = $themes[$themes[$ancestor]->base_theme]; + $ancestor = $themes[$ancestor]->base_theme; + } + _drupal_theme_initialize($themes[$theme], array_reverse($base_theme), '_theme_load_offline_registry'); + + // These are usually added from system_init() -except maintenance.css. + // When the database is inactive it's not called so we add it here. + $path = drupal_get_path('module', 'system'); + drupal_add_css($path . '/system.base.css'); + drupal_add_css($path . '/system.admin.css'); + drupal_add_css($path . '/system.menus.css'); + drupal_add_css($path . '/system.messages.css'); + drupal_add_css($path . '/system.theme.css'); + drupal_add_css($path . '/system.maintenance.css'); +} + +/** + * This builds the registry when the site needs to bypass any database calls. + */ +function _theme_load_offline_registry($theme, $base_theme = NULL, $theme_engine = NULL) { + return _theme_build_registry($theme, $base_theme, $theme_engine); +} + +/** + * Returns HTML for a list of maintenance tasks to perform. + * + * @param $variables + * An associative array containing: + * - items: An associative array of maintenance tasks. + * - active: The key for the currently active maintenance task. + * + * @ingroup themeable + */ +function theme_task_list($variables) { + $items = $variables['items']; + $active = $variables['active']; + + $done = isset($items[$active]) || $active == NULL; + $output = '

Installation tasks

'; + $output .= '
    '; + + foreach ($items as $k => $item) { + if ($active == $k) { + $class = 'active'; + $status = '(' . t('active') . ')'; + $done = FALSE; + } + else { + $class = $done ? 'done' : ''; + $status = $done ? '(' . t('done') . ')' : ''; + } + $output .= ''; + $output .= $item; + $output .= ($status ? '' . $status . '' : ''); + $output .= ''; + } + $output .= '
'; + return $output; +} + +/** + * Returns HTML for the installation page. + * + * Note: this function is not themeable. + * + * @param $variables + * An associative array containing: + * - content: The page content to show. + */ +function theme_install_page($variables) { + drupal_add_http_header('Content-Type', 'text/html; charset=utf-8'); + return theme('maintenance_page', $variables); +} + +/** + * Returns HTML for the update page. + * + * Note: this function is not themeable. + * + * @param $variables + * An associative array containing: + * - content: The page content to show. + * - show_messages: Whether to output status and error messages. + * FALSE can be useful to postpone the messages to a subsequent page. + */ +function theme_update_page($variables) { + drupal_add_http_header('Content-Type', 'text/html; charset=utf-8'); + return theme('maintenance_page', $variables); +} + +/** + * Returns HTML for a report of the results from an operation run via authorize.php. + * + * @param $variables + * An associative array containing: + * - messages: An array of result messages. + * + * @ingroup themeable + */ +function theme_authorize_report($variables) { + $messages = $variables['messages']; + $output = ''; + if (!empty($messages)) { + $output .= '
'; + foreach ($messages as $heading => $logs) { + $items = array(); + foreach ($logs as $number => $log_message) { + if ($number === '#abort') { + continue; + } + $items[] = theme('authorize_message', array('message' => $log_message['message'], 'success' => $log_message['success'])); + } + $output .= theme('item_list', array('items' => $items, 'title' => $heading)); + } + $output .= '
'; + } + return $output; +} + +/** + * Returns HTML for a single log message from the authorize.php batch operation. + * + * @param $variables + * An associative array containing: + * - message: The log message. + * - success: A boolean indicating failure or success. + * + * @ingroup themeable + */ +function theme_authorize_message($variables) { + $message = $variables['message']; + $success = $variables['success']; + if ($success) { + $item = array('data' => $message, 'class' => array('success')); + } + else { + $item = array('data' => '' . $message . '', 'class' => array('failure')); + } + return $item; +} diff --git a/includes/token.inc b/core/includes/token.inc similarity index 100% rename from includes/token.inc rename to core/includes/token.inc diff --git a/includes/unicode.entities.inc b/core/includes/unicode.entities.inc similarity index 100% rename from includes/unicode.entities.inc rename to core/includes/unicode.entities.inc diff --git a/includes/unicode.inc b/core/includes/unicode.inc similarity index 100% rename from includes/unicode.inc rename to core/includes/unicode.inc diff --git a/core/includes/update.inc b/core/includes/update.inc new file mode 100644 index 0000000..1f9b0a5 --- /dev/null +++ b/core/includes/update.inc @@ -0,0 +1,700 @@ +name, $row->type)) { + $incompatible[] = $row->name; + } + } + if (!empty($incompatible)) { + db_update('system') + ->fields(array('status' => 0)) + ->condition('name', $incompatible, 'IN') + ->execute(); + } +} + +/** + * Helper function to test compatibility of a module or theme. + */ +function update_check_incompatibility($name, $type = 'module') { + static $themes, $modules; + + // Store values of expensive functions for future use. + if (empty($themes) || empty($modules)) { + // We need to do a full rebuild here to make sure the database reflects any + // code changes that were made in the filesystem before the update script + // was initiated. + $themes = system_rebuild_theme_data(); + $modules = system_rebuild_module_data(); + } + + if ($type == 'module' && isset($modules[$name])) { + $file = $modules[$name]; + } + elseif ($type == 'theme' && isset($themes[$name])) { + $file = $themes[$name]; + } + if (!isset($file) + || !isset($file->info['core']) + || $file->info['core'] != DRUPAL_CORE_COMPATIBILITY + || version_compare(phpversion(), $file->info['php']) < 0) { + return TRUE; + } + return FALSE; +} + +/** + * Performs extra steps required to bootstrap when using a Drupal 7 database. + * + * Users who still have a Drupal 7 database (and are in the process of + * updating to Drupal 8) need extra help before a full bootstrap can be + * achieved. This function does the necessary preliminary work that allows + * the bootstrap to be successful. + * + * No access check has been performed when this function is called, so no + * irreversible changes to the database are made here. + */ +function update_prepare_d8_bootstrap() { + // Allow the database system to work even if the registry has not been + // created yet. + include_once DRUPAL_ROOT . '/core/includes/install.inc'; + drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE); + + // If the site has not updated to Drupal 8 yet, check to make sure that it is + // running an up-to-date version of Drupal 7 before proceeding. Note this has + // to happen AFTER the database bootstraps because of + // drupal_get_installed_schema_version(). + $system_schema = drupal_get_installed_schema_version('system'); + if ($system_schema < 8000) { + $has_required_schema = $system_schema >= REQUIRED_D7_SCHEMA_VERSION; + $requirements = array( + 'drupal 7 version' => array( + 'title' => 'Drupal 7 version', + 'value' => $has_required_schema ? 'You are running a current version of Drupal 7.' : 'You are not running a current version of Drupal 7', + 'severity' => $has_required_schema ? REQUIREMENT_OK : REQUIREMENT_ERROR, + 'description' => $has_required_schema ? '' : 'Please update your Drupal 7 installation to the most recent version before attempting to upgrade to Drupal 8', + ), + ); + } +} + +/** + * Perform Drupal 7.x to 8.x updates that are required for update.php + * to function properly. + * + * This function runs when update.php is run the first time for 8.x, + * even before updates are selected or performed. It is important + * that if updates are not ultimately performed that no changes are + * made which make it impossible to continue using the prior version. + */ +function update_fix_d8_requirements() { + global $conf; + + if (drupal_get_installed_schema_version('system') < 8000 && !variable_get('update_d8_requirements', FALSE)) { + // @todo: Make critical, first-run changes to the database here. + variable_set('update_d8_requirements', TRUE); + } +} + +/** + * Perform one update and store the results for display on finished page. + * + * If an update function completes successfully, it should return a message + * as a string indicating success, for example: + * @code + * return t('New index added successfully.'); + * @endcode + * + * Alternatively, it may return nothing. In that case, no message + * will be displayed at all. + * + * If it fails for whatever reason, it should throw an instance of + * DrupalUpdateException with an appropriate error message, for example: + * @code + * throw new DrupalUpdateException(t('Description of what went wrong')); + * @endcode + * + * If an exception is thrown, the current update and all updates that depend on + * it will be aborted. The schema version will not be updated in this case, and + * all the aborted updates will continue to appear on update.php as updates + * that have not yet been run. + * + * If an update function needs to be re-run as part of a batch process, it + * should accept the $sandbox array by reference as its first parameter + * and set the #finished property to the percentage completed that it is, as a + * fraction of 1. + * + * @param $module + * The module whose update will be run. + * @param $number + * The update number to run. + * @param $dependency_map + * An array whose keys are the names of all update functions that will be + * performed during this batch process, and whose values are arrays of other + * update functions that each one depends on. + * @param $context + * The batch context array. + * + * @see update_resolve_dependencies() + */ +function update_do_one($module, $number, $dependency_map, &$context) { + $function = $module . '_update_' . $number; + + // If this update was aborted in a previous step, or has a dependency that + // was aborted in a previous step, go no further. + if (!empty($context['results']['#abort']) && array_intersect($context['results']['#abort'], array_merge($dependency_map, array($function)))) { + return; + } + + $ret = array(); + if (function_exists($function)) { + try { + $ret['results']['query'] = $function($context['sandbox']); + $ret['results']['success'] = TRUE; + } + // @TODO We may want to do different error handling for different + // exception types, but for now we'll just log the exception and + // return the message for printing. + catch (Exception $e) { + watchdog_exception('update', $e); + + require_once DRUPAL_ROOT . '/core/includes/errors.inc'; + $variables = _drupal_decode_exception($e); + // The exception message is run through check_plain() by _drupal_decode_exception(). + $ret['#abort'] = array('success' => FALSE, 'query' => t('%type: !message in %function (line %line of %file).', $variables)); + } + } + + if (isset($context['sandbox']['#finished'])) { + $context['finished'] = $context['sandbox']['#finished']; + unset($context['sandbox']['#finished']); + } + + if (!isset($context['results'][$module])) { + $context['results'][$module] = array(); + } + if (!isset($context['results'][$module][$number])) { + $context['results'][$module][$number] = array(); + } + $context['results'][$module][$number] = array_merge($context['results'][$module][$number], $ret); + + if (!empty($ret['#abort'])) { + // Record this function in the list of updates that were aborted. + $context['results']['#abort'][] = $function; + } + + // Record the schema update if it was completed successfully. + if ($context['finished'] == 1 && empty($ret['#abort'])) { + drupal_set_installed_schema_version($module, $number); + } + + $context['message'] = 'Updating ' . check_plain($module) . ' module'; +} + +/** + * @class Exception class used to throw error if a module update fails. + */ +class DrupalUpdateException extends Exception { } + +/** + * Start the database update batch process. + * + * @param $start + * An array whose keys contain the names of modules to be updated during the + * current batch process, and whose values contain the number of the first + * requested update for that module. The actual updates that are run (and the + * order they are run in) will depend on the results of passing this data + * through the update dependency system. + * @param $redirect + * Path to redirect to when the batch has finished processing. + * @param $url + * URL of the batch processing page (should only be used for separate + * scripts like update.php). + * @param $batch + * Optional parameters to pass into the batch API. + * @param $redirect_callback + * (optional) Specify a function to be called to redirect to the progressive + * processing page. + * + * @see update_resolve_dependencies() + */ +function update_batch($start, $redirect = NULL, $url = NULL, $batch = array(), $redirect_callback = 'drupal_goto') { + // During the update, bring the site offline so that schema changes do not + // affect visiting users. + $_SESSION['maintenance_mode'] = variable_get('maintenance_mode', FALSE); + if ($_SESSION['maintenance_mode'] == FALSE) { + variable_set('maintenance_mode', TRUE); + } + + // Resolve any update dependencies to determine the actual updates that will + // be run and the order they will be run in. + $updates = update_resolve_dependencies($start); + + // Store the dependencies for each update function in an array which the + // batch API can pass in to the batch operation each time it is called. (We + // do not store the entire update dependency array here because it is + // potentially very large.) + $dependency_map = array(); + foreach ($updates as $function => $update) { + $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : array(); + } + + $operations = array(); + foreach ($updates as $update) { + if ($update['allowed']) { + // Set the installed version of each module so updates will start at the + // correct place. (The updates are already sorted, so we can simply base + // this on the first one we come across in the above foreach loop.) + if (isset($start[$update['module']])) { + drupal_set_installed_schema_version($update['module'], $update['number'] - 1); + unset($start[$update['module']]); + } + // Add this update function to the batch. + $function = $update['module'] . '_update_' . $update['number']; + $operations[] = array('update_do_one', array($update['module'], $update['number'], $dependency_map[$function])); + } + } + $batch['operations'] = $operations; + $batch += array( + 'title' => 'Updating', + 'init_message' => 'Starting updates', + 'error_message' => 'An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.', + 'finished' => 'update_finished', + 'file' => 'core/includes/update.inc', + ); + batch_set($batch); + batch_process($redirect, $url, $redirect_callback); +} + +/** + * Finish the update process and store results for eventual display. + * + * After the updates run, all caches are flushed. The update results are + * stored into the session (for example, to be displayed on the update results + * page in update.php). Additionally, if the site was off-line, now that the + * update process is completed, the site is set back online. + * + * @param $success + * Indicate that the batch API tasks were all completed successfully. + * @param $results + * An array of all the results that were updated in update_do_one(). + * @param $operations + * A list of all the operations that had not been completed by the batch API. + * + * @see update_batch() + */ +function update_finished($success, $results, $operations) { + // Clear the caches in case the data has been updated. + drupal_flush_all_caches(); + + $_SESSION['update_results'] = $results; + $_SESSION['update_success'] = $success; + $_SESSION['updates_remaining'] = $operations; + + // Now that the update is done, we can put the site back online if it was + // previously in maintenance mode. + if (isset($_SESSION['maintenance_mode']) && $_SESSION['maintenance_mode'] == FALSE) { + variable_set('maintenance_mode', FALSE); + unset($_SESSION['maintenance_mode']); + } +} + +/** + * Return a list of all the pending database updates. + * + * @return + * An associative array keyed by module name which contains all information + * about database updates that need to be run, and any updates that are not + * going to proceed due to missing requirements. The system module will + * always be listed first. + * + * The subarray for each module can contain the following keys: + * - start: The starting update that is to be processed. If this does not + * exist then do not process any updates for this module as there are + * other requirements that need to be resolved. + * - warning: Any warnings about why this module can not be updated. + * - pending: An array of all the pending updates for the module including + * the update number and the description from source code comment for + * each update function. This array is keyed by the update number. + */ +function update_get_update_list() { + // Make sure that the system module is first in the list of updates. + $ret = array('system' => array()); + + $modules = drupal_get_installed_schema_version(NULL, FALSE, TRUE); + foreach ($modules as $module => $schema_version) { + // Skip uninstalled and incompatible modules. + if ($schema_version == SCHEMA_UNINSTALLED || update_check_incompatibility($module)) { + continue; + } + // Otherwise, get the list of updates defined by this module. + $updates = drupal_get_schema_versions($module); + if ($updates !== FALSE) { + // module_invoke returns NULL for nonexisting hooks, so if no updates + // are removed, it will == 0. + $last_removed = module_invoke($module, 'update_last_removed'); + if ($schema_version < $last_removed) { + $ret[$module]['warning'] = '' . $module . ' module can not be updated. Its schema version is ' . $schema_version . '. Updates up to and including ' . $last_removed . ' have been removed in this release. In order to update ' . $module . ' module, you will first need to upgrade to the last version in which these updates were available.'; + continue; + } + + $updates = drupal_map_assoc($updates); + foreach (array_keys($updates) as $update) { + if ($update > $schema_version) { + // The description for an update comes from its Doxygen. + $func = new ReflectionFunction($module . '_update_' . $update); + $description = str_replace(array("\n", '*', '/'), '', $func->getDocComment()); + $ret[$module]['pending'][$update] = "$update - $description"; + if (!isset($ret[$module]['start'])) { + $ret[$module]['start'] = $update; + } + } + } + if (!isset($ret[$module]['start']) && isset($ret[$module]['pending'])) { + $ret[$module]['start'] = $schema_version; + } + } + } + + if (empty($ret['system'])) { + unset($ret['system']); + } + return $ret; +} + +/** + * Resolves dependencies in a set of module updates, and orders them correctly. + * + * This function receives a list of requested module updates and determines an + * appropriate order to run them in such that all update dependencies are met. + * Any updates whose dependencies cannot be met are included in the returned + * array but have the key 'allowed' set to FALSE; the calling function should + * take responsibility for ensuring that these updates are ultimately not + * performed. + * + * In addition, the returned array also includes detailed information about the + * dependency chain for each update, as provided by the depth-first search + * algorithm in drupal_depth_first_search(). + * + * @param $starting_updates + * An array whose keys contain the names of modules with updates to be run + * and whose values contain the number of the first requested update for that + * module. + * + * @return + * An array whose keys are the names of all update functions within the + * provided modules that would need to be run in order to fulfill the + * request, arranged in the order in which the update functions should be + * run. (This includes the provided starting update for each module and all + * subsequent updates that are available.) The values are themselves arrays + * containing all the keys provided by the drupal_depth_first_search() + * algorithm, which encode detailed information about the dependency chain + * for this update function (for example: 'paths', 'reverse_paths', 'weight', + * and 'component'), as well as the following additional keys: + * - 'allowed': A boolean which is TRUE when the update function's + * dependencies are met, and FALSE otherwise. Calling functions should + * inspect this value before running the update. + * - 'missing_dependencies': An array containing the names of any other + * update functions that are required by this one but that are unavailable + * to be run. This array will be empty when 'allowed' is TRUE. + * - 'module': The name of the module that this update function belongs to. + * - 'number': The number of this update function within that module. + * + * @see drupal_depth_first_search() + */ +function update_resolve_dependencies($starting_updates) { + // Obtain a dependency graph for the requested update functions. + $update_functions = update_get_update_function_list($starting_updates); + $graph = update_build_dependency_graph($update_functions); + + // Perform the depth-first search and sort the results. + require_once DRUPAL_ROOT . '/core/includes/graph.inc'; + drupal_depth_first_search($graph); + uasort($graph, 'drupal_sort_weight'); + + foreach ($graph as $function => &$data) { + $module = $data['module']; + $number = $data['number']; + // If the update function is missing and has not yet been performed, mark + // it and everything that ultimately depends on it as disallowed. + if (update_is_missing($module, $number, $update_functions) && !update_already_performed($module, $number)) { + $data['allowed'] = FALSE; + foreach (array_keys($data['paths']) as $dependent) { + $graph[$dependent]['allowed'] = FALSE; + $graph[$dependent]['missing_dependencies'][] = $function; + } + } + elseif (!isset($data['allowed'])) { + $data['allowed'] = TRUE; + $data['missing_dependencies'] = array(); + } + // Now that we have finished processing this function, remove it from the + // graph if it was not part of the original list. This ensures that we + // never try to run any updates that were not specifically requested. + if (!isset($update_functions[$module][$number])) { + unset($graph[$function]); + } + } + + return $graph; +} + +/** + * Returns an organized list of update functions for a set of modules. + * + * @param $starting_updates + * An array whose keys contain the names of modules and whose values contain + * the number of the first requested update for that module. + * + * @return + * An array containing all the update functions that should be run for each + * module, including the provided starting update and all subsequent updates + * that are available. The keys of the array contain the module names, and + * each value is an ordered array of update functions, keyed by the update + * number. + * + * @see update_resolve_dependencies() + */ +function update_get_update_function_list($starting_updates) { + // Go through each module and find all updates that we need (including the + // first update that was requested and any updates that run after it). + $update_functions = array(); + foreach ($starting_updates as $module => $version) { + $update_functions[$module] = array(); + $updates = drupal_get_schema_versions($module); + if ($updates !== FALSE) { + $max_version = max($updates); + if ($version <= $max_version) { + foreach ($updates as $update) { + if ($update >= $version) { + $update_functions[$module][$update] = $module . '_update_' . $update; + } + } + } + } + } + return $update_functions; +} + +/** + * Constructs a graph which encodes the dependencies between module updates. + * + * This function returns an associative array which contains a "directed graph" + * representation of the dependencies between a provided list of update + * functions, as well as any outside update functions that they directly depend + * on but that were not in the provided list. The vertices of the graph + * represent the update functions themselves, and each edge represents a + * requirement that the first update function needs to run before the second. + * For example, consider this graph: + * + * system_update_8000 ---> system_update_8001 ---> system_update_8002 + * + * Visually, this indicates that system_update_8000() must run before + * system_update_8001(), which in turn must run before system_update_8002(). + * + * The function takes into account standard dependencies within each module, as + * shown above (i.e., the fact that each module's updates must run in numerical + * order), but also finds any cross-module dependencies that are defined by + * modules which implement hook_update_dependencies(), and builds them into the + * graph as well. + * + * @param $update_functions + * An organized array of update functions, in the format returned by + * update_get_update_function_list(). + * + * @return + * A multidimensional array representing the dependency graph, suitable for + * passing in to drupal_depth_first_search(), but with extra information + * about each update function also included. Each array key contains the name + * of an update function, including all update functions from the provided + * list as well as any outside update functions which they directly depend + * on. Each value is an associative array containing the following keys: + * - 'edges': A representation of any other update functions that immediately + * depend on this one. See drupal_depth_first_search() for more details on + * the format. + * - 'module': The name of the module that this update function belongs to. + * - 'number': The number of this update function within that module. + * + * @see drupal_depth_first_search() + * @see update_resolve_dependencies() + */ +function update_build_dependency_graph($update_functions) { + // Initialize an array that will define a directed graph representing the + // dependencies between update functions. + $graph = array(); + + // Go through each update function and build an initial list of dependencies. + foreach ($update_functions as $module => $functions) { + $previous_function = NULL; + foreach ($functions as $number => $function) { + // Add an edge to the directed graph representing the fact that each + // update function in a given module must run after the update that + // numerically precedes it. + if ($previous_function) { + $graph[$previous_function]['edges'][$function] = TRUE; + } + $previous_function = $function; + + // Define the module and update number associated with this function. + $graph[$function]['module'] = $module; + $graph[$function]['number'] = $number; + } + } + + // Now add any explicit update dependencies declared by modules. + $update_dependencies = update_retrieve_dependencies(); + foreach ($graph as $function => $data) { + if (!empty($update_dependencies[$data['module']][$data['number']])) { + foreach ($update_dependencies[$data['module']][$data['number']] as $module => $number) { + $dependency = $module . '_update_' . $number; + $graph[$dependency]['edges'][$function] = TRUE; + $graph[$dependency]['module'] = $module; + $graph[$dependency]['number'] = $number; + } + } + } + + return $graph; +} + +/** + * Determines if a module update is missing or unavailable. + * + * @param $module + * The name of the module. + * @param $number + * The number of the update within that module. + * @param $update_functions + * An organized array of update functions, in the format returned by + * update_get_update_function_list(). This should represent all module + * updates that are requested to run at the time this function is called. + * + * @return + * TRUE if the provided module update is not installed or is not in the + * provided list of updates to run; FALSE otherwise. + */ +function update_is_missing($module, $number, $update_functions) { + return !isset($update_functions[$module][$number]) || !function_exists($update_functions[$module][$number]); +} + +/** + * Determines if a module update has already been performed. + * + * @param $module + * The name of the module. + * @param $number + * The number of the update within that module. + * + * @return + * TRUE if the database schema indicates that the update has already been + * performed; FALSE otherwise. + */ +function update_already_performed($module, $number) { + return $number <= drupal_get_installed_schema_version($module); +} + +/** + * Invoke hook_update_dependencies() in all installed modules. + * + * This function is similar to module_invoke_all(), with the main difference + * that it does not require that a module be enabled to invoke its hook, only + * that it be installed. This allows the update system to properly perform + * updates even on modules that are currently disabled. + * + * @return + * An array of return values obtained by merging the results of the + * hook_update_dependencies() implementations in all installed modules. + * + * @see module_invoke_all() + * @see hook_update_dependencies() + */ +function update_retrieve_dependencies() { + $return = array(); + // Get a list of installed modules, arranged so that we invoke their hooks in + // the same order that module_invoke_all() does. + $modules = db_query("SELECT name FROM {system} WHERE type = 'module' AND schema_version != :schema ORDER BY weight ASC, name ASC", array(':schema' => SCHEMA_UNINSTALLED))->fetchCol(); + foreach ($modules as $module) { + $function = $module . '_update_dependencies'; + if (function_exists($function)) { + $result = $function(); + // Each implementation of hook_update_dependencies() returns a + // multidimensional, associative array containing some keys that + // represent module names (which are strings) and other keys that + // represent update function numbers (which are integers). We cannot use + // array_merge_recursive() to properly merge these results, since it + // treats strings and integers differently. Therefore, we have to + // explicitly loop through the expected array structure here and perform + // the merge manually. + if (isset($result) && is_array($result)) { + foreach ($result as $module => $module_data) { + foreach ($module_data as $update => $update_data) { + foreach ($update_data as $module_dependency => $update_dependency) { + // If there are redundant dependencies declared for the same + // update function (so that it is declared to depend on more than + // one update from a particular module), record the dependency on + // the highest numbered update here, since that automatically + // implies the previous ones. For example, if one module's + // implementation of hook_update_dependencies() required this + // ordering: + // + // system_update_8001 ---> user_update_8000 + // + // but another module's implementation of the hook required this + // one: + // + // system_update_8002 ---> user_update_8000 + // + // we record the second one, since system_update_8001() is always + // guaranteed to run before system_update_8002() anyway (within + // an individual module, updates are always run in numerical + // order). + if (!isset($return[$module][$update][$module_dependency]) || $update_dependency > $return[$module][$update][$module_dependency]) { + $return[$module][$update][$module_dependency] = $update_dependency; + } + } + } + } + } + } + } + + return $return; +} + +/** + * @defgroup update-api-7.x-to-8.x Update versions of API functions + * @{ + * Functions similar to normal API function but not firing hooks. + * + * During update, it is impossible to judge the consequences of firing a hook + * as it might hit a module not yet updated. So simplified versions of some + * core APIs are provided. + */ + +/** + * @} End of "defgroup update-api-7.x-to-8.x" + */ diff --git a/includes/updater.inc b/core/includes/updater.inc similarity index 100% rename from includes/updater.inc rename to core/includes/updater.inc diff --git a/includes/utility.inc b/core/includes/utility.inc similarity index 100% rename from includes/utility.inc rename to core/includes/utility.inc diff --git a/includes/xmlrpc.inc b/core/includes/xmlrpc.inc similarity index 100% rename from includes/xmlrpc.inc rename to core/includes/xmlrpc.inc diff --git a/includes/xmlrpcs.inc b/core/includes/xmlrpcs.inc similarity index 100% rename from includes/xmlrpcs.inc rename to core/includes/xmlrpcs.inc diff --git a/core/install.php b/core/install.php new file mode 100644 index 0000000..01f54b7 --- /dev/null +++ b/core/install.php @@ -0,0 +1,32 @@ +system requirements page for more information.'; + exit; +} + +// Start the installer. +require_once DRUPAL_ROOT . '/core/includes/install.core.inc'; +install_drupal(); diff --git a/misc/ajax.js b/core/misc/ajax.js similarity index 100% rename from misc/ajax.js rename to core/misc/ajax.js diff --git a/misc/arrow-asc.png b/core/misc/arrow-asc.png similarity index 100% rename from misc/arrow-asc.png rename to core/misc/arrow-asc.png diff --git a/misc/arrow-desc.png b/core/misc/arrow-desc.png similarity index 100% rename from misc/arrow-desc.png rename to core/misc/arrow-desc.png diff --git a/misc/authorize.js b/core/misc/authorize.js similarity index 100% rename from misc/authorize.js rename to core/misc/authorize.js diff --git a/misc/autocomplete.js b/core/misc/autocomplete.js similarity index 100% rename from misc/autocomplete.js rename to core/misc/autocomplete.js diff --git a/misc/batch.js b/core/misc/batch.js similarity index 100% rename from misc/batch.js rename to core/misc/batch.js diff --git a/misc/collapse.js b/core/misc/collapse.js similarity index 100% rename from misc/collapse.js rename to core/misc/collapse.js diff --git a/misc/configure.png b/core/misc/configure.png similarity index 100% rename from misc/configure.png rename to core/misc/configure.png diff --git a/misc/draggable.png b/core/misc/draggable.png similarity index 100% rename from misc/draggable.png rename to core/misc/draggable.png diff --git a/misc/drupal.js b/core/misc/drupal.js similarity index 100% rename from misc/drupal.js rename to core/misc/drupal.js diff --git a/misc/druplicon.png b/core/misc/druplicon.png similarity index 100% rename from misc/druplicon.png rename to core/misc/druplicon.png diff --git a/misc/farbtastic/farbtastic.css b/core/misc/farbtastic/farbtastic.css similarity index 100% rename from misc/farbtastic/farbtastic.css rename to core/misc/farbtastic/farbtastic.css diff --git a/misc/farbtastic/farbtastic.js b/core/misc/farbtastic/farbtastic.js similarity index 100% rename from misc/farbtastic/farbtastic.js rename to core/misc/farbtastic/farbtastic.js diff --git a/misc/farbtastic/marker.png b/core/misc/farbtastic/marker.png similarity index 100% rename from misc/farbtastic/marker.png rename to core/misc/farbtastic/marker.png diff --git a/misc/farbtastic/mask.png b/core/misc/farbtastic/mask.png similarity index 100% rename from misc/farbtastic/mask.png rename to core/misc/farbtastic/mask.png diff --git a/misc/farbtastic/wheel.png b/core/misc/farbtastic/wheel.png similarity index 100% rename from misc/farbtastic/wheel.png rename to core/misc/farbtastic/wheel.png diff --git a/misc/favicon.ico b/core/misc/favicon.ico similarity index 100% rename from misc/favicon.ico rename to core/misc/favicon.ico diff --git a/misc/feed.png b/core/misc/feed.png similarity index 100% rename from misc/feed.png rename to core/misc/feed.png diff --git a/misc/form.js b/core/misc/form.js similarity index 100% rename from misc/form.js rename to core/misc/form.js diff --git a/misc/forum-icons.png b/core/misc/forum-icons.png similarity index 100% rename from misc/forum-icons.png rename to core/misc/forum-icons.png diff --git a/misc/grippie.png b/core/misc/grippie.png similarity index 100% rename from misc/grippie.png rename to core/misc/grippie.png diff --git a/misc/help.png b/core/misc/help.png similarity index 100% rename from misc/help.png rename to core/misc/help.png diff --git a/misc/jquery.ba-bbq.js b/core/misc/jquery.ba-bbq.js similarity index 100% rename from misc/jquery.ba-bbq.js rename to core/misc/jquery.ba-bbq.js diff --git a/misc/jquery.cookie.js b/core/misc/jquery.cookie.js similarity index 100% rename from misc/jquery.cookie.js rename to core/misc/jquery.cookie.js diff --git a/misc/jquery.form.js b/core/misc/jquery.form.js similarity index 100% rename from misc/jquery.form.js rename to core/misc/jquery.form.js diff --git a/misc/jquery.js b/core/misc/jquery.js similarity index 100% rename from misc/jquery.js rename to core/misc/jquery.js diff --git a/misc/jquery.once.js b/core/misc/jquery.once.js similarity index 100% rename from misc/jquery.once.js rename to core/misc/jquery.once.js diff --git a/misc/machine-name.js b/core/misc/machine-name.js similarity index 100% rename from misc/machine-name.js rename to core/misc/machine-name.js diff --git a/misc/menu-collapsed-rtl.png b/core/misc/menu-collapsed-rtl.png similarity index 100% rename from misc/menu-collapsed-rtl.png rename to core/misc/menu-collapsed-rtl.png diff --git a/misc/menu-collapsed.png b/core/misc/menu-collapsed.png similarity index 100% rename from misc/menu-collapsed.png rename to core/misc/menu-collapsed.png diff --git a/misc/menu-expanded.png b/core/misc/menu-expanded.png similarity index 100% rename from misc/menu-expanded.png rename to core/misc/menu-expanded.png diff --git a/misc/menu-leaf.png b/core/misc/menu-leaf.png similarity index 100% rename from misc/menu-leaf.png rename to core/misc/menu-leaf.png diff --git a/misc/message-16-error.png b/core/misc/message-16-error.png similarity index 100% rename from misc/message-16-error.png rename to core/misc/message-16-error.png diff --git a/misc/message-16-help.png b/core/misc/message-16-help.png similarity index 100% rename from misc/message-16-help.png rename to core/misc/message-16-help.png diff --git a/misc/message-16-info.png b/core/misc/message-16-info.png similarity index 100% rename from misc/message-16-info.png rename to core/misc/message-16-info.png diff --git a/misc/message-16-ok.png b/core/misc/message-16-ok.png similarity index 100% rename from misc/message-16-ok.png rename to core/misc/message-16-ok.png diff --git a/misc/message-16-warning.png b/core/misc/message-16-warning.png similarity index 100% rename from misc/message-16-warning.png rename to core/misc/message-16-warning.png diff --git a/misc/message-24-error.png b/core/misc/message-24-error.png similarity index 100% rename from misc/message-24-error.png rename to core/misc/message-24-error.png diff --git a/misc/message-24-help.png b/core/misc/message-24-help.png similarity index 100% rename from misc/message-24-help.png rename to core/misc/message-24-help.png diff --git a/misc/message-24-info.png b/core/misc/message-24-info.png similarity index 100% rename from misc/message-24-info.png rename to core/misc/message-24-info.png diff --git a/misc/message-24-ok.png b/core/misc/message-24-ok.png similarity index 100% rename from misc/message-24-ok.png rename to core/misc/message-24-ok.png diff --git a/misc/message-24-warning.png b/core/misc/message-24-warning.png similarity index 100% rename from misc/message-24-warning.png rename to core/misc/message-24-warning.png diff --git a/misc/permissions.png b/core/misc/permissions.png similarity index 100% rename from misc/permissions.png rename to core/misc/permissions.png diff --git a/misc/powered-black-135x42.png b/core/misc/powered-black-135x42.png similarity index 100% rename from misc/powered-black-135x42.png rename to core/misc/powered-black-135x42.png diff --git a/misc/powered-black-80x15.png b/core/misc/powered-black-80x15.png similarity index 100% rename from misc/powered-black-80x15.png rename to core/misc/powered-black-80x15.png diff --git a/misc/powered-black-88x31.png b/core/misc/powered-black-88x31.png similarity index 100% rename from misc/powered-black-88x31.png rename to core/misc/powered-black-88x31.png diff --git a/misc/powered-blue-135x42.png b/core/misc/powered-blue-135x42.png similarity index 100% rename from misc/powered-blue-135x42.png rename to core/misc/powered-blue-135x42.png diff --git a/misc/powered-blue-80x15.png b/core/misc/powered-blue-80x15.png similarity index 100% rename from misc/powered-blue-80x15.png rename to core/misc/powered-blue-80x15.png diff --git a/misc/powered-blue-88x31.png b/core/misc/powered-blue-88x31.png similarity index 100% rename from misc/powered-blue-88x31.png rename to core/misc/powered-blue-88x31.png diff --git a/misc/powered-gray-135x42.png b/core/misc/powered-gray-135x42.png similarity index 100% rename from misc/powered-gray-135x42.png rename to core/misc/powered-gray-135x42.png diff --git a/misc/powered-gray-80x15.png b/core/misc/powered-gray-80x15.png similarity index 100% rename from misc/powered-gray-80x15.png rename to core/misc/powered-gray-80x15.png diff --git a/misc/powered-gray-88x31.png b/core/misc/powered-gray-88x31.png similarity index 100% rename from misc/powered-gray-88x31.png rename to core/misc/powered-gray-88x31.png diff --git a/misc/print-rtl.css b/core/misc/print-rtl.css similarity index 100% rename from misc/print-rtl.css rename to core/misc/print-rtl.css diff --git a/misc/print.css b/core/misc/print.css similarity index 100% rename from misc/print.css rename to core/misc/print.css diff --git a/misc/progress.gif b/core/misc/progress.gif similarity index 100% rename from misc/progress.gif rename to core/misc/progress.gif diff --git a/misc/progress.js b/core/misc/progress.js similarity index 100% rename from misc/progress.js rename to core/misc/progress.js diff --git a/misc/states.js b/core/misc/states.js similarity index 100% rename from misc/states.js rename to core/misc/states.js diff --git a/misc/tabledrag.js b/core/misc/tabledrag.js similarity index 100% rename from misc/tabledrag.js rename to core/misc/tabledrag.js diff --git a/misc/tableheader.js b/core/misc/tableheader.js similarity index 100% rename from misc/tableheader.js rename to core/misc/tableheader.js diff --git a/misc/tableselect.js b/core/misc/tableselect.js similarity index 100% rename from misc/tableselect.js rename to core/misc/tableselect.js diff --git a/misc/textarea.js b/core/misc/textarea.js similarity index 100% rename from misc/textarea.js rename to core/misc/textarea.js diff --git a/misc/throbber.gif b/core/misc/throbber.gif similarity index 100% rename from misc/throbber.gif rename to core/misc/throbber.gif diff --git a/misc/timezone.js b/core/misc/timezone.js similarity index 100% rename from misc/timezone.js rename to core/misc/timezone.js diff --git a/misc/tree-bottom.png b/core/misc/tree-bottom.png similarity index 100% rename from misc/tree-bottom.png rename to core/misc/tree-bottom.png diff --git a/misc/tree.png b/core/misc/tree.png similarity index 100% rename from misc/tree.png rename to core/misc/tree.png diff --git a/misc/ui/images/ui-bg_flat_0_aaaaaa_40x100.png b/core/misc/ui/images/ui-bg_flat_0_aaaaaa_40x100.png similarity index 100% rename from misc/ui/images/ui-bg_flat_0_aaaaaa_40x100.png rename to core/misc/ui/images/ui-bg_flat_0_aaaaaa_40x100.png diff --git a/misc/ui/images/ui-bg_flat_75_ffffff_40x100.png b/core/misc/ui/images/ui-bg_flat_75_ffffff_40x100.png similarity index 100% rename from misc/ui/images/ui-bg_flat_75_ffffff_40x100.png rename to core/misc/ui/images/ui-bg_flat_75_ffffff_40x100.png diff --git a/misc/ui/images/ui-bg_glass_55_fbf9ee_1x400.png b/core/misc/ui/images/ui-bg_glass_55_fbf9ee_1x400.png similarity index 100% rename from misc/ui/images/ui-bg_glass_55_fbf9ee_1x400.png rename to core/misc/ui/images/ui-bg_glass_55_fbf9ee_1x400.png diff --git a/misc/ui/images/ui-bg_glass_65_ffffff_1x400.png b/core/misc/ui/images/ui-bg_glass_65_ffffff_1x400.png similarity index 100% rename from misc/ui/images/ui-bg_glass_65_ffffff_1x400.png rename to core/misc/ui/images/ui-bg_glass_65_ffffff_1x400.png diff --git a/misc/ui/images/ui-bg_glass_75_dadada_1x400.png b/core/misc/ui/images/ui-bg_glass_75_dadada_1x400.png similarity index 100% rename from misc/ui/images/ui-bg_glass_75_dadada_1x400.png rename to core/misc/ui/images/ui-bg_glass_75_dadada_1x400.png diff --git a/misc/ui/images/ui-bg_glass_75_e6e6e6_1x400.png b/core/misc/ui/images/ui-bg_glass_75_e6e6e6_1x400.png similarity index 100% rename from misc/ui/images/ui-bg_glass_75_e6e6e6_1x400.png rename to core/misc/ui/images/ui-bg_glass_75_e6e6e6_1x400.png diff --git a/misc/ui/images/ui-bg_glass_95_fef1ec_1x400.png b/core/misc/ui/images/ui-bg_glass_95_fef1ec_1x400.png similarity index 100% rename from misc/ui/images/ui-bg_glass_95_fef1ec_1x400.png rename to core/misc/ui/images/ui-bg_glass_95_fef1ec_1x400.png diff --git a/misc/ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/core/misc/ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png similarity index 100% rename from misc/ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png rename to core/misc/ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png diff --git a/misc/ui/images/ui-icons_222222_256x240.png b/core/misc/ui/images/ui-icons_222222_256x240.png similarity index 100% rename from misc/ui/images/ui-icons_222222_256x240.png rename to core/misc/ui/images/ui-icons_222222_256x240.png diff --git a/misc/ui/images/ui-icons_2e83ff_256x240.png b/core/misc/ui/images/ui-icons_2e83ff_256x240.png similarity index 100% rename from misc/ui/images/ui-icons_2e83ff_256x240.png rename to core/misc/ui/images/ui-icons_2e83ff_256x240.png diff --git a/misc/ui/images/ui-icons_454545_256x240.png b/core/misc/ui/images/ui-icons_454545_256x240.png similarity index 100% rename from misc/ui/images/ui-icons_454545_256x240.png rename to core/misc/ui/images/ui-icons_454545_256x240.png diff --git a/misc/ui/images/ui-icons_888888_256x240.png b/core/misc/ui/images/ui-icons_888888_256x240.png similarity index 100% rename from misc/ui/images/ui-icons_888888_256x240.png rename to core/misc/ui/images/ui-icons_888888_256x240.png diff --git a/misc/ui/images/ui-icons_cd0a0a_256x240.png b/core/misc/ui/images/ui-icons_cd0a0a_256x240.png similarity index 100% rename from misc/ui/images/ui-icons_cd0a0a_256x240.png rename to core/misc/ui/images/ui-icons_cd0a0a_256x240.png diff --git a/misc/ui/jquery.effects.blind.min.js b/core/misc/ui/jquery.effects.blind.min.js similarity index 100% rename from misc/ui/jquery.effects.blind.min.js rename to core/misc/ui/jquery.effects.blind.min.js diff --git a/misc/ui/jquery.effects.bounce.min.js b/core/misc/ui/jquery.effects.bounce.min.js similarity index 100% rename from misc/ui/jquery.effects.bounce.min.js rename to core/misc/ui/jquery.effects.bounce.min.js diff --git a/misc/ui/jquery.effects.clip.min.js b/core/misc/ui/jquery.effects.clip.min.js similarity index 100% rename from misc/ui/jquery.effects.clip.min.js rename to core/misc/ui/jquery.effects.clip.min.js diff --git a/misc/ui/jquery.effects.core.min.js b/core/misc/ui/jquery.effects.core.min.js similarity index 100% rename from misc/ui/jquery.effects.core.min.js rename to core/misc/ui/jquery.effects.core.min.js diff --git a/misc/ui/jquery.effects.drop.min.js b/core/misc/ui/jquery.effects.drop.min.js similarity index 100% rename from misc/ui/jquery.effects.drop.min.js rename to core/misc/ui/jquery.effects.drop.min.js diff --git a/misc/ui/jquery.effects.explode.min.js b/core/misc/ui/jquery.effects.explode.min.js similarity index 100% rename from misc/ui/jquery.effects.explode.min.js rename to core/misc/ui/jquery.effects.explode.min.js diff --git a/misc/ui/jquery.effects.fade.min.js b/core/misc/ui/jquery.effects.fade.min.js similarity index 100% rename from misc/ui/jquery.effects.fade.min.js rename to core/misc/ui/jquery.effects.fade.min.js diff --git a/misc/ui/jquery.effects.fold.min.js b/core/misc/ui/jquery.effects.fold.min.js similarity index 100% rename from misc/ui/jquery.effects.fold.min.js rename to core/misc/ui/jquery.effects.fold.min.js diff --git a/misc/ui/jquery.effects.highlight.min.js b/core/misc/ui/jquery.effects.highlight.min.js similarity index 100% rename from misc/ui/jquery.effects.highlight.min.js rename to core/misc/ui/jquery.effects.highlight.min.js diff --git a/misc/ui/jquery.effects.pulsate.min.js b/core/misc/ui/jquery.effects.pulsate.min.js similarity index 100% rename from misc/ui/jquery.effects.pulsate.min.js rename to core/misc/ui/jquery.effects.pulsate.min.js diff --git a/misc/ui/jquery.effects.scale.min.js b/core/misc/ui/jquery.effects.scale.min.js similarity index 100% rename from misc/ui/jquery.effects.scale.min.js rename to core/misc/ui/jquery.effects.scale.min.js diff --git a/misc/ui/jquery.effects.shake.min.js b/core/misc/ui/jquery.effects.shake.min.js similarity index 100% rename from misc/ui/jquery.effects.shake.min.js rename to core/misc/ui/jquery.effects.shake.min.js diff --git a/misc/ui/jquery.effects.slide.min.js b/core/misc/ui/jquery.effects.slide.min.js similarity index 100% rename from misc/ui/jquery.effects.slide.min.js rename to core/misc/ui/jquery.effects.slide.min.js diff --git a/misc/ui/jquery.effects.transfer.min.js b/core/misc/ui/jquery.effects.transfer.min.js similarity index 100% rename from misc/ui/jquery.effects.transfer.min.js rename to core/misc/ui/jquery.effects.transfer.min.js diff --git a/misc/ui/jquery.ui.accordion.css b/core/misc/ui/jquery.ui.accordion.css similarity index 100% rename from misc/ui/jquery.ui.accordion.css rename to core/misc/ui/jquery.ui.accordion.css diff --git a/misc/ui/jquery.ui.accordion.min.js b/core/misc/ui/jquery.ui.accordion.min.js similarity index 100% rename from misc/ui/jquery.ui.accordion.min.js rename to core/misc/ui/jquery.ui.accordion.min.js diff --git a/misc/ui/jquery.ui.autocomplete.css b/core/misc/ui/jquery.ui.autocomplete.css similarity index 100% rename from misc/ui/jquery.ui.autocomplete.css rename to core/misc/ui/jquery.ui.autocomplete.css diff --git a/misc/ui/jquery.ui.autocomplete.min.js b/core/misc/ui/jquery.ui.autocomplete.min.js similarity index 100% rename from misc/ui/jquery.ui.autocomplete.min.js rename to core/misc/ui/jquery.ui.autocomplete.min.js diff --git a/misc/ui/jquery.ui.button.css b/core/misc/ui/jquery.ui.button.css similarity index 100% rename from misc/ui/jquery.ui.button.css rename to core/misc/ui/jquery.ui.button.css diff --git a/misc/ui/jquery.ui.button.min.js b/core/misc/ui/jquery.ui.button.min.js similarity index 100% rename from misc/ui/jquery.ui.button.min.js rename to core/misc/ui/jquery.ui.button.min.js diff --git a/misc/ui/jquery.ui.core.css b/core/misc/ui/jquery.ui.core.css similarity index 100% rename from misc/ui/jquery.ui.core.css rename to core/misc/ui/jquery.ui.core.css diff --git a/misc/ui/jquery.ui.core.min.js b/core/misc/ui/jquery.ui.core.min.js similarity index 100% rename from misc/ui/jquery.ui.core.min.js rename to core/misc/ui/jquery.ui.core.min.js diff --git a/misc/ui/jquery.ui.datepicker.css b/core/misc/ui/jquery.ui.datepicker.css similarity index 100% rename from misc/ui/jquery.ui.datepicker.css rename to core/misc/ui/jquery.ui.datepicker.css diff --git a/misc/ui/jquery.ui.datepicker.min.js b/core/misc/ui/jquery.ui.datepicker.min.js similarity index 100% rename from misc/ui/jquery.ui.datepicker.min.js rename to core/misc/ui/jquery.ui.datepicker.min.js diff --git a/misc/ui/jquery.ui.dialog.css b/core/misc/ui/jquery.ui.dialog.css similarity index 100% rename from misc/ui/jquery.ui.dialog.css rename to core/misc/ui/jquery.ui.dialog.css diff --git a/misc/ui/jquery.ui.dialog.min.js b/core/misc/ui/jquery.ui.dialog.min.js similarity index 100% rename from misc/ui/jquery.ui.dialog.min.js rename to core/misc/ui/jquery.ui.dialog.min.js diff --git a/misc/ui/jquery.ui.draggable.min.js b/core/misc/ui/jquery.ui.draggable.min.js similarity index 100% rename from misc/ui/jquery.ui.draggable.min.js rename to core/misc/ui/jquery.ui.draggable.min.js diff --git a/misc/ui/jquery.ui.droppable.min.js b/core/misc/ui/jquery.ui.droppable.min.js similarity index 100% rename from misc/ui/jquery.ui.droppable.min.js rename to core/misc/ui/jquery.ui.droppable.min.js diff --git a/misc/ui/jquery.ui.mouse.min.js b/core/misc/ui/jquery.ui.mouse.min.js similarity index 100% rename from misc/ui/jquery.ui.mouse.min.js rename to core/misc/ui/jquery.ui.mouse.min.js diff --git a/misc/ui/jquery.ui.position.min.js b/core/misc/ui/jquery.ui.position.min.js similarity index 100% rename from misc/ui/jquery.ui.position.min.js rename to core/misc/ui/jquery.ui.position.min.js diff --git a/misc/ui/jquery.ui.progressbar.css b/core/misc/ui/jquery.ui.progressbar.css similarity index 100% rename from misc/ui/jquery.ui.progressbar.css rename to core/misc/ui/jquery.ui.progressbar.css diff --git a/misc/ui/jquery.ui.progressbar.min.js b/core/misc/ui/jquery.ui.progressbar.min.js similarity index 100% rename from misc/ui/jquery.ui.progressbar.min.js rename to core/misc/ui/jquery.ui.progressbar.min.js diff --git a/misc/ui/jquery.ui.resizable.css b/core/misc/ui/jquery.ui.resizable.css similarity index 100% rename from misc/ui/jquery.ui.resizable.css rename to core/misc/ui/jquery.ui.resizable.css diff --git a/misc/ui/jquery.ui.resizable.min.js b/core/misc/ui/jquery.ui.resizable.min.js similarity index 100% rename from misc/ui/jquery.ui.resizable.min.js rename to core/misc/ui/jquery.ui.resizable.min.js diff --git a/misc/ui/jquery.ui.selectable.css b/core/misc/ui/jquery.ui.selectable.css similarity index 100% rename from misc/ui/jquery.ui.selectable.css rename to core/misc/ui/jquery.ui.selectable.css diff --git a/misc/ui/jquery.ui.selectable.min.js b/core/misc/ui/jquery.ui.selectable.min.js similarity index 100% rename from misc/ui/jquery.ui.selectable.min.js rename to core/misc/ui/jquery.ui.selectable.min.js diff --git a/misc/ui/jquery.ui.slider.css b/core/misc/ui/jquery.ui.slider.css similarity index 100% rename from misc/ui/jquery.ui.slider.css rename to core/misc/ui/jquery.ui.slider.css diff --git a/misc/ui/jquery.ui.slider.min.js b/core/misc/ui/jquery.ui.slider.min.js similarity index 100% rename from misc/ui/jquery.ui.slider.min.js rename to core/misc/ui/jquery.ui.slider.min.js diff --git a/misc/ui/jquery.ui.sortable.min.js b/core/misc/ui/jquery.ui.sortable.min.js similarity index 100% rename from misc/ui/jquery.ui.sortable.min.js rename to core/misc/ui/jquery.ui.sortable.min.js diff --git a/misc/ui/jquery.ui.tabs.css b/core/misc/ui/jquery.ui.tabs.css similarity index 100% rename from misc/ui/jquery.ui.tabs.css rename to core/misc/ui/jquery.ui.tabs.css diff --git a/misc/ui/jquery.ui.tabs.min.js b/core/misc/ui/jquery.ui.tabs.min.js similarity index 100% rename from misc/ui/jquery.ui.tabs.min.js rename to core/misc/ui/jquery.ui.tabs.min.js diff --git a/misc/ui/jquery.ui.theme.css b/core/misc/ui/jquery.ui.theme.css similarity index 100% rename from misc/ui/jquery.ui.theme.css rename to core/misc/ui/jquery.ui.theme.css diff --git a/misc/ui/jquery.ui.widget.min.js b/core/misc/ui/jquery.ui.widget.min.js similarity index 100% rename from misc/ui/jquery.ui.widget.min.js rename to core/misc/ui/jquery.ui.widget.min.js diff --git a/misc/vertical-tabs-rtl.css b/core/misc/vertical-tabs-rtl.css similarity index 100% rename from misc/vertical-tabs-rtl.css rename to core/misc/vertical-tabs-rtl.css diff --git a/misc/vertical-tabs.css b/core/misc/vertical-tabs.css similarity index 100% rename from misc/vertical-tabs.css rename to core/misc/vertical-tabs.css diff --git a/misc/vertical-tabs.js b/core/misc/vertical-tabs.js similarity index 100% rename from misc/vertical-tabs.js rename to core/misc/vertical-tabs.js diff --git a/misc/watchdog-error.png b/core/misc/watchdog-error.png similarity index 100% rename from misc/watchdog-error.png rename to core/misc/watchdog-error.png diff --git a/misc/watchdog-ok.png b/core/misc/watchdog-ok.png similarity index 100% rename from misc/watchdog-ok.png rename to core/misc/watchdog-ok.png diff --git a/misc/watchdog-warning.png b/core/misc/watchdog-warning.png similarity index 100% rename from misc/watchdog-warning.png rename to core/misc/watchdog-warning.png diff --git a/modules/README.txt b/core/modules/README.txt similarity index 100% rename from modules/README.txt rename to core/modules/README.txt diff --git a/modules/aggregator/aggregator-feed-source.tpl.php b/core/modules/aggregator/aggregator-feed-source.tpl.php similarity index 100% rename from modules/aggregator/aggregator-feed-source.tpl.php rename to core/modules/aggregator/aggregator-feed-source.tpl.php diff --git a/modules/aggregator/aggregator-item.tpl.php b/core/modules/aggregator/aggregator-item.tpl.php similarity index 100% rename from modules/aggregator/aggregator-item.tpl.php rename to core/modules/aggregator/aggregator-item.tpl.php diff --git a/modules/aggregator/aggregator-rtl.css b/core/modules/aggregator/aggregator-rtl.css similarity index 100% rename from modules/aggregator/aggregator-rtl.css rename to core/modules/aggregator/aggregator-rtl.css diff --git a/modules/aggregator/aggregator-summary-item.tpl.php b/core/modules/aggregator/aggregator-summary-item.tpl.php similarity index 100% rename from modules/aggregator/aggregator-summary-item.tpl.php rename to core/modules/aggregator/aggregator-summary-item.tpl.php diff --git a/modules/aggregator/aggregator-summary-items.tpl.php b/core/modules/aggregator/aggregator-summary-items.tpl.php similarity index 100% rename from modules/aggregator/aggregator-summary-items.tpl.php rename to core/modules/aggregator/aggregator-summary-items.tpl.php diff --git a/modules/aggregator/aggregator-wrapper.tpl.php b/core/modules/aggregator/aggregator-wrapper.tpl.php similarity index 100% rename from modules/aggregator/aggregator-wrapper.tpl.php rename to core/modules/aggregator/aggregator-wrapper.tpl.php diff --git a/modules/aggregator/aggregator.admin.inc b/core/modules/aggregator/aggregator.admin.inc similarity index 100% rename from modules/aggregator/aggregator.admin.inc rename to core/modules/aggregator/aggregator.admin.inc diff --git a/modules/aggregator/aggregator.api.php b/core/modules/aggregator/aggregator.api.php similarity index 100% rename from modules/aggregator/aggregator.api.php rename to core/modules/aggregator/aggregator.api.php diff --git a/modules/aggregator/aggregator.css b/core/modules/aggregator/aggregator.css similarity index 100% rename from modules/aggregator/aggregator.css rename to core/modules/aggregator/aggregator.css diff --git a/modules/aggregator/aggregator.fetcher.inc b/core/modules/aggregator/aggregator.fetcher.inc similarity index 100% rename from modules/aggregator/aggregator.fetcher.inc rename to core/modules/aggregator/aggregator.fetcher.inc diff --git a/modules/aggregator/aggregator.info b/core/modules/aggregator/aggregator.info similarity index 100% rename from modules/aggregator/aggregator.info rename to core/modules/aggregator/aggregator.info diff --git a/modules/aggregator/aggregator.install b/core/modules/aggregator/aggregator.install similarity index 100% rename from modules/aggregator/aggregator.install rename to core/modules/aggregator/aggregator.install diff --git a/modules/aggregator/aggregator.module b/core/modules/aggregator/aggregator.module similarity index 100% rename from modules/aggregator/aggregator.module rename to core/modules/aggregator/aggregator.module diff --git a/modules/aggregator/aggregator.pages.inc b/core/modules/aggregator/aggregator.pages.inc similarity index 100% rename from modules/aggregator/aggregator.pages.inc rename to core/modules/aggregator/aggregator.pages.inc diff --git a/modules/aggregator/aggregator.parser.inc b/core/modules/aggregator/aggregator.parser.inc similarity index 100% rename from modules/aggregator/aggregator.parser.inc rename to core/modules/aggregator/aggregator.parser.inc diff --git a/modules/aggregator/aggregator.processor.inc b/core/modules/aggregator/aggregator.processor.inc similarity index 100% rename from modules/aggregator/aggregator.processor.inc rename to core/modules/aggregator/aggregator.processor.inc diff --git a/core/modules/aggregator/aggregator.test b/core/modules/aggregator/aggregator.test new file mode 100644 index 0000000..c4f42a4 --- /dev/null +++ b/core/modules/aggregator/aggregator.test @@ -0,0 +1,859 @@ +drupalCreateUser(array('administer news feeds', 'access news feeds', 'create article content')); + $this->drupalLogin($web_user); + } + + /** + * Create an aggregator feed (simulate form submission on admin/config/services/aggregator/add/feed). + * + * @param $feed_url + * If given, feed will be created with this URL, otherwise /rss.xml will be used. + * @return $feed + * Full feed object if possible. + * + * @see getFeedEditArray() + */ + function createFeed($feed_url = NULL) { + $edit = $this->getFeedEditArray($feed_url); + $this->drupalPost('admin/config/services/aggregator/add/feed', $edit, t('Save')); + $this->assertRaw(t('The feed %name has been added.', array('%name' => $edit['title'])), t('The feed !name has been added.', array('!name' => $edit['title']))); + + $feed = db_query("SELECT * FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $edit['title'], ':url' => $edit['url']))->fetch(); + $this->assertTrue(!empty($feed), t('The feed found in database.')); + return $feed; + } + + /** + * Delete an aggregator feed. + * + * @param $feed + * Feed object representing the feed. + */ + function deleteFeed($feed) { + $this->drupalPost('admin/config/services/aggregator/edit/feed/' . $feed->fid, array(), t('Delete')); + $this->assertRaw(t('The feed %title has been deleted.', array('%title' => $feed->title)), t('Feed deleted successfully.')); + } + + /** + * Return a randomly generated feed edit array. + * + * @param $feed_url + * If given, feed will be created with this URL, otherwise /rss.xml will be used. + * @return + * A feed array. + */ + function getFeedEditArray($feed_url = NULL) { + $feed_name = $this->randomName(10); + if (!$feed_url) { + $feed_url = url('rss.xml', array( + 'query' => array('feed' => $feed_name), + 'absolute' => TRUE, + )); + } + $edit = array( + 'title' => $feed_name, + 'url' => $feed_url, + 'refresh' => '900', + ); + return $edit; + } + + /** + * Return the count of the randomly created feed array. + * + * @return + * Number of feed items on default feed created by createFeed(). + */ + function getDefaultFeedItemCount() { + // Our tests are based off of rss.xml, so let's find out how many elements should be related. + $feed_count = db_query_range('SELECT COUNT(*) FROM {node} n WHERE n.promote = 1 AND n.status = 1', 0, variable_get('feed_default_items', 10))->fetchField(); + return $feed_count > 10 ? 10 : $feed_count; + } + + /** + * Update feed items (simulate click to admin/config/services/aggregator/update/$fid). + * + * @param $feed + * Feed object representing the feed. + * @param $expected_count + * Expected number of feed items. + */ + function updateFeedItems(&$feed, $expected_count) { + // First, let's ensure we can get to the rss xml. + $this->drupalGet($feed->url); + $this->assertResponse(200, t('!url is reachable.', array('!url' => $feed->url))); + + // Refresh the feed (simulated link click). + $this->drupalGet('admin/config/services/aggregator/update/' . $feed->fid); + + // Ensure we have the right number of items. + $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid)); + $items = array(); + $feed->items = array(); + foreach ($result as $item) { + $feed->items[] = $item->iid; + } + $feed->item_count = count($feed->items); + $this->assertEqual($expected_count, $feed->item_count, t('Total items in feed equal to the total items in database (!val1 != !val2)', array('!val1' => $expected_count, '!val2' => $feed->item_count))); + } + + /** + * Confirm item removal from a feed. + * + * @param $feed + * Feed object representing the feed. + */ + function removeFeedItems($feed) { + $this->drupalPost('admin/config/services/aggregator/remove/' . $feed->fid, array(), t('Remove items')); + $this->assertRaw(t('The news items from %title have been removed.', array('%title' => $feed->title)), t('Feed items removed.')); + } + + /** + * Add and remove feed items and ensure that the count is zero. + * + * @param $feed + * Feed object representing the feed. + * @param $expected_count + * Expected number of feed items. + */ + function updateAndRemove($feed, $expected_count) { + $this->updateFeedItems($feed, $expected_count); + $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(); + $this->assertTrue($count); + $this->removeFeedItems($feed); + $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(); + $this->assertTrue($count == 0); + } + + /** + * Pull feed categories from aggregator_category_feed table. + * + * @param $feed + * Feed object representing the feed. + */ + function getFeedCategories($feed) { + // add the categories to the feed so we can use them + $result = db_query('SELECT cid FROM {aggregator_category_feed} WHERE fid = :fid', array(':fid' => $feed->fid)); + foreach ($result as $category) { + $feed->categories[] = $category->cid; + } + } + + /** + * Pull categories from aggregator_category table. + */ + function getCategories() { + $categories = array(); + $result = db_query('SELECT * FROM {aggregator_category}'); + foreach ($result as $category) { + $categories[$category->cid] = $category; + } + return $categories; + } + + + /** + * Check if the feed name and url is unique. + * + * @param $feed_name + * String containing the feed name to check. + * @param $feed_url + * String containing the feed url to check. + * @return + * TRUE if feed is unique. + */ + function uniqueFeed($feed_name, $feed_url) { + $result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $feed_name, ':url' => $feed_url))->fetchField(); + return (1 == $result); + } + + /** + * Create a valid OPML file from an array of feeds. + * + * @param $feeds + * An array of feeds. + * @return + * Path to valid OPML file. + */ + function getValidOpml($feeds) { + // Properly escape URLs so that XML parsers don't choke on them. + foreach ($feeds as &$feed) { + $feed['url'] = htmlspecialchars($feed['url']); + } + /** + * Does not have an XML declaration, must pass the parser. + */ + $opml = << + + + + + + + + + + + + + + + + + +EOF; + + $path = 'public://valid-opml.xml'; + return file_unmanaged_save_data($opml, $path); + } + + /** + * Create an invalid OPML file. + * + * @return + * Path to invalid OPML file. + */ + function getInvalidOpml() { + $opml = << + + +EOF; + + $path = 'public://invalid-opml.xml'; + return file_unmanaged_save_data($opml, $path); + } + + /** + * Create a valid but empty OPML file. + * + * @return + * Path to empty OPML file. + */ + function getEmptyOpml() { + $opml = << + + + + + + + +EOF; + + $path = 'public://empty-opml.xml'; + return file_unmanaged_save_data($opml, $path); + } + + function getRSS091Sample() { + return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_rss091.xml'; + } + + function getAtomSample() { + // The content of this sample ATOM feed is based directly off of the + // example provided in RFC 4287. + return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_atom.xml'; + } + + function createSampleNodes() { + $langcode = LANGUAGE_NONE; + // Post 5 articles. + for ($i = 0; $i < 5; $i++) { + $edit = array(); + $edit['title'] = $this->randomName(); + $edit["body[$langcode][0][value]"] = $this->randomName(); + $this->drupalPost('node/add/article', $edit, t('Save')); + } + } +} + +class AddFeedTestCase extends AggregatorTestCase { + public static function getInfo() { + return array( + 'name' => 'Add feed functionality', + 'description' => 'Add feed test.', + 'group' => 'Aggregator' + ); + } + + /** + * Create a feed, ensure that it is unique, check the source, and delete the feed. + */ + function testAddFeed() { + $feed = $this->createFeed(); + + // Check feed data. + $this->assertEqual($this->getUrl(), url('admin/config/services/aggregator/add/feed', array('absolute' => TRUE)), t('Directed to correct url.')); + $this->assertTrue($this->uniqueFeed($feed->title, $feed->url), t('The feed is unique.')); + + // Check feed source. + $this->drupalGet('aggregator/sources/' . $feed->fid); + $this->assertResponse(200, t('Feed source exists.')); + $this->assertText($feed->title, t('Page title')); + $this->drupalGet('aggregator/sources/' . $feed->fid . '/categorize'); + $this->assertResponse(200, t('Feed categorization page exists.')); + + // Delete feed. + $this->deleteFeed($feed); + } +} + +class CategorizeFeedTestCase extends AggregatorTestCase { + public static function getInfo() { + return array( + 'name' => 'Categorize feed functionality', + 'description' => 'Categorize feed test.', + 'group' => 'Aggregator' + ); + } + + /** + * Create a feed and make sure you can add more than one category to it. + */ + function testCategorizeFeed() { + + // Create 2 categories. + $category_1 = array('title' => $this->randomName(10), 'description' => ''); + $this->drupalPost('admin/config/services/aggregator/add/category', $category_1, t('Save')); + $this->assertRaw(t('The category %title has been added.', array('%title' => $category_1['title'])), t('The category %title has been added.', array('%title' => $category_1['title']))); + + $category_2 = array('title' => $this->randomName(10), 'description' => ''); + $this->drupalPost('admin/config/services/aggregator/add/category', $category_2, t('Save')); + $this->assertRaw(t('The category %title has been added.', array('%title' => $category_2['title'])), t('The category %title has been added.', array('%title' => $category_2['title']))); + + // Get categories from database. + $categories = $this->getCategories(); + + // Create a feed and assign 2 categories to it. + $feed = $this->getFeedEditArray(); + $feed['block'] = 5; + foreach ($categories as $cid => $category) { + $feed['category'][$cid] = $cid; + } + + // Use aggregator_save_feed() function to save the feed. + aggregator_save_feed($feed); + $db_feed = db_query("SELECT * FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $feed['title'], ':url' => $feed['url']))->fetch(); + + // Assert the feed has two categories. + $this->getFeedCategories($db_feed); + $this->assertEqual(count($db_feed->categories), 2, t('Feed has 2 categories')); + } +} + +class UpdateFeedTestCase extends AggregatorTestCase { + public static function getInfo() { + return array( + 'name' => 'Update feed functionality', + 'description' => 'Update feed test.', + 'group' => 'Aggregator' + ); + } + + /** + * Create a feed and attempt to update it. + */ + function testUpdateFeed() { + $remamining_fields = array('title', 'url', ''); + foreach ($remamining_fields as $same_field) { + $feed = $this->createFeed(); + + // Get new feed data array and modify newly created feed. + $edit = $this->getFeedEditArray(); + $edit['refresh'] = 1800; // Change refresh value. + if (isset($feed->{$same_field})) { + $edit[$same_field] = $feed->{$same_field}; + } + $this->drupalPost('admin/config/services/aggregator/edit/feed/' . $feed->fid, $edit, t('Save')); + $this->assertRaw(t('The feed %name has been updated.', array('%name' => $edit['title'])), t('The feed %name has been updated.', array('%name' => $edit['title']))); + + // Check feed data. + $this->assertEqual($this->getUrl(), url('admin/config/services/aggregator/', array('absolute' => TRUE))); + $this->assertTrue($this->uniqueFeed($edit['title'], $edit['url']), t('The feed is unique.')); + + // Check feed source. + $this->drupalGet('aggregator/sources/' . $feed->fid); + $this->assertResponse(200, t('Feed source exists.')); + $this->assertText($edit['title'], t('Page title')); + + // Delete feed. + $feed->title = $edit['title']; // Set correct title so deleteFeed() will work. + $this->deleteFeed($feed); + } + } +} + +class RemoveFeedTestCase extends AggregatorTestCase { + public static function getInfo() { + return array( + 'name' => 'Remove feed functionality', + 'description' => 'Remove feed test.', + 'group' => 'Aggregator' + ); + } + + /** + * Remove a feed and ensure that all it services are removed. + */ + function testRemoveFeed() { + $feed = $this->createFeed(); + + // Delete feed. + $this->deleteFeed($feed); + + // Check feed source. + $this->drupalGet('aggregator/sources/' . $feed->fid); + $this->assertResponse(404, t('Deleted feed source does not exists.')); + + // Check database for feed. + $result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $feed->title, ':url' => $feed->url))->fetchField(); + $this->assertFalse($result, t('Feed not found in database')); + } +} + +class UpdateFeedItemTestCase extends AggregatorTestCase { + public static function getInfo() { + return array( + 'name' => 'Update feed item functionality', + 'description' => 'Update feed items from a feed.', + 'group' => 'Aggregator' + ); + } + + /** + * Test running "update items" from the 'admin/config/services/aggregator' page. + */ + function testUpdateFeedItem() { + $this->createSampleNodes(); + + // Create a feed and test updating feed items if possible. + $feed = $this->createFeed(); + if (!empty($feed)) { + $this->updateFeedItems($feed, $this->getDefaultFeedItemCount()); + $this->removeFeedItems($feed); + } + + // Delete feed. + $this->deleteFeed($feed); + + // Test updating feed items without valid timestamp information. + $edit = array( + 'title' => "Feed without publish timestamp", + 'url' => $this->getRSS091Sample(), + ); + + $this->drupalGet($edit['url']); + $this->assertResponse(array(200), t('URL !url is accessible', array('!url' => $edit['url']))); + + $this->drupalPost('admin/config/services/aggregator/add/feed', $edit, t('Save')); + $this->assertRaw(t('The feed %name has been added.', array('%name' => $edit['title'])), t('The feed !name has been added.', array('!name' => $edit['title']))); + + $feed = db_query("SELECT * FROM {aggregator_feed} WHERE url = :url", array(':url' => $edit['url']))->fetchObject(); + $this->drupalGet('admin/config/services/aggregator/update/' . $feed->fid); + + $before = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(); + + // Sleep for 3 second. + sleep(3); + db_update('aggregator_feed') + ->condition('fid', $feed->fid) + ->fields(array( + 'checked' => 0, + 'hash' => '', + 'etag' => '', + 'modified' => 0, + )) + ->execute(); + $this->drupalGet('admin/config/services/aggregator/update/' . $feed->fid); + + $after = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(); + + $this->assertTrue($before === $after, t('Publish timestamp of feed item was not updated (!before === !after)', array('!before' => $before, '!after' => $after))); + } +} + +class RemoveFeedItemTestCase extends AggregatorTestCase { + public static function getInfo() { + return array( + 'name' => 'Remove feed item functionality', + 'description' => 'Remove feed items from a feed.', + 'group' => 'Aggregator' + ); + } + + /** + * Test running "remove items" from the 'admin/config/services/aggregator' page. + */ + function testRemoveFeedItem() { + // Create a bunch of test feeds. + $feed_urls = array(); + // No last-modified, no etag. + $feed_urls[] = url('aggregator/test-feed', array('absolute' => TRUE)); + // Last-modified, but no etag. + $feed_urls[] = url('aggregator/test-feed/1', array('absolute' => TRUE)); + // No Last-modified, but etag. + $feed_urls[] = url('aggregator/test-feed/0/1', array('absolute' => TRUE)); + // Last-modified and etag. + $feed_urls[] = url('aggregator/test-feed/1/1', array('absolute' => TRUE)); + + foreach ($feed_urls as $feed_url) { + $feed = $this->createFeed($feed_url); + // Update and remove items two times in a row to make sure that removal + // resets all 'modified' information (modified, etag, hash) and allows for + // immediate update. + $this->updateAndRemove($feed, 2); + $this->updateAndRemove($feed, 2); + $this->updateAndRemove($feed, 2); + // Delete feed. + $this->deleteFeed($feed); + } + } +} + +class CategorizeFeedItemTestCase extends AggregatorTestCase { + public static function getInfo() { + return array( + 'name' => 'Categorize feed item functionality', + 'description' => 'Test feed item categorization.', + 'group' => 'Aggregator' + ); + } + + /** + * If a feed has a category, make sure that the children inherit that + * categorization. + */ + function testCategorizeFeedItem() { + $this->createSampleNodes(); + + // Simulate form submission on "admin/config/services/aggregator/add/category". + $edit = array('title' => $this->randomName(10), 'description' => ''); + $this->drupalPost('admin/config/services/aggregator/add/category', $edit, t('Save')); + $this->assertRaw(t('The category %title has been added.', array('%title' => $edit['title'])), t('The category %title has been added.', array('%title' => $edit['title']))); + + $category = db_query("SELECT * FROM {aggregator_category} WHERE title = :title", array(':title' => $edit['title']))->fetch(); + $this->assertTrue(!empty($category), t('The category found in database.')); + + $link_path = 'aggregator/categories/' . $category->cid; + $menu_link = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path", array(':link_path' => $link_path))->fetch(); + $this->assertTrue(!empty($menu_link), t('The menu link associated with the category found in database.')); + + $feed = $this->createFeed(); + db_insert('aggregator_category_feed') + ->fields(array( + 'cid' => $category->cid, + 'fid' => $feed->fid, + )) + ->execute(); + $this->updateFeedItems($feed, $this->getDefaultFeedItemCount()); + $this->getFeedCategories($feed); + $this->assertTrue(!empty($feed->categories), t('The category found in the feed.')); + + // For each category of a feed, ensure feed items have that category, too. + if (!empty($feed->categories) && !empty($feed->items)) { + foreach ($feed->categories as $category) { + $categorized_count = db_select('aggregator_category_item') + ->condition('iid', $feed->items, 'IN') + ->countQuery() + ->execute() + ->fetchField(); + + $this->assertEqual($feed->item_count, $categorized_count, t('Total items in feed equal to the total categorized feed items in database')); + } + } + + // Delete feed. + $this->deleteFeed($feed); + } +} + +class ImportOPMLTestCase extends AggregatorTestCase { + public static function getInfo() { + return array( + 'name' => 'Import feeds from OPML functionality', + 'description' => 'Test OPML import.', + 'group' => 'Aggregator', + ); + } + + /** + * Open OPML import form. + */ + function openImportForm() { + db_delete('aggregator_category')->execute(); + + $category = $this->randomName(10); + $cid = db_insert('aggregator_category') + ->fields(array( + 'title' => $category, + 'description' => '', + )) + ->execute(); + + $this->drupalGet('admin/config/services/aggregator/add/opml'); + $this->assertText('A single OPML document may contain a collection of many feeds.', t('Found OPML help text.')); + $this->assertField('files[upload]', t('Found file upload field.')); + $this->assertField('remote', t('Found Remote URL field.')); + $this->assertField('refresh', '', t('Found Refresh field.')); + $this->assertFieldByName("category[$cid]", $cid, t('Found category field.')); + } + + /** + * Submit form filled with invalid fields. + */ + function validateImportFormFields() { + $before = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField(); + + $edit = array(); + $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import')); + $this->assertRaw(t('You must either upload a file or enter a URL.'), t('Error if no fields are filled.')); + + $path = $this->getEmptyOpml(); + $edit = array( + 'files[upload]' => $path, + 'remote' => file_create_url($path), + ); + $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import')); + $this->assertRaw(t('You must either upload a file or enter a URL.'), t('Error if both fields are filled.')); + + $edit = array('remote' => 'invalidUrl://empty'); + $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import')); + $this->assertText(t('This URL is not valid.'), t('Error if the URL is invalid.')); + + $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField(); + $this->assertEqual($before, $after, t('No feeds were added during the three last form submissions.')); + } + + /** + * Submit form with invalid, empty and valid OPML files. + */ + function submitImportForm() { + $before = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField(); + + $form['files[upload]'] = $this->getInvalidOpml(); + $this->drupalPost('admin/config/services/aggregator/add/opml', $form, t('Import')); + $this->assertText(t('No new feed has been added.'), t('Attempting to upload invalid XML.')); + + $edit = array('remote' => file_create_url($this->getEmptyOpml())); + $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import')); + $this->assertText(t('No new feed has been added.'), t('Attempting to load empty OPML from remote URL.')); + + $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField(); + $this->assertEqual($before, $after, t('No feeds were added during the two last form submissions.')); + + db_delete('aggregator_feed')->execute(); + db_delete('aggregator_category')->execute(); + db_delete('aggregator_category_feed')->execute(); + + $category = $this->randomName(10); + db_insert('aggregator_category') + ->fields(array( + 'cid' => 1, + 'title' => $category, + 'description' => '', + )) + ->execute(); + + $feeds[0] = $this->getFeedEditArray(); + $feeds[1] = $this->getFeedEditArray(); + $feeds[2] = $this->getFeedEditArray(); + $edit = array( + 'files[upload]' => $this->getValidOpml($feeds), + 'refresh' => '900', + 'category[1]' => $category, + ); + $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import')); + $this->assertRaw(t('A feed with the URL %url already exists.', array('%url' => $feeds[0]['url'])), t('Verifying that a duplicate URL was identified')); + $this->assertRaw(t('A feed named %title already exists.', array('%title' => $feeds[1]['title'])), t('Verifying that a duplicate title was identified')); + + $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField(); + $this->assertEqual($after, 2, t('Verifying that two distinct feeds were added.')); + + $feeds_from_db = db_query("SELECT f.title, f.url, f.refresh, cf.cid FROM {aggregator_feed} f LEFT JOIN {aggregator_category_feed} cf ON f.fid = cf.fid"); + $refresh = $category = TRUE; + foreach ($feeds_from_db as $feed) { + $title[$feed->url] = $feed->title; + $url[$feed->title] = $feed->url; + $category = $category && $feed->cid == 1; + $refresh = $refresh && $feed->refresh == 900; + } + + $this->assertEqual($title[$feeds[0]['url']], $feeds[0]['title'], t('First feed was added correctly.')); + $this->assertEqual($url[$feeds[1]['title']], $feeds[1]['url'], t('Second feed was added correctly.')); + $this->assertTrue($refresh, t('Refresh times are correct.')); + $this->assertTrue($category, t('Categories are correct.')); + } + + function testOPMLImport() { + $this->openImportForm(); + $this->validateImportFormFields(); + $this->submitImportForm(); + } +} + +class AggregatorCronTestCase extends AggregatorTestCase { + public static function getInfo() { + return array( + 'name' => 'Update on cron functionality', + 'description' => 'Update feeds on cron.', + 'group' => 'Aggregator' + ); + } + + /** + * Add feeds update them on cron. + */ + public function testCron() { + // Create feed and test basic updating on cron. + global $base_url; + $key = variable_get('cron_key', 'drupal'); + $this->createSampleNodes(); + $feed = $this->createFeed(); + $this->drupalGet($base_url . '/core/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key))); + $this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.'); + $this->removeFeedItems($feed); + $this->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.'); + $this->drupalGet($base_url . '/core/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key))); + $this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.'); + + // Test feed locking when queued for update. + $this->removeFeedItems($feed); + db_update('aggregator_feed') + ->condition('fid', $feed->fid) + ->fields(array( + 'queued' => REQUEST_TIME, + )) + ->execute(); + $this->drupalGet($base_url . '/core/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key))); + $this->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.'); + db_update('aggregator_feed') + ->condition('fid', $feed->fid) + ->fields(array( + 'queued' => 0, + )) + ->execute(); + $this->drupalGet($base_url . '/core/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key))); + $this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.'); + } +} + +class AggregatorRenderingTestCase extends AggregatorTestCase { + public static function getInfo() { + return array( + 'name' => 'Checks display of aggregator items', + 'description' => 'Checks display of aggregator items on the page.', + 'group' => 'Aggregator' + ); + } + + /** + * Add a feed block to the page and checks its links. + * + * TODO: Test the category block as well. + */ + public function testBlockLinks() { + // Create feed. + $this->createSampleNodes(); + $feed = $this->createFeed(); + $this->updateFeedItems($feed, $this->getDefaultFeedItemCount()); + + // Place block on page (@see block.test:moveBlockToRegion()) + // Need admin user to be able to access block admin. + $this->admin_user = $this->drupalCreateUser(array( + 'administer blocks', + 'access administration pages', + 'administer news feeds', + 'access news feeds', + )); + $this->drupalLogin($this->admin_user); + + // Prepare to use the block admin form. + $block = array( + 'module' => 'aggregator', + 'delta' => 'feed-' . $feed->fid, + 'title' => $feed->title, + ); + $region = 'footer'; + $edit = array(); + $edit['blocks[' . $block['module'] . '_' . $block['delta'] . '][region]'] = $region; + // Check the feed block is available in the block list form. + $this->drupalGet('admin/structure/block'); + $this->assertFieldByName('blocks[' . $block['module'] . '_' . $block['delta'] . '][region]', '', 'Aggregator feed block is available for positioning.'); + // Position it. + $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); + $this->assertText(t('The block settings have been updated.'), t('Block successfully moved to %region_name region.', array( '%region_name' => $region))); + // Confirm that the block is now being displayed on pages. + $this->drupalGet('node'); + $this->assertText(t($block['title']), t('Feed block is displayed on the page.')); + + // Find the expected read_more link. + $href = 'aggregator/sources/' . $feed->fid; + $links = $this->xpath('//a[@href = :href]', array(':href' => url($href))); + $this->assert(isset($links[0]), t('Link to href %href found.', array('%href' => $href))); + + // Visit that page. + $this->drupalGet($href); + $correct_titles = $this->xpath('//h1[normalize-space(text())=:title]', array(':title' => $feed->title)); + $this->assertFalse(empty($correct_titles), t('Aggregator feed page is available and has the correct title.')); + } +} + +/** + * Tests for feed parsing. + */ +class FeedParserTestCase extends AggregatorTestCase { + public static function getInfo() { + return array( + 'name' => 'Feed parser functionality', + 'description' => 'Test the built-in feed parser with valid feed samples.', + 'group' => 'Aggregator', + ); + } + + function setUp() { + parent::setUp(); + // Do not remove old aggregator items during these tests, since our sample + // feeds have hardcoded dates in them (which may be expired when this test + // is run). + variable_set('aggregator_clear', AGGREGATOR_CLEAR_NEVER); + } + + /** + * Test a feed that uses the RSS 0.91 format. + */ + function testRSS091Sample() { + $feed = $this->createFeed($this->getRSS091Sample()); + aggregator_refresh($feed); + $this->drupalGet('aggregator/sources/' . $feed->fid); + $this->assertResponse(200, t('Feed %name exists.', array('%name' => $feed->title))); + $this->assertText('First example feed item title'); + $this->assertLinkByHref('http://example.com/example-turns-one'); + $this->assertText('First example feed item description.'); + } + + /** + * Test a feed that uses the Atom format. + */ + function testAtomSample() { + $feed = $this->createFeed($this->getAtomSample()); + aggregator_refresh($feed); + $this->drupalGet('aggregator/sources/' . $feed->fid); + $this->assertResponse(200, t('Feed %name exists.', array('%name' => $feed->title))); + $this->assertText('Atom-Powered Robots Run Amok'); + $this->assertLinkByHref('http://example.org/2003/12/13/atom03'); + $this->assertText('Some text.'); + $this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a', db_query('SELECT guid FROM {aggregator_item} WHERE link = :link', array(':link' => 'http://example.org/2003/12/13/atom03'))->fetchField(), 'Atom entry id element is parsed correctly.'); + } +} + diff --git a/modules/aggregator/tests/aggregator_test.info b/core/modules/aggregator/tests/aggregator_test.info similarity index 100% rename from modules/aggregator/tests/aggregator_test.info rename to core/modules/aggregator/tests/aggregator_test.info diff --git a/modules/aggregator/tests/aggregator_test.module b/core/modules/aggregator/tests/aggregator_test.module similarity index 100% rename from modules/aggregator/tests/aggregator_test.module rename to core/modules/aggregator/tests/aggregator_test.module diff --git a/modules/aggregator/tests/aggregator_test_atom.xml b/core/modules/aggregator/tests/aggregator_test_atom.xml similarity index 100% rename from modules/aggregator/tests/aggregator_test_atom.xml rename to core/modules/aggregator/tests/aggregator_test_atom.xml diff --git a/modules/aggregator/tests/aggregator_test_rss091.xml b/core/modules/aggregator/tests/aggregator_test_rss091.xml similarity index 100% rename from modules/aggregator/tests/aggregator_test_rss091.xml rename to core/modules/aggregator/tests/aggregator_test_rss091.xml diff --git a/core/modules/block/block-admin-display-form.tpl.php b/core/modules/block/block-admin-display-form.tpl.php new file mode 100644 index 0000000..bb3d887 --- /dev/null +++ b/core/modules/block/block-admin-display-form.tpl.php @@ -0,0 +1,67 @@ +region_title: Region title for the listed block. + * - $data->block_title: Block title. + * - $data->region_select: Drop-down menu for assigning a region. + * - $data->weight_select: Drop-down menu for setting weights. + * - $data->configure_link: Block configuration link. + * - $data->delete_link: For deleting user added blocks. + * + * @see template_preprocess_block_admin_display_form() + * @see theme_block_admin_display() + */ +?> + $title) { + drupal_add_tabledrag('blocks', 'match', 'sibling', 'block-region-select', 'block-region-' . $region, NULL, FALSE); + drupal_add_tabledrag('blocks', 'order', 'sibling', 'block-weight', 'block-weight-' . $region); + } +?> + + + + + + + + + + + + $title): ?> + + + + + + + $data): ?> + + + + + + + + + + + +
block_title; ?>region_select; ?>weight_select; ?>configure_link; ?>delete_link; ?>
+ + diff --git a/modules/block/block.admin.inc b/core/modules/block/block.admin.inc similarity index 100% rename from modules/block/block.admin.inc rename to core/modules/block/block.admin.inc diff --git a/modules/block/block.api.php b/core/modules/block/block.api.php similarity index 100% rename from modules/block/block.api.php rename to core/modules/block/block.api.php diff --git a/modules/block/block.css b/core/modules/block/block.css similarity index 100% rename from modules/block/block.css rename to core/modules/block/block.css diff --git a/modules/block/block.info b/core/modules/block/block.info similarity index 100% rename from modules/block/block.info rename to core/modules/block/block.info diff --git a/modules/block/block.install b/core/modules/block/block.install similarity index 100% rename from modules/block/block.install rename to core/modules/block/block.install diff --git a/modules/block/block.js b/core/modules/block/block.js similarity index 100% rename from modules/block/block.js rename to core/modules/block/block.js diff --git a/modules/block/block.module b/core/modules/block/block.module similarity index 100% rename from modules/block/block.module rename to core/modules/block/block.module diff --git a/modules/block/block.test b/core/modules/block/block.test similarity index 100% rename from modules/block/block.test rename to core/modules/block/block.test diff --git a/modules/block/block.tpl.php b/core/modules/block/block.tpl.php similarity index 100% rename from modules/block/block.tpl.php rename to core/modules/block/block.tpl.php diff --git a/modules/block/tests/block_test.info b/core/modules/block/tests/block_test.info similarity index 100% rename from modules/block/tests/block_test.info rename to core/modules/block/tests/block_test.info diff --git a/modules/block/tests/block_test.module b/core/modules/block/tests/block_test.module similarity index 100% rename from modules/block/tests/block_test.module rename to core/modules/block/tests/block_test.module diff --git a/modules/blog/blog.info b/core/modules/blog/blog.info similarity index 100% rename from modules/blog/blog.info rename to core/modules/blog/blog.info diff --git a/modules/blog/blog.install b/core/modules/blog/blog.install similarity index 100% rename from modules/blog/blog.install rename to core/modules/blog/blog.install diff --git a/modules/blog/blog.module b/core/modules/blog/blog.module similarity index 100% rename from modules/blog/blog.module rename to core/modules/blog/blog.module diff --git a/modules/blog/blog.pages.inc b/core/modules/blog/blog.pages.inc similarity index 100% rename from modules/blog/blog.pages.inc rename to core/modules/blog/blog.pages.inc diff --git a/modules/blog/blog.test b/core/modules/blog/blog.test similarity index 100% rename from modules/blog/blog.test rename to core/modules/blog/blog.test diff --git a/modules/book/book-all-books-block.tpl.php b/core/modules/book/book-all-books-block.tpl.php similarity index 100% rename from modules/book/book-all-books-block.tpl.php rename to core/modules/book/book-all-books-block.tpl.php diff --git a/modules/book/book-export-html.tpl.php b/core/modules/book/book-export-html.tpl.php similarity index 100% rename from modules/book/book-export-html.tpl.php rename to core/modules/book/book-export-html.tpl.php diff --git a/modules/book/book-navigation.tpl.php b/core/modules/book/book-navigation.tpl.php similarity index 100% rename from modules/book/book-navigation.tpl.php rename to core/modules/book/book-navigation.tpl.php diff --git a/modules/book/book-node-export-html.tpl.php b/core/modules/book/book-node-export-html.tpl.php similarity index 100% rename from modules/book/book-node-export-html.tpl.php rename to core/modules/book/book-node-export-html.tpl.php diff --git a/modules/book/book-rtl.css b/core/modules/book/book-rtl.css similarity index 100% rename from modules/book/book-rtl.css rename to core/modules/book/book-rtl.css diff --git a/modules/book/book.admin.inc b/core/modules/book/book.admin.inc similarity index 100% rename from modules/book/book.admin.inc rename to core/modules/book/book.admin.inc diff --git a/modules/book/book.css b/core/modules/book/book.css similarity index 100% rename from modules/book/book.css rename to core/modules/book/book.css diff --git a/modules/book/book.info b/core/modules/book/book.info similarity index 100% rename from modules/book/book.info rename to core/modules/book/book.info diff --git a/modules/book/book.install b/core/modules/book/book.install similarity index 100% rename from modules/book/book.install rename to core/modules/book/book.install diff --git a/modules/book/book.js b/core/modules/book/book.js similarity index 100% rename from modules/book/book.js rename to core/modules/book/book.js diff --git a/modules/book/book.module b/core/modules/book/book.module similarity index 100% rename from modules/book/book.module rename to core/modules/book/book.module diff --git a/modules/book/book.pages.inc b/core/modules/book/book.pages.inc similarity index 100% rename from modules/book/book.pages.inc rename to core/modules/book/book.pages.inc diff --git a/modules/book/book.test b/core/modules/book/book.test similarity index 100% rename from modules/book/book.test rename to core/modules/book/book.test diff --git a/modules/color/color-rtl.css b/core/modules/color/color-rtl.css similarity index 100% rename from modules/color/color-rtl.css rename to core/modules/color/color-rtl.css diff --git a/modules/color/color.css b/core/modules/color/color.css similarity index 100% rename from modules/color/color.css rename to core/modules/color/color.css diff --git a/modules/color/color.info b/core/modules/color/color.info similarity index 100% rename from modules/color/color.info rename to core/modules/color/color.info diff --git a/modules/color/color.install b/core/modules/color/color.install similarity index 100% rename from modules/color/color.install rename to core/modules/color/color.install diff --git a/modules/color/color.js b/core/modules/color/color.js similarity index 100% rename from modules/color/color.js rename to core/modules/color/color.js diff --git a/modules/color/color.module b/core/modules/color/color.module similarity index 100% rename from modules/color/color.module rename to core/modules/color/color.module diff --git a/modules/color/color.test b/core/modules/color/color.test similarity index 100% rename from modules/color/color.test rename to core/modules/color/color.test diff --git a/modules/color/images/hook-rtl.png b/core/modules/color/images/hook-rtl.png similarity index 100% rename from modules/color/images/hook-rtl.png rename to core/modules/color/images/hook-rtl.png diff --git a/modules/color/images/hook.png b/core/modules/color/images/hook.png similarity index 100% rename from modules/color/images/hook.png rename to core/modules/color/images/hook.png diff --git a/modules/color/images/lock.png b/core/modules/color/images/lock.png similarity index 100% rename from modules/color/images/lock.png rename to core/modules/color/images/lock.png diff --git a/modules/color/preview.html b/core/modules/color/preview.html similarity index 100% rename from modules/color/preview.html rename to core/modules/color/preview.html diff --git a/modules/color/preview.js b/core/modules/color/preview.js similarity index 100% rename from modules/color/preview.js rename to core/modules/color/preview.js diff --git a/modules/comment/comment-node-form.js b/core/modules/comment/comment-node-form.js similarity index 100% rename from modules/comment/comment-node-form.js rename to core/modules/comment/comment-node-form.js diff --git a/modules/comment/comment-rtl.css b/core/modules/comment/comment-rtl.css similarity index 100% rename from modules/comment/comment-rtl.css rename to core/modules/comment/comment-rtl.css diff --git a/modules/comment/comment-wrapper.tpl.php b/core/modules/comment/comment-wrapper.tpl.php similarity index 100% rename from modules/comment/comment-wrapper.tpl.php rename to core/modules/comment/comment-wrapper.tpl.php diff --git a/modules/comment/comment.admin.inc b/core/modules/comment/comment.admin.inc similarity index 100% rename from modules/comment/comment.admin.inc rename to core/modules/comment/comment.admin.inc diff --git a/modules/comment/comment.api.php b/core/modules/comment/comment.api.php similarity index 100% rename from modules/comment/comment.api.php rename to core/modules/comment/comment.api.php diff --git a/modules/comment/comment.css b/core/modules/comment/comment.css similarity index 100% rename from modules/comment/comment.css rename to core/modules/comment/comment.css diff --git a/modules/comment/comment.info b/core/modules/comment/comment.info similarity index 100% rename from modules/comment/comment.info rename to core/modules/comment/comment.info diff --git a/modules/comment/comment.install b/core/modules/comment/comment.install similarity index 100% rename from modules/comment/comment.install rename to core/modules/comment/comment.install diff --git a/modules/comment/comment.module b/core/modules/comment/comment.module similarity index 100% rename from modules/comment/comment.module rename to core/modules/comment/comment.module diff --git a/modules/comment/comment.pages.inc b/core/modules/comment/comment.pages.inc similarity index 100% rename from modules/comment/comment.pages.inc rename to core/modules/comment/comment.pages.inc diff --git a/modules/comment/comment.test b/core/modules/comment/comment.test similarity index 100% rename from modules/comment/comment.test rename to core/modules/comment/comment.test diff --git a/modules/comment/comment.tokens.inc b/core/modules/comment/comment.tokens.inc similarity index 100% rename from modules/comment/comment.tokens.inc rename to core/modules/comment/comment.tokens.inc diff --git a/modules/comment/comment.tpl.php b/core/modules/comment/comment.tpl.php similarity index 100% rename from modules/comment/comment.tpl.php rename to core/modules/comment/comment.tpl.php diff --git a/modules/contact/contact.admin.inc b/core/modules/contact/contact.admin.inc similarity index 100% rename from modules/contact/contact.admin.inc rename to core/modules/contact/contact.admin.inc diff --git a/modules/contact/contact.info b/core/modules/contact/contact.info similarity index 100% rename from modules/contact/contact.info rename to core/modules/contact/contact.info diff --git a/modules/contact/contact.install b/core/modules/contact/contact.install similarity index 100% rename from modules/contact/contact.install rename to core/modules/contact/contact.install diff --git a/modules/contact/contact.module b/core/modules/contact/contact.module similarity index 100% rename from modules/contact/contact.module rename to core/modules/contact/contact.module diff --git a/modules/contact/contact.pages.inc b/core/modules/contact/contact.pages.inc similarity index 100% rename from modules/contact/contact.pages.inc rename to core/modules/contact/contact.pages.inc diff --git a/modules/contact/contact.test b/core/modules/contact/contact.test similarity index 100% rename from modules/contact/contact.test rename to core/modules/contact/contact.test diff --git a/modules/contextual/contextual-rtl.css b/core/modules/contextual/contextual-rtl.css similarity index 100% rename from modules/contextual/contextual-rtl.css rename to core/modules/contextual/contextual-rtl.css diff --git a/modules/contextual/contextual.api.php b/core/modules/contextual/contextual.api.php similarity index 100% rename from modules/contextual/contextual.api.php rename to core/modules/contextual/contextual.api.php diff --git a/modules/contextual/contextual.css b/core/modules/contextual/contextual.css similarity index 100% rename from modules/contextual/contextual.css rename to core/modules/contextual/contextual.css diff --git a/modules/contextual/contextual.info b/core/modules/contextual/contextual.info similarity index 100% rename from modules/contextual/contextual.info rename to core/modules/contextual/contextual.info diff --git a/modules/contextual/contextual.js b/core/modules/contextual/contextual.js similarity index 100% rename from modules/contextual/contextual.js rename to core/modules/contextual/contextual.js diff --git a/modules/contextual/contextual.module b/core/modules/contextual/contextual.module similarity index 100% rename from modules/contextual/contextual.module rename to core/modules/contextual/contextual.module diff --git a/modules/contextual/images/gear-select.png b/core/modules/contextual/images/gear-select.png similarity index 100% rename from modules/contextual/images/gear-select.png rename to core/modules/contextual/images/gear-select.png diff --git a/modules/dashboard/dashboard-rtl.css b/core/modules/dashboard/dashboard-rtl.css similarity index 100% rename from modules/dashboard/dashboard-rtl.css rename to core/modules/dashboard/dashboard-rtl.css diff --git a/modules/dashboard/dashboard.api.php b/core/modules/dashboard/dashboard.api.php similarity index 100% rename from modules/dashboard/dashboard.api.php rename to core/modules/dashboard/dashboard.api.php diff --git a/modules/dashboard/dashboard.css b/core/modules/dashboard/dashboard.css similarity index 100% rename from modules/dashboard/dashboard.css rename to core/modules/dashboard/dashboard.css diff --git a/modules/dashboard/dashboard.info b/core/modules/dashboard/dashboard.info similarity index 100% rename from modules/dashboard/dashboard.info rename to core/modules/dashboard/dashboard.info diff --git a/modules/dashboard/dashboard.install b/core/modules/dashboard/dashboard.install similarity index 100% rename from modules/dashboard/dashboard.install rename to core/modules/dashboard/dashboard.install diff --git a/modules/dashboard/dashboard.js b/core/modules/dashboard/dashboard.js similarity index 100% rename from modules/dashboard/dashboard.js rename to core/modules/dashboard/dashboard.js diff --git a/modules/dashboard/dashboard.module b/core/modules/dashboard/dashboard.module similarity index 100% rename from modules/dashboard/dashboard.module rename to core/modules/dashboard/dashboard.module diff --git a/modules/dashboard/dashboard.test b/core/modules/dashboard/dashboard.test similarity index 100% rename from modules/dashboard/dashboard.test rename to core/modules/dashboard/dashboard.test diff --git a/modules/dblog/dblog-rtl.css b/core/modules/dblog/dblog-rtl.css similarity index 100% rename from modules/dblog/dblog-rtl.css rename to core/modules/dblog/dblog-rtl.css diff --git a/modules/dblog/dblog.admin.inc b/core/modules/dblog/dblog.admin.inc similarity index 100% rename from modules/dblog/dblog.admin.inc rename to core/modules/dblog/dblog.admin.inc diff --git a/modules/dblog/dblog.css b/core/modules/dblog/dblog.css similarity index 100% rename from modules/dblog/dblog.css rename to core/modules/dblog/dblog.css diff --git a/modules/dblog/dblog.info b/core/modules/dblog/dblog.info similarity index 100% rename from modules/dblog/dblog.info rename to core/modules/dblog/dblog.info diff --git a/modules/dblog/dblog.install b/core/modules/dblog/dblog.install similarity index 100% rename from modules/dblog/dblog.install rename to core/modules/dblog/dblog.install diff --git a/modules/dblog/dblog.module b/core/modules/dblog/dblog.module similarity index 100% rename from modules/dblog/dblog.module rename to core/modules/dblog/dblog.module diff --git a/modules/dblog/dblog.test b/core/modules/dblog/dblog.test similarity index 100% rename from modules/dblog/dblog.test rename to core/modules/dblog/dblog.test diff --git a/modules/field/field.api.php b/core/modules/field/field.api.php similarity index 100% rename from modules/field/field.api.php rename to core/modules/field/field.api.php diff --git a/modules/field/field.attach.inc b/core/modules/field/field.attach.inc similarity index 100% rename from modules/field/field.attach.inc rename to core/modules/field/field.attach.inc diff --git a/modules/field/field.crud.inc b/core/modules/field/field.crud.inc similarity index 100% rename from modules/field/field.crud.inc rename to core/modules/field/field.crud.inc diff --git a/modules/field/field.default.inc b/core/modules/field/field.default.inc similarity index 100% rename from modules/field/field.default.inc rename to core/modules/field/field.default.inc diff --git a/modules/field/field.form.inc b/core/modules/field/field.form.inc similarity index 100% rename from modules/field/field.form.inc rename to core/modules/field/field.form.inc diff --git a/modules/field/field.info b/core/modules/field/field.info similarity index 100% rename from modules/field/field.info rename to core/modules/field/field.info diff --git a/modules/field/field.info.inc b/core/modules/field/field.info.inc similarity index 100% rename from modules/field/field.info.inc rename to core/modules/field/field.info.inc diff --git a/modules/field/field.install b/core/modules/field/field.install similarity index 100% rename from modules/field/field.install rename to core/modules/field/field.install diff --git a/core/modules/field/field.module b/core/modules/field/field.module new file mode 100644 index 0000000..69c54ce --- /dev/null +++ b/core/modules/field/field.module @@ -0,0 +1,1206 @@ +$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 + * - ... + * + * 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_flush_caches(). + */ +function field_flush_caches() { + return array('cache_field'); +} + +/** + * 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/handbook/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_theme(). + */ +function field_theme() { + return array( + 'field' => array( + 'render element' => 'element', + ), + 'field_multiple_value_form' => array( + 'render element' => 'element', + ), + ); +} + +/** + * Implements hook_cron(). + * + * Purges some deleted Field API data, if any exists. + */ +function field_cron() { + $limit = variable_get('field_purge_batch_size', 10); + field_purge_batch($limit); +} + +/** + * Implements hook_modules_uninstalled(). + */ +function field_modules_uninstalled($modules) { + module_load_include('inc', 'field', 'field.crud'); + foreach ($modules as $module) { + // TODO D7: field_module_delete is not yet implemented + // field_module_delete($module); + } +} + +/** + * Implements hook_modules_enabled(). + */ +function field_modules_enabled($modules) { + foreach ($modules as $module) { + field_associate_fields($module); + } + field_cache_clear(); +} + +/** + * Implements hook_modules_disabled(). + */ +function field_modules_disabled($modules) { + // Track fields whose field type is being disabled. + db_update('field_config') + ->fields(array('active' => 0)) + ->condition('module', $modules, 'IN') + ->execute(); + + // Track fields whose storage backend is being disabled. + db_update('field_config') + ->fields(array('storage_active' => 0)) + ->condition('storage_module', $modules, 'IN') + ->execute(); + + field_cache_clear(); +} + +/** + * Allows a module to update the database for fields and columns it controls. + * + * @param string $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'); + foreach ($field_types as $name => $field_info) { + watchdog('field', 'Updating field type %type with module %module.', array('%type' => $name, '%module' => $module)); + db_update('field_config') + ->fields(array('module' => $module, 'active' => 1)) + ->condition('type', $name) + ->execute(); + } + // Associate storage backends. + $storage_types = (array) module_invoke($module, 'field_storage_info'); + foreach ($storage_types as $name => $storage_info) { + watchdog('field', 'Updating field storage %type with module %module.', array('%type' => $name, '%module' => $module)); + db_update('field_config') + ->fields(array('storage_module' => $module, 'storage_active' => 1)) + ->condition('storage_type', $name) + ->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'; + function_exists($function); + 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. + * + * For each bundle, settings are provided as a nested array with the following + * structure: + * @code + * array( + * 'view_modes' => array( + * // One sub-array per view mode for the entity type: + * 'full' => array( + * 'custom_display' => Whether the view mode uses custom display + * settings or settings of the 'default' mode, + * ), + * 'teaser' => ... + * ), + * 'extra_fields' => array( + * 'form' => array( + * // One sub-array per pseudo-field in displayed entities: + * 'extra_field_1' => array( + * 'weight' => The weight of the pseudo-field, + * ), + * 'extra_field_2' => ... + * ), + * 'display' => array( + * // One sub-array per pseudo-field in displayed entities: + * 'extra_field_1' => array( + * // One sub-array per view mode for the entity type, including + * // the 'default' mode: + * 'default' => array( + * 'weight' => The weight of the pseudo-field, + * 'visible' => TRUE if the pseudo-field is visible, FALSE if hidden, + * ), + * 'full' => ... + * ), + * 'extra_field_2' => ... + * ), + * ), + * ); + * @endcode + * + * @param $entity_type + * The type of $entity; e.g. 'node' or 'user'. + * @param $bundle + * The bundle name. + * @param $settings + * (optional) The settings to store. + * + * @return + * If no $settings are passed, the current settings are returned. + */ +function field_bundle_settings($entity_type, $bundle, $settings = NULL) { + $stored_settings = variable_get('field_bundle_settings', array()); + + if (isset($settings)) { + $stored_settings[$entity_type][$bundle] = $settings; + + variable_set('field_bundle_settings', $stored_settings); + field_info_cache_clear(); + } + else { + $settings = isset($stored_settings[$entity_type][$bundle]) ? $stored_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 other entities) 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. + $display = _field_info_prepare_instance_display($field, $display); + } + + // 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, + ); + 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. + * @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(); + return (bool) $query + ->fieldCondition($field) + ->range(0, 1) + ->count() + ->execute(); +} + +/** + * Determine whether the user has access to a given field. + * + * @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; 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; +} + +/** + * Helper form element validator: integer. + */ +function _element_validate_integer($element, &$form_state) { + $value = $element['#value']; + if ($value !== '' && (!is_numeric($value) || intval($value) != $value)) { + form_error($element, t('%name must be an integer.', array('%name' => $element['#title']))); + } +} + +/** + * Helper form element validator: integer > 0. + */ +function _element_validate_integer_positive($element, &$form_state) { + $value = $element['#value']; + if ($value !== '' && (!is_numeric($value) || intval($value) != $value || $value <= 0)) { + form_error($element, t('%name must be a positive integer.', array('%name' => $element['#title']))); + } +} + +/** + * Helper form element validator: number. + */ +function _element_validate_number($element, &$form_state) { + $value = $element['#value']; + if ($value != '' && !is_numeric($value)) { + form_error($element, t('%name must be a number.', array('%name' => $element['#title']))); + } +} diff --git a/modules/field/field.multilingual.inc b/core/modules/field/field.multilingual.inc similarity index 100% rename from modules/field/field.multilingual.inc rename to core/modules/field/field.multilingual.inc diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.info b/core/modules/field/modules/field_sql_storage/field_sql_storage.info similarity index 100% rename from modules/field/modules/field_sql_storage/field_sql_storage.info rename to core/modules/field/modules/field_sql_storage/field_sql_storage.info diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.install b/core/modules/field/modules/field_sql_storage/field_sql_storage.install similarity index 100% rename from modules/field/modules/field_sql_storage/field_sql_storage.install rename to core/modules/field/modules/field_sql_storage/field_sql_storage.install diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.module b/core/modules/field/modules/field_sql_storage/field_sql_storage.module similarity index 100% rename from modules/field/modules/field_sql_storage/field_sql_storage.module rename to core/modules/field/modules/field_sql_storage/field_sql_storage.module diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.test b/core/modules/field/modules/field_sql_storage/field_sql_storage.test similarity index 100% rename from modules/field/modules/field_sql_storage/field_sql_storage.test rename to core/modules/field/modules/field_sql_storage/field_sql_storage.test diff --git a/modules/field/modules/list/list.info b/core/modules/field/modules/list/list.info similarity index 100% rename from modules/field/modules/list/list.info rename to core/modules/field/modules/list/list.info diff --git a/modules/field/modules/list/list.install b/core/modules/field/modules/list/list.install similarity index 100% rename from modules/field/modules/list/list.install rename to core/modules/field/modules/list/list.install diff --git a/modules/field/modules/list/list.module b/core/modules/field/modules/list/list.module similarity index 100% rename from modules/field/modules/list/list.module rename to core/modules/field/modules/list/list.module diff --git a/modules/field/modules/list/tests/list.test b/core/modules/field/modules/list/tests/list.test similarity index 100% rename from modules/field/modules/list/tests/list.test rename to core/modules/field/modules/list/tests/list.test diff --git a/modules/field/modules/list/tests/list_test.info b/core/modules/field/modules/list/tests/list_test.info similarity index 100% rename from modules/field/modules/list/tests/list_test.info rename to core/modules/field/modules/list/tests/list_test.info diff --git a/modules/field/modules/list/tests/list_test.module b/core/modules/field/modules/list/tests/list_test.module similarity index 100% rename from modules/field/modules/list/tests/list_test.module rename to core/modules/field/modules/list/tests/list_test.module diff --git a/modules/field/modules/number/number.info b/core/modules/field/modules/number/number.info similarity index 100% rename from modules/field/modules/number/number.info rename to core/modules/field/modules/number/number.info diff --git a/modules/field/modules/number/number.install b/core/modules/field/modules/number/number.install similarity index 100% rename from modules/field/modules/number/number.install rename to core/modules/field/modules/number/number.install diff --git a/modules/field/modules/number/number.module b/core/modules/field/modules/number/number.module similarity index 100% rename from modules/field/modules/number/number.module rename to core/modules/field/modules/number/number.module diff --git a/modules/field/modules/number/number.test b/core/modules/field/modules/number/number.test similarity index 100% rename from modules/field/modules/number/number.test rename to core/modules/field/modules/number/number.test diff --git a/modules/field/modules/options/options.api.php b/core/modules/field/modules/options/options.api.php similarity index 100% rename from modules/field/modules/options/options.api.php rename to core/modules/field/modules/options/options.api.php diff --git a/modules/field/modules/options/options.info b/core/modules/field/modules/options/options.info similarity index 100% rename from modules/field/modules/options/options.info rename to core/modules/field/modules/options/options.info diff --git a/modules/field/modules/options/options.module b/core/modules/field/modules/options/options.module similarity index 100% rename from modules/field/modules/options/options.module rename to core/modules/field/modules/options/options.module diff --git a/modules/field/modules/options/options.test b/core/modules/field/modules/options/options.test similarity index 100% rename from modules/field/modules/options/options.test rename to core/modules/field/modules/options/options.test diff --git a/modules/field/modules/text/text.info b/core/modules/field/modules/text/text.info similarity index 100% rename from modules/field/modules/text/text.info rename to core/modules/field/modules/text/text.info diff --git a/modules/field/modules/text/text.install b/core/modules/field/modules/text/text.install similarity index 100% rename from modules/field/modules/text/text.install rename to core/modules/field/modules/text/text.install diff --git a/modules/field/modules/text/text.js b/core/modules/field/modules/text/text.js similarity index 100% rename from modules/field/modules/text/text.js rename to core/modules/field/modules/text/text.js diff --git a/modules/field/modules/text/text.module b/core/modules/field/modules/text/text.module similarity index 100% rename from modules/field/modules/text/text.module rename to core/modules/field/modules/text/text.module diff --git a/modules/field/modules/text/text.test b/core/modules/field/modules/text/text.test similarity index 100% rename from modules/field/modules/text/text.test rename to core/modules/field/modules/text/text.test diff --git a/core/modules/field/tests/field.test b/core/modules/field/tests/field.test new file mode 100644 index 0000000..f32de13 --- /dev/null +++ b/core/modules/field/tests/field.test @@ -0,0 +1,3247 @@ +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), t('Expected number of values were saved.')); + foreach ($expected_values as $key => $value) { + $this->assertEqual($values[$key][$column], $value, t('Value @value was saved correctly.', array('@value' => $value))); + } + } +} + +class FieldAttachTestCase extends FieldTestCase { + function setUp($modules = array()) { + // 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. + if (!is_array($modules)) { + $modules = func_get_args(); + } + if (!in_array('field_test', $modules)) { + $modules[] = 'field_test'; + } + parent::setUp($modules); + + $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); + $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'], t('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'], t('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', t('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'], t('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'], t('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', t('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], t('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', t('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]], t('Entity %index: expected value was found.', array('%index' => 1))); + $this->assertEqual($entity->{$field_names[1]}[$langcode][0]['additional_key'], 'additional_value', t('Entity %index: extra information was found', array('%index' => 1))); + $this->assert(!isset($entity->{$field_names[2]}), t('Entity %index: field %field_name is not loaded.', array('%index' => 2, '%field_name' => $field_names[2]))); + $this->assert(!isset($entity->{$field_names[3]}), t('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], t('%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']), t('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]), t('Moon is available in the instance array.')); + $this->assertTrue(array_key_exists('mars', $details[FIELD_LOAD_REVISION]), t('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, t('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, t('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}), t('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}), t('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, t('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, t('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}), t('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, t('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}), t('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]), t('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, t('Insert: missing field results in default value saved')); + } + + /** + * 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(), t('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 { + 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() { + $entity_type = 'test_entity'; + $entity_init = field_test_create_stub_entity(); + $langcode = LANGUAGE_NONE; + + // Populate values to be displayed. + $values = $this->_generateTestFieldValues($this->field['cardinality']); + $entity_init->{$this->field_name}[$langcode] = $values; + + // 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); + 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'], "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."); + } + + // 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($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, t('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'), t('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'), t('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'), t('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'), t('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'), t('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'), t('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, t('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'), t('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, t('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'), t('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, t('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'), t('Cached: no cache entry after delete')); + } + + /** + * Test field_attach_validate(). + * + * Verify that field_attach_validate() invokes the correct + * hook_field_validate. + */ + function testFieldAttachValidate() { + $entity_type = 'test_entity'; + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $langcode = LANGUAGE_NONE; + + // Set up values 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; + + 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 value $delta"); + $this->assertEqual(count($errors[$this->field_name][$langcode][$delta]), 1, "Only one error set on value $delta"); + unset($errors[$this->field_name][$langcode][$delta]); + } + else { + $this->assertFalse(isset($errors[$this->field_name][$langcode][$delta]), "No error set on value $delta"); + } + } + $this->assertEqual(count($errors[$this->field_name][$langcode]), 0, 'No extraneous errors set'); + + // Check that cardinality is validated. + $entity->{$this->field_name}[$langcode] = $this->_generateTestFieldValues($this->field['cardinality'] + 1); + try { + field_attach_validate($entity_type, $entity); + } + catch (FieldValidationException $e) { + $errors = $e->errors; + } + $this->assertEqual($errors[$this->field_name][$langcode][0][0]['error'], 'field_cardinality', t('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() { + $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); + + $langcode = LANGUAGE_NONE; + $this->assertEqual($form[$this->field_name][$langcode]['#title'], $this->instance['label'], "Form title is {$this->instance['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', "Form delta $delta widget is textfield"); + } + } + + /** + * Test field_attach_submit(). + */ + function testFieldAttachSubmit() { + $entity_type = 'test_entity'; + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + + // Build the form. + $form = array(); + $form_state = form_state_defaults(); + field_attach_form($entity_type, $entity, $form, $form_state); + + // Simulate incoming values. + $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; + + $langcode = LANGUAGE_NONE; + // 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; + field_attach_submit($entity_type, $entity, $form, $form_state); + + asort($weights); + $expected_values = 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'); + } +} + +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, t("Field type $t_key key $key is $val")); + } + $this->assertEqual($info[$t_key]['module'], 'field_test', t("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, t("Formatter type $f_key key $key is $val")); + } + $this->assertEqual($info[$f_key]['module'], 'field_test', t("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, t("Widget type $w_key key $key is $val")); + } + $this->assertEqual($info[$w_key]['module'], 'field_test', t("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, t("Storage type $s_key key $key is $val")); + } + $this->assertEqual($info[$s_key]['module'], 'field_test', t("Storage type field_test module appears")); + } + + // Verify that no unexpected instances exist. + $core_fields = field_info_fields(); + $instances = field_info_instances('test_entity', 'test_bundle'); + $this->assertTrue(empty($instances), t('With no instances, info bundles is empty.')); + + // Create a field, verify it shows up. + $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, t('One new field exists')); + $this->assertEqual($fields[$field['field_name']]['field_name'], $field['field_name'], t('info fields contains field name')); + $this->assertEqual($fields[$field['field_name']]['type'], $field['type'], t('info fields contains field type')); + $this->assertEqual($fields[$field['field_name']]['module'], 'field_test', t('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, t("Field setting $key has correct default value $val")); + } + $this->assertEqual($fields[$field['field_name']]['cardinality'], 1, t('info fields contains cardinality 1')); + $this->assertEqual($fields[$field['field_name']]['active'], 1, t('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); + + $instances = field_info_instances('test_entity', $instance['bundle']); + $this->assertEqual(count($instances), 1, t('One instance shows up in info when attached to a bundle.')); + $this->assertTrue($instance < $instances[$instance['field_name']], t('Instance appears in info correctly')); + } + + /** + * 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'], t('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'] , t('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'], t('Unavailable widget replaced with default widget.')); + $widget_type = field_info_widget_types($instance['widget']['type']); + $this->assertIdentical($instance['widget']['settings'], $widget_type['settings'] , t('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'], t("Formatter is set for the 'default' view mode")); + $formatter_type = field_info_formatter_types($display['type']); + $this->assertIdentical($display['settings'], $formatter_type['settings'] , t("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'), t('No instances are returned on disabled entity types.')); + } + + /** + * 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'], "field_info_field_settings returns {$type}'s field settings"); + $this->assertIdentical(field_info_instance_settings($type), $data['instance_settings'], "field_info_field_settings returns {$type}'s field instance settings"); + } + + $info = field_test_field_widget_info(); + foreach ($info as $type => $data) { + $this->assertIdentical(field_info_widget_settings($type), $data['settings'], "field_info_widget_settings returns {$type}'s widget settings"); + } + + $info = field_test_field_formatter_info(); + foreach ($info as $type => $data) { + $this->assertIdentical(field_info_formatter_settings($type), $data['settings'], "field_info_formatter_settings returns {$type}'s formatter settings"); + } + } +} + +class FieldFormTestCase extends FieldTestCase { + 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 + } + + 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]", '', t('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', t('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', t('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; + + // Display creation form. + $this->drupalGet('test-entity/add/test-bundle'); + $this->assertNoFieldByName("{$field_name_no_access}[$langcode][0][value]", '', t('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, t('Default value was saved for the field with no edit access.')); + $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 1, t('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, t('New revision has the expected value for the field with no edit access.')); + $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 2, t('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, t('New revision has the expected value for the field with no edit access.')); + $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 2, t('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, t('Entity 1: field_single value appears correctly is the form.')); + $this->assertFieldByName('field_unlimited[und][0][value]', 1, t('Entity 1: field_unlimited value 0 appears correctly is the form.')); + $this->assertFieldByName('entity_2[field_single][und][0][value]', 10, t('Entity 2: field_single value appears correctly is the form.')); + $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 11, t('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')), t('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, t('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')), t('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, t('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, t('Entity 1: field_unlimited value 0 appears correctly is the form.')); + $this->assertFieldByName('field_unlimited[und][1][value]', 2, t('Entity 1: field_unlimited value 1 appears correctly is the form.')); + $this->assertFieldByName('field_unlimited[und][2][value]', '', t('Entity 1: field_unlimited value 2 appears correctly is the form.')); + $this->assertFieldByName('field_unlimited[und][3][value]', '', t('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, t('Entity 2: field_unlimited value 0 appears correctly is the form.')); + $this->assertFieldByName('entity_2[field_unlimited][und][1][value]', 14, t('Entity 2: field_unlimited value 1 appears correctly is the form.')); + $this->assertFieldByName('entity_2[field_unlimited][und][2][value]', 15, t('Entity 2: field_unlimited value 2 appears correctly is the form.')); + $this->assertFieldByName('entity_2[field_unlimited][und][3][value]', '', t('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 { + 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, t('Label was displayed.')); + foreach ($this->values as $delta => $value) { + $this->assertText($setting . '|' . $value['value'], t('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); + $this->drupalSetContent(drupal_render($output)); + $setting = $display['settings']['test_formatter_setting_multiple']; + $this->assertNoText($this->label, t('Label was not displayed.')); + $this->assertText('field_test_field_attach_view_alter', t('Alter fired, display passed.')); + $array = array(); + foreach ($this->values as $delta => $value) { + $array[] = $delta . ':' . $value['value']; + } + $this->assertText($setting . '|' . implode('|', $array), t('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, t('Label was not displayed.')); + $this->assertNoText('field_test_field_attach_view_alter', t('Alter not fired.')); + foreach ($this->values as $delta => $value) { + $this->assertText($setting . '|' . $value['value'] . '|' . ($value['value'] + 1), t('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, t('Label was displayed.')); + foreach ($this->values as $delta => $value) { + $this->assertText($setting . '|' . $value['value'], t('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, t('Label was displayed.')); + foreach ($this->values as $delta => $value) { + $this->assertText($setting . '|' . $value['value'], t('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'], t('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'], t('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), t('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'], t('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'], t('Value @delta was displayed with expected setting.', array('@delta' => $delta))); + } + } +} + +class FieldCrudTestCase extends FieldTestCase { + 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'], t('The field name is properly saved.')); + $this->assertEqual($record['type'], $field_definition['type'], t('The field type is properly saved.')); + + // Ensure that cardinality defaults to 1. + $this->assertEqual($record['cardinality'], 1, t('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'], t('Default field settings have been written.')); + + // Ensure that default storage was set. + $this->assertEqual($record['storage_type'], variable_get('field_storage_default'), t('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, t('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, t('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, t('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, t('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']), t('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']), t('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']), t('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), t('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), t('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']), t('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']), t('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']), t('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']), t('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, t('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, t('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), t('%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, t('The field was was marked active.')); + } +} + +class FieldInstanceCrudTestCase extends FieldTestCase { + protected $field; + + 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, t('Required defaults to false.')); + $this->assertIdentical($record['data']['label'], $this->instance_definition['field_name'], t('Label defaults to field name.')); + $this->assertIdentical($record['data']['description'], '', t('Description defaults to empty string.')); + $this->assertIdentical($record['data']['widget']['type'], $field_type['default_widget'], t('Default widget has been written.')); + $this->assertTrue(isset($record['data']['display']['default']), t('Display for "full" view_mode has been written.')); + $this->assertIdentical($record['data']['display']['default']['type'], $field_type['default_formatter'], t('Default formatter for "full" view_mode has been written.')); + + // Check that default settings are set. + $this->assertIdentical($record['data']['settings'], $field_type['instance_settings'] , t('Default instance settings have been written.')); + $this->assertIdentical($record['data']['widget']['settings'], $widget_type['settings'] , t('Default widget settings have been written.')); + $this->assertIdentical($record['data']['display']['default']['settings'], $formatter_type['settings'], t('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, t('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'], t('"required" change is saved')); + $this->assertEqual($instance['label'], $instance_new['label'], t('"label" change is saved')); + $this->assertEqual($instance['description'], $instance_new['description'], t('"description" change is saved')); + $this->assertEqual($instance['widget']['settings']['test_widget_setting'], $instance_new['widget']['settings']['test_widget_setting'], t('Widget setting change is saved')); + $this->assertEqual($instance['widget']['weight'], $instance_new['widget']['weight'], t('Widget weight change is saved')); + $this->assertEqual($instance['display']['default']['settings']['test_formatter_setting'], $instance_new['display']['default']['settings']['test_formatter_setting'], t('Formatter setting change is saved')); + $this->assertEqual($instance['display']['default']['weight'], $instance_new['display']['default']['weight'], t('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'] , t('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) , t('Widget type change updates default settings.')); + $this->assertEqual($instance['display']['default']['type'], $instance_new['display']['default']['type'] , t('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) , t('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']), t('Display for the new view_mode has been written.')); + $this->assertIdentical($instance_new['display']['teaser']['type'], $field_type['default_formatter'], t('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'] , t('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']), t('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']), t('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), t('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']), t('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']), t('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 { + 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 . '/core/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), t('%language is an enabled language.', array('%language' => $langcode))); + } + } + $this->assertTrue(in_array('xx', $available_languages), t('%language was made available.', array('%language' => 'xx'))); + $this->assertFalse(in_array('en', $available_languages), t('%language was made unavailable.', array('%language' => 'en'))); + + // Test field_available_languages() behavior for untranslatable fields. + $this->field['translatable'] = FALSE; + $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = drupal_strtolower($this->randomName() . '_field_name'); + $available_languages = field_available_languages($this->entity_type, $this->field); + $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === LANGUAGE_NONE, t('For untranslatable fields only LANGUAGE_NONE is available.')); + } + + /** + * Test the multilanguage logic of _field_invoke(). + */ + function testFieldInvoke() { + $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, t('The result for %language is correctly stored.', array('%language' => $langcode))); + } + $this->assertEqual(count($results), count($available_languages), t('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 = mt_rand(2, 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() were correctly + // forwarded to the callback function. + $this->assertEqual($hash, $result, t('The result for entity %id/%language is correctly stored.', array('%id' => $id, '%language' => $langcode))); + } + } + $this->assertEqual(count($results), count($available_languages), t('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]), t('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']), t('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, t('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, t('%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, t('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, t('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, t('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, t('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, t('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; + + 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 $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[] = $stub; + } + return $stubs; + } + + function setUp() { + parent::setUp('field_test'); + + // Clean up data from previous test cases. + $this->fields = array(); + $this->instances = 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; + 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() { + 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 -= 2) { + // 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'); + } + + // hook_field_delete() was called on a pseudo-entity for each entity. Each + // pseudo entity has a $field property that matches the original entity, + // but no others. + $mem = field_test_memorize(); + $this->assertEqual(count($mem['field_test_field_delete']), 10, 'hook_field_delete was called for the right number of entities'); + $stubs = $this->_generateStubEntities($this->entity_type, $this->entities, $field['field_name']); + $count = count($stubs); + foreach ($mem['field_test_field_delete'] as $args) { + $entity = $args[1]; + $this->assertEqual($stubs[$entity->ftid], $entity, 'hook_field_delete() called with the correct stub'); + unset($stubs[$entity->ftid]); + } + $this->assertEqual(count($stubs), $count-10, 'hook_field_delete was called with each entity once'); + + // 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() { + $field = reset($this->fields); + + // Delete the first instance. + $instance = field_info_instance($this->entity_type, $field['field_name'], 'bb_1'); + field_delete_instance($instance); + + // Purge the data. + field_purge_batch(10); + + // 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. + $instance = field_info_instance($this->entity_type, $field['field_name'], 'bb_2'); + field_delete_instance($instance); + + // Purge the data. + field_purge_batch(10); + + // 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; + } + } + } +} diff --git a/modules/field/tests/field_test.entity.inc b/core/modules/field/tests/field_test.entity.inc similarity index 100% rename from modules/field/tests/field_test.entity.inc rename to core/modules/field/tests/field_test.entity.inc diff --git a/modules/field/tests/field_test.field.inc b/core/modules/field/tests/field_test.field.inc similarity index 100% rename from modules/field/tests/field_test.field.inc rename to core/modules/field/tests/field_test.field.inc diff --git a/modules/field/tests/field_test.info b/core/modules/field/tests/field_test.info similarity index 100% rename from modules/field/tests/field_test.info rename to core/modules/field/tests/field_test.info diff --git a/modules/field/tests/field_test.install b/core/modules/field/tests/field_test.install similarity index 100% rename from modules/field/tests/field_test.install rename to core/modules/field/tests/field_test.install diff --git a/core/modules/field/tests/field_test.module b/core/modules/field/tests/field_test.module new file mode 100644 index 0000000..e134c42 --- /dev/null +++ b/core/modules/field/tests/field_test.module @@ -0,0 +1,249 @@ + 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) { + if (isset($items[$id])) { + $result[$id] = array($langcode => hash('sha256', serialize(array($entity_type, $entity, $field['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); +} + +/** + * Memorize calls to hook_field_insert(). + */ +function field_test_field_insert($entity_type, $entity, $field, $instance, $items) { + $args = func_get_args(); + field_test_memorize(__FUNCTION__, $args); +} + +/** + * Memorize calls to hook_field_update(). + */ +function field_test_field_update($entity_type, $entity, $field, $instance, $items) { + $args = func_get_args(); + field_test_memorize(__FUNCTION__, $args); +} + +/** + * Memorize calls to 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_entity_query_alter(). + */ +function field_test_entity_query_alter(&$query) { + if (!empty($query->alterMyExecuteCallbackPlease)) { + $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)), + ), + ); +} + +/** + * Entity label callback. + * + * @param $entity_type + * The entity type. + * @param $entity + * The entity object. + * + * @return + * The label of the entity prefixed with "label callback". + */ +function field_test_entity_label_callback($entity_type, $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'); + } +} diff --git a/modules/field/tests/field_test.storage.inc b/core/modules/field/tests/field_test.storage.inc similarity index 100% rename from modules/field/tests/field_test.storage.inc rename to core/modules/field/tests/field_test.storage.inc diff --git a/modules/field/theme/field-rtl.css b/core/modules/field/theme/field-rtl.css similarity index 100% rename from modules/field/theme/field-rtl.css rename to core/modules/field/theme/field-rtl.css diff --git a/modules/field/theme/field.css b/core/modules/field/theme/field.css similarity index 100% rename from modules/field/theme/field.css rename to core/modules/field/theme/field.css diff --git a/modules/field/theme/field.tpl.php b/core/modules/field/theme/field.tpl.php similarity index 100% rename from modules/field/theme/field.tpl.php rename to core/modules/field/theme/field.tpl.php diff --git a/modules/field_ui/field_ui-rtl.css b/core/modules/field_ui/field_ui-rtl.css similarity index 100% rename from modules/field_ui/field_ui-rtl.css rename to core/modules/field_ui/field_ui-rtl.css diff --git a/core/modules/field_ui/field_ui.admin.inc b/core/modules/field_ui/field_ui.admin.inc new file mode 100644 index 0000000..7208869 --- /dev/null +++ b/core/modules/field_ui/field_ui.admin.inc @@ -0,0 +1,2020 @@ + $type_bundles) { + foreach ($type_bundles as $bundle => $bundle_instances) { + foreach ($bundle_instances as $field_name => $instance) { + $field = field_info_field($field_name); + $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle); + $rows[$field_name]['data'][0] = $field['locked'] ? t('@field_name (Locked)', array('@field_name' => $field_name)) : $field_name; + $rows[$field_name]['data'][1] = $field_types[$field['type']]['label']; + $rows[$field_name]['data'][2][] = $admin_path ? l($bundles[$entity_type][$bundle]['label'], $admin_path . '/fields') : $bundles[$entity_type][$bundle]['label']; + $rows[$field_name]['class'] = $field['locked'] ? array('menu-disabled') : array(''); + } + } + } + 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; +} + +/** + * Helper function to display a message about inactive fields. + */ +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'); + } +} + +/** + * Helper function: determines the rendering order of a tree array. + * + * This is intended as a callback for array_reduce(). + */ +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 #row_callback in field_ui_field_overview_form(), + * and 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 #row_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])) { + $cell = current(element_children($row)); + $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. + $colums_count = 0; + foreach ($table['header'] as $header) { + $colums_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' => $colums_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' => $colums_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']; + } + + foreach (element_children($element) as $cell_key) { + $cell = array('data' => drupal_render($element[$cell_key])); + if (isset($element[$cell_key]['#cell_attributes'])) { + $cell += $element[$cell_key]['#cell_attributes']; + } + $row['data'][] = $cell; + } + $table['rows'][] = $row; + } + } + + return theme('table', $table); +} + +/** + * Menu callback; listing of fields for a bundle. + * + * Allows fields and pseudo-fields to be re-ordered. + */ +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('Name'), + t('Field'), + 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' => '
' . t('Add new field') .'
', + '#suffix' => '
', + ), + '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' => 'textfield', + '#title' => t('New field name'), + '#title_display' => 'invisible', + // This field should stay LTR even for RTL languages. + '#field_prefix' => 'field_', + '#field_suffix' => '‎', + '#attributes' => array('dir'=>'ltr'), + '#size' => 10, + '#description' => t('Field name (a-z, 0-9, _)'), + '#prefix' => '
 
', + ), + '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' => '
 
', + ), + ); + } + + // Additional row: add existing field. + $existing_field_options = field_ui_existing_field_options($entity_type, $bundle); + if ($existing_field_options && $widget_type_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' => '
' . t('Add existing field') .'
', + '#suffix' => '
', + ), + '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_field_options as $field_name => $fields) { + $field = field_info_field($field_name); + $instance = field_info_instance($form['#entity_type'], $field_name, $form['#bundle']); + $js_fields[$field_name] = array('label' => $instance['label'], 'type' => $field['type'], 'widget' => $instance['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; +} + +/** + * Validate handler for the field overview form. + */ +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); +} + +/** + * Helper function for field_ui_field_overview_form_validate. + * + * Validate the 'add new field' row. + */ +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. + if (substr($field_name, 0, 6) != 'field_') { + $field_name = 'field_' . $field_name; + form_set_value($form['fields']['_add_new_field']['field_name'], $field_name, $form_state); + } + + // Invalid field name. + if (!preg_match('!^field_[a-z0-9_]+$!', $field_name)) { + form_set_error('fields][_add_new_field][field_name', t('Add new field: the field name %field_name is invalid. The name must include only lowercase unaccentuated letters, numbers, and underscores.', array('%field_name' => $field_name))); + } + if (strlen($field_name) > 32) { + form_set_error('fields][_add_new_field][field_name', t("Add new field: the field name %field_name is too long. The name is limited to 32 characters, including the 'field_' prefix.", array('%field_name' => $field_name))); + } + + // Field name already exists. We need to check inactive fields as well, so + // we can't use field_info_fields(). + $fields = field_read_fields(array('field_name' => $field_name), array('include_inactive' => TRUE)); + if ($fields) { + form_set_error('fields][_add_new_field][field_name', t('Add new field: the field name %field_name already exists.', array('%field_name' => $field_name))); + } + } + + // 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.')); + } + } + } +} + +/** + * Helper function for field_ui_field_overview_form_validate. + * + * Validate the 'add existing field' row. + */ +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.')); + } + } + } + } +} + +/** + * Submit handler for the field overview form. + */ +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(); + + // 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' => TRUE, + ); + $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()))); + } + } + + // 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']))); + } + 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()))); + } + } + } + + 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.')); + } +} + +/** + * Menu callback; presents field display settings for a given view mode. + */ +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(''), + ); + $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(''); + $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); + $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' => 'core/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 submit handler for multistep buttons on the 'Manage display' screen. + */ +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 ro 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']; +} + +/** + * Submit handler for the 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.')); +} + +/** + * Helper function for field_ui_display_overview_form_submit(). + * + * 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(). + * + * @see 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(). + */ +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']; + } + } +} + +/** + * Return 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; +} + +/** + * Return 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; +} + +/** + * Return 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; +} + +/** + * Return an array of existing field to be added to a bundle. + */ +function field_ui_existing_field_options($entity_type, $bundle) { + $options = 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 that shoud 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'])) { + $text = t('@type: @field (@label)', array( + '@type' => $field_types[$field['type']]['label'], + '@label' => t($instance['label']), '@field' => $instance['field_name'], + )); + $options[$instance['field_name']] = (drupal_strlen($text) > 80 ? truncate_utf8($text, 77) . '...' : $text); + } + } + } + } + } + // Sort the list by field name. + asort($options); + return $options; +} + +/** + * Menu callback; presents the field settings edit page. + */ +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; +} + +/** + * Save a field's settings after editing. + */ +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 (FieldException $e) { + drupal_set_message(t('Attempt to update field %label failed: %message.', array('%label' => $instance['label'], '%message' => $e->getMessage())), 'error'); + // TODO: Where do we go from here? + $form_state['redirect'] = field_ui_next_destination($entity_type, $bundle); + } +} + +/** + * Menu callback; select a widget for the field. + */ +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; +} + +/** + * Submit the change in widget type. + */ +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 (FieldException $e) { + drupal_set_message(t('There was a problem changing the widget for field %label.', array('%label' => $instance['label']))); + } + + $form_state['redirect'] = field_ui_next_destination($entity_type, $bundle); +} + +/** + * Menu callback; present a form for removing a field instance from a bundle. + */ +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; +} + +/** + * 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))); + } + + $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle); + $form_state['redirect'] = field_ui_get_destinations(array($admin_path . '/fields')); +} + +/** + * Menu callback; presents the field instance edit page. + */ +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; +} + +/** + * Build default value fieldset. + */ +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(NULL, NULL, $field, $instance, LANGUAGE_NONE, $items, $element, $form_state, 0); + + return $element; +} + +/** + * Form validation handler for field instance settings form. + */ +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 submit handler for field instance settings form. + */ +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); + field_update_field($field); + + // 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']); +} + +/** + * Helper functions to handle multipage redirects. + */ +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); +} + +/** + * Return 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']); + return field_ui_get_destinations($destinations); + } + $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle); + return $admin_path . '/fields'; +} diff --git a/modules/field_ui/field_ui.api.php b/core/modules/field_ui/field_ui.api.php similarity index 100% rename from modules/field_ui/field_ui.api.php rename to core/modules/field_ui/field_ui.api.php diff --git a/modules/field_ui/field_ui.css b/core/modules/field_ui/field_ui.css similarity index 100% rename from modules/field_ui/field_ui.css rename to core/modules/field_ui/field_ui.css diff --git a/modules/field_ui/field_ui.info b/core/modules/field_ui/field_ui.info similarity index 100% rename from modules/field_ui/field_ui.info rename to core/modules/field_ui/field_ui.info diff --git a/modules/field_ui/field_ui.js b/core/modules/field_ui/field_ui.js similarity index 100% rename from modules/field_ui/field_ui.js rename to core/modules/field_ui/field_ui.js diff --git a/modules/field_ui/field_ui.module b/core/modules/field_ui/field_ui.module similarity index 100% rename from modules/field_ui/field_ui.module rename to core/modules/field_ui/field_ui.module diff --git a/modules/field_ui/field_ui.test b/core/modules/field_ui/field_ui.test similarity index 100% rename from modules/field_ui/field_ui.test rename to core/modules/field_ui/field_ui.test diff --git a/modules/file/file.api.php b/core/modules/file/file.api.php similarity index 100% rename from modules/file/file.api.php rename to core/modules/file/file.api.php diff --git a/modules/file/file.css b/core/modules/file/file.css similarity index 100% rename from modules/file/file.css rename to core/modules/file/file.css diff --git a/modules/file/file.field.inc b/core/modules/file/file.field.inc similarity index 100% rename from modules/file/file.field.inc rename to core/modules/file/file.field.inc diff --git a/modules/file/file.info b/core/modules/file/file.info similarity index 100% rename from modules/file/file.info rename to core/modules/file/file.info diff --git a/modules/file/file.install b/core/modules/file/file.install similarity index 100% rename from modules/file/file.install rename to core/modules/file/file.install diff --git a/modules/file/file.js b/core/modules/file/file.js similarity index 100% rename from modules/file/file.js rename to core/modules/file/file.js diff --git a/core/modules/file/file.module b/core/modules/file/file.module new file mode 100644 index 0000000..e5209a0 --- /dev/null +++ b/core/modules/file/file.module @@ -0,0 +1,984 @@ +' . 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/handbook/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', + 'delivery callback' => 'ajax_deliver', + '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 independently 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, + '#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), + ), + '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); + + // If there are no references, stop processing, to avoid returning headers + // for files controlled by other modules. + if (empty($references)) { + 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 = NULL; + if ($entity) { + // Load all fields 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 $field_item) { + if ($field_item['uri'] == $uri) { + $field = $field_item; + break; + } + } + } + + // Check that $entity and $field 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) || !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, $entity_type, $entity))); + } + // Allow other modules to alter the returned grants/denies. + drupal_alter('file_download_access', $grants, $field, $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); + + 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_replace(NULL, theme('status_messages')); + return array('#type' => 'ajax', '#commands' => $commands); + } + + list($form, $form_state) = 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_replace(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'] .= ''; + } + + $output = theme('status_messages') . drupal_render($form); + $js = drupal_add_js(); + $settings = call_user_func_array('array_merge_recursive', $js['settings']['data']); + + $commands = array(); + $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); +} + +/** + * Determine 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, + ); + + $ajax_settings['progress']['type'] ? $ajax_settings['progress']['type'] == 'bar' : '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')), + ); + } + elseif ($implementation == 'apc') { + $element['APC_UPLOAD_PROGRESS'] = array( + '#type' => 'hidden', + '#value' => $upload_progress_key, + '#attributes' => array('class' => array('file-progress')), + ); + } + + // 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', + '#title' => t('Choose a file'), + '#title_display' => 'invisible', + '#size' => 22, + '#theme_wrappers' => array(), + '#weight' => -10, + ); + + if ($fid && $element['#file']) { + $element['filename'] = array( + '#type' => 'markup', + '#markup' => theme('file_link', array('file' => $element['#file'])) . ' ', + '#weight' => -10, + ); + } + + // 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['#id'] . '-upload' => $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; + + // 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); + } + } + // Load file if the FID has changed to confirm it exists. + if (isset($input['fid']) && $file = file_load($input['fid'])) { + $fid = $file->fid; + } + } + } + + // If there is no input, set the default value. + else { + 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['clicked_button']['#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); + } +} + +/** + * Submit handler for upload and remove buttons of managed_file elements. + */ +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; +} + +/** + * Given a managed_file element, save any files that have been uploaded into it. + * + * @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']; + + // 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); + $icon = theme('file_icon', array('file' => $file, 'icon_directory' => $icon_directory)); + + // 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. + * + * @ingroup themeable + */ +function theme_file_icon($variables) { + $file = $variables['file']; + $icon_directory = $variables['icon_directory']; + + $mime = check_plain($file->filemime); + $icon_url = file_icon_url($file, $icon_directory); + return ''; +} + +/** + * Given a file object, create a URL to a matching icon. + * + * @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; +} + +/** + * Given a file object, create a path to a matching icon. + * + * @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; +} + +/** + * Determine 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. + */ + +/** + * Gets 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. + * + * @return + * An integer value. + */ +function file_get_file_references($file, $field = NULL, $age = FIELD_LOAD_REVISION, $field_type = 'file') { + $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); + $references[$field_name] = $query->execute(); + } + } + + return isset($field) ? $references[$field['field_name']] : $references; +} + +/** + * @} End of "defgroup file-module-api". + */ diff --git a/modules/file/icons/application-octet-stream.png b/core/modules/file/icons/application-octet-stream.png similarity index 100% rename from modules/file/icons/application-octet-stream.png rename to core/modules/file/icons/application-octet-stream.png diff --git a/modules/file/icons/application-pdf.png b/core/modules/file/icons/application-pdf.png similarity index 100% rename from modules/file/icons/application-pdf.png rename to core/modules/file/icons/application-pdf.png diff --git a/modules/file/icons/application-x-executable.png b/core/modules/file/icons/application-x-executable.png similarity index 100% rename from modules/file/icons/application-x-executable.png rename to core/modules/file/icons/application-x-executable.png diff --git a/modules/file/icons/audio-x-generic.png b/core/modules/file/icons/audio-x-generic.png similarity index 100% rename from modules/file/icons/audio-x-generic.png rename to core/modules/file/icons/audio-x-generic.png diff --git a/modules/file/icons/image-x-generic.png b/core/modules/file/icons/image-x-generic.png similarity index 100% rename from modules/file/icons/image-x-generic.png rename to core/modules/file/icons/image-x-generic.png diff --git a/modules/file/icons/package-x-generic.png b/core/modules/file/icons/package-x-generic.png similarity index 100% rename from modules/file/icons/package-x-generic.png rename to core/modules/file/icons/package-x-generic.png diff --git a/modules/file/icons/text-html.png b/core/modules/file/icons/text-html.png similarity index 100% rename from modules/file/icons/text-html.png rename to core/modules/file/icons/text-html.png diff --git a/modules/file/icons/text-plain.png b/core/modules/file/icons/text-plain.png similarity index 100% rename from modules/file/icons/text-plain.png rename to core/modules/file/icons/text-plain.png diff --git a/modules/file/icons/text-x-generic.png b/core/modules/file/icons/text-x-generic.png similarity index 100% rename from modules/file/icons/text-x-generic.png rename to core/modules/file/icons/text-x-generic.png diff --git a/modules/file/icons/text-x-script.png b/core/modules/file/icons/text-x-script.png similarity index 100% rename from modules/file/icons/text-x-script.png rename to core/modules/file/icons/text-x-script.png diff --git a/modules/file/icons/video-x-generic.png b/core/modules/file/icons/video-x-generic.png similarity index 100% rename from modules/file/icons/video-x-generic.png rename to core/modules/file/icons/video-x-generic.png diff --git a/modules/file/icons/x-office-document.png b/core/modules/file/icons/x-office-document.png similarity index 100% rename from modules/file/icons/x-office-document.png rename to core/modules/file/icons/x-office-document.png diff --git a/modules/file/icons/x-office-presentation.png b/core/modules/file/icons/x-office-presentation.png similarity index 100% rename from modules/file/icons/x-office-presentation.png rename to core/modules/file/icons/x-office-presentation.png diff --git a/modules/file/icons/x-office-spreadsheet.png b/core/modules/file/icons/x-office-spreadsheet.png similarity index 100% rename from modules/file/icons/x-office-spreadsheet.png rename to core/modules/file/icons/x-office-spreadsheet.png diff --git a/modules/file/tests/file.test b/core/modules/file/tests/file.test similarity index 100% rename from modules/file/tests/file.test rename to core/modules/file/tests/file.test diff --git a/modules/file/tests/file_module_test.info b/core/modules/file/tests/file_module_test.info similarity index 100% rename from modules/file/tests/file_module_test.info rename to core/modules/file/tests/file_module_test.info diff --git a/modules/file/tests/file_module_test.module b/core/modules/file/tests/file_module_test.module similarity index 100% rename from modules/file/tests/file_module_test.module rename to core/modules/file/tests/file_module_test.module diff --git a/modules/filter/filter.admin.inc b/core/modules/filter/filter.admin.inc similarity index 100% rename from modules/filter/filter.admin.inc rename to core/modules/filter/filter.admin.inc diff --git a/modules/filter/filter.admin.js b/core/modules/filter/filter.admin.js similarity index 100% rename from modules/filter/filter.admin.js rename to core/modules/filter/filter.admin.js diff --git a/modules/filter/filter.api.php b/core/modules/filter/filter.api.php similarity index 100% rename from modules/filter/filter.api.php rename to core/modules/filter/filter.api.php diff --git a/modules/filter/filter.css b/core/modules/filter/filter.css similarity index 100% rename from modules/filter/filter.css rename to core/modules/filter/filter.css diff --git a/modules/filter/filter.info b/core/modules/filter/filter.info similarity index 100% rename from modules/filter/filter.info rename to core/modules/filter/filter.info diff --git a/modules/filter/filter.install b/core/modules/filter/filter.install similarity index 100% rename from modules/filter/filter.install rename to core/modules/filter/filter.install diff --git a/modules/filter/filter.js b/core/modules/filter/filter.js similarity index 100% rename from modules/filter/filter.js rename to core/modules/filter/filter.js diff --git a/modules/filter/filter.module b/core/modules/filter/filter.module similarity index 100% rename from modules/filter/filter.module rename to core/modules/filter/filter.module diff --git a/modules/filter/filter.pages.inc b/core/modules/filter/filter.pages.inc similarity index 100% rename from modules/filter/filter.pages.inc rename to core/modules/filter/filter.pages.inc diff --git a/modules/filter/filter.test b/core/modules/filter/filter.test similarity index 100% rename from modules/filter/filter.test rename to core/modules/filter/filter.test diff --git a/modules/filter/tests/filter.url-input.txt b/core/modules/filter/tests/filter.url-input.txt similarity index 100% rename from modules/filter/tests/filter.url-input.txt rename to core/modules/filter/tests/filter.url-input.txt diff --git a/modules/filter/tests/filter.url-output.txt b/core/modules/filter/tests/filter.url-output.txt similarity index 100% rename from modules/filter/tests/filter.url-output.txt rename to core/modules/filter/tests/filter.url-output.txt diff --git a/modules/forum/forum-icon.tpl.php b/core/modules/forum/forum-icon.tpl.php similarity index 100% rename from modules/forum/forum-icon.tpl.php rename to core/modules/forum/forum-icon.tpl.php diff --git a/modules/forum/forum-list.tpl.php b/core/modules/forum/forum-list.tpl.php similarity index 100% rename from modules/forum/forum-list.tpl.php rename to core/modules/forum/forum-list.tpl.php diff --git a/modules/forum/forum-rtl.css b/core/modules/forum/forum-rtl.css similarity index 100% rename from modules/forum/forum-rtl.css rename to core/modules/forum/forum-rtl.css diff --git a/modules/forum/forum-submitted.tpl.php b/core/modules/forum/forum-submitted.tpl.php similarity index 100% rename from modules/forum/forum-submitted.tpl.php rename to core/modules/forum/forum-submitted.tpl.php diff --git a/modules/forum/forum-topic-list.tpl.php b/core/modules/forum/forum-topic-list.tpl.php similarity index 100% rename from modules/forum/forum-topic-list.tpl.php rename to core/modules/forum/forum-topic-list.tpl.php diff --git a/modules/forum/forum.admin.inc b/core/modules/forum/forum.admin.inc similarity index 100% rename from modules/forum/forum.admin.inc rename to core/modules/forum/forum.admin.inc diff --git a/modules/forum/forum.css b/core/modules/forum/forum.css similarity index 100% rename from modules/forum/forum.css rename to core/modules/forum/forum.css diff --git a/modules/forum/forum.info b/core/modules/forum/forum.info similarity index 100% rename from modules/forum/forum.info rename to core/modules/forum/forum.info diff --git a/modules/forum/forum.install b/core/modules/forum/forum.install similarity index 100% rename from modules/forum/forum.install rename to core/modules/forum/forum.install diff --git a/modules/forum/forum.module b/core/modules/forum/forum.module similarity index 100% rename from modules/forum/forum.module rename to core/modules/forum/forum.module diff --git a/modules/forum/forum.pages.inc b/core/modules/forum/forum.pages.inc similarity index 100% rename from modules/forum/forum.pages.inc rename to core/modules/forum/forum.pages.inc diff --git a/modules/forum/forum.test b/core/modules/forum/forum.test similarity index 100% rename from modules/forum/forum.test rename to core/modules/forum/forum.test diff --git a/modules/forum/forums.tpl.php b/core/modules/forum/forums.tpl.php similarity index 100% rename from modules/forum/forums.tpl.php rename to core/modules/forum/forums.tpl.php diff --git a/modules/help/help-rtl.css b/core/modules/help/help-rtl.css similarity index 100% rename from modules/help/help-rtl.css rename to core/modules/help/help-rtl.css diff --git a/modules/help/help.admin.inc b/core/modules/help/help.admin.inc similarity index 100% rename from modules/help/help.admin.inc rename to core/modules/help/help.admin.inc diff --git a/modules/help/help.api.php b/core/modules/help/help.api.php similarity index 100% rename from modules/help/help.api.php rename to core/modules/help/help.api.php diff --git a/modules/help/help.css b/core/modules/help/help.css similarity index 100% rename from modules/help/help.css rename to core/modules/help/help.css diff --git a/modules/help/help.info b/core/modules/help/help.info similarity index 100% rename from modules/help/help.info rename to core/modules/help/help.info diff --git a/modules/help/help.module b/core/modules/help/help.module similarity index 100% rename from modules/help/help.module rename to core/modules/help/help.module diff --git a/modules/help/help.test b/core/modules/help/help.test similarity index 100% rename from modules/help/help.test rename to core/modules/help/help.test diff --git a/modules/image/image-rtl.css b/core/modules/image/image-rtl.css similarity index 100% rename from modules/image/image-rtl.css rename to core/modules/image/image-rtl.css diff --git a/modules/image/image.admin.css b/core/modules/image/image.admin.css similarity index 100% rename from modules/image/image.admin.css rename to core/modules/image/image.admin.css diff --git a/modules/image/image.admin.inc b/core/modules/image/image.admin.inc similarity index 100% rename from modules/image/image.admin.inc rename to core/modules/image/image.admin.inc diff --git a/modules/image/image.api.php b/core/modules/image/image.api.php similarity index 100% rename from modules/image/image.api.php rename to core/modules/image/image.api.php diff --git a/modules/image/image.css b/core/modules/image/image.css similarity index 100% rename from modules/image/image.css rename to core/modules/image/image.css diff --git a/modules/image/image.effects.inc b/core/modules/image/image.effects.inc similarity index 100% rename from modules/image/image.effects.inc rename to core/modules/image/image.effects.inc diff --git a/modules/image/image.field.inc b/core/modules/image/image.field.inc similarity index 100% rename from modules/image/image.field.inc rename to core/modules/image/image.field.inc diff --git a/modules/image/image.info b/core/modules/image/image.info similarity index 100% rename from modules/image/image.info rename to core/modules/image/image.info diff --git a/modules/image/image.install b/core/modules/image/image.install similarity index 100% rename from modules/image/image.install rename to core/modules/image/image.install diff --git a/core/modules/image/image.module b/core/modules/image/image.module new file mode 100644 index 0000000..cb9d8f0 --- /dev/null +++ b/core/modules/image/image.module @@ -0,0 +1,1182 @@ +' . t('About') . ''; + $output .= '

' . t('The Image module allows you to manipulate images on your website. It exposes a setting for using the Image toolkit, allows you to configure Image styles that can be used for resizing or adjusting images on display, and provides an Image field for attaching images to content. For more information, see the online handbook entry for Image module.', array('@image' => 'http://drupal.org/handbook/modules/image')) . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '
'; + $output .= '
' . t('Manipulating images') . '
'; + $output .= '
' . t('With the Image module you can scale, crop, resize, rotate and desaturate images without affecting the original image using image styles. When you change an image style, the module automatically refreshes all created images. Every image style must have a name, which will be used in the URL of the generated images. There are two common approaches to naming image styles (which you use will depend on how the image style is being applied):',array('@image' => url('admin/config/media/image-styles'))); + $output .= '
  • ' . t('Based on where it will be used: eg. profile-picture') . '
  • '; + $output .= '
  • ' . t('Describing its appearance: eg. square-85x85') . '
'; + $output .= t('After you create an image style, you can add effects: crop, scale, resize, rotate, and desaturate (other contributed modules provide additional effects). For example, by combining effects as crop, scale, and desaturate, you can create square, grayscale thumbnails.') . '
'; + $output .= '
' . t('Attaching images to content as fields') . '
'; + $output .= '
' . t("Image module also allows you to attach images to content as fields. To add an image field to a content type, go to the content type's manage fields page, and add a new field of type Image. Attaching images to content this way allows image styles to be applied and maintained, and also allows you more flexibility when theming.", array('@content-type' => url('admin/structure/types'))) . '
'; + $output .= '
'; + return $output; + case 'admin/config/media/image-styles': + return '

' . t('Image styles commonly provide thumbnail sizes by scaling and cropping images, but can also add various effects before an image is displayed. When an image is displayed with a style, a new file is created and the original image is left unchanged.') . '

'; + case 'admin/config/media/image-styles/edit/%/add/%': + $effect = image_effect_definition_load($arg[7]); + return isset($effect['help']) ? ('

' . $effect['help'] . '

') : NULL; + case 'admin/config/media/image-styles/edit/%/effects/%': + $effect = ($arg[5] == 'add') ? image_effect_definition_load($arg[6]) : image_effect_load($arg[6], $arg[4]); + return isset($effect['help']) ? ('

' . $effect['help'] . '

') : NULL; + } +} + +/** + * Implements hook_menu(). + */ +function image_menu() { + $items = array(); + + // Generate image derivatives of publicly available files. + // If clean URLs are disabled, image derivatives will always be served + // through the menu system. + // If clean URLs are enabled and the image derivative already exists, + // PHP will be bypassed. + $directory_path = file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath(); + $items[$directory_path . '/styles/%image_style'] = array( + 'title' => 'Generate image style', + 'page callback' => 'image_style_deliver', + 'page arguments' => array(count(explode('/', $directory_path)) + 1), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + // Generate and deliver image derivatives of private files. + // These image derivatives are always delivered through the menu system. + $items['system/files/styles/%image_style'] = array( + 'title' => 'Generate image style', + 'page callback' => 'image_style_deliver', + 'page arguments' => array(3), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + $items['admin/config/media/image-styles'] = array( + 'title' => 'Image styles', + 'description' => 'Configure styles that can be used for resizing or adjusting images on display.', + 'page callback' => 'image_style_list', + 'access arguments' => array('administer image styles'), + 'file' => 'image.admin.inc', + ); + $items['admin/config/media/image-styles/list'] = array( + 'title' => 'List', + 'description' => 'List the current image styles on the site.', + 'page callback' => 'image_style_list', + 'access arguments' => array('administer image styles'), + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => 1, + 'file' => 'image.admin.inc', + ); + $items['admin/config/media/image-styles/add'] = array( + 'title' => 'Add style', + 'description' => 'Add a new image style.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('image_style_add_form'), + 'access arguments' => array('administer image styles'), + 'type' => MENU_LOCAL_ACTION, + 'weight' => 2, + 'file' => 'image.admin.inc', + ); + $items['admin/config/media/image-styles/edit/%image_style'] = array( + 'title' => 'Edit style', + 'description' => 'Configure an image style.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('image_style_form', 5), + 'access arguments' => array('administer image styles'), + 'file' => 'image.admin.inc', + ); + $items['admin/config/media/image-styles/delete/%image_style'] = array( + 'title' => 'Delete style', + 'description' => 'Delete an image style.', + 'load arguments' => array(NULL, (string) IMAGE_STORAGE_NORMAL), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('image_style_delete_form', 5), + 'access arguments' => array('administer image styles'), + 'file' => 'image.admin.inc', + ); + $items['admin/config/media/image-styles/revert/%image_style'] = array( + 'title' => 'Revert style', + 'description' => 'Revert an image style.', + 'load arguments' => array(NULL, (string) IMAGE_STORAGE_OVERRIDE), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('image_style_revert_form', 5), + 'access arguments' => array('administer image styles'), + 'file' => 'image.admin.inc', + ); + $items['admin/config/media/image-styles/edit/%image_style/effects/%image_effect'] = array( + 'title' => 'Edit image effect', + 'description' => 'Edit an existing effect within a style.', + 'load arguments' => array(5, (string) IMAGE_STORAGE_EDITABLE), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('image_effect_form', 5, 7), + 'access arguments' => array('administer image styles'), + 'file' => 'image.admin.inc', + ); + $items['admin/config/media/image-styles/edit/%image_style/effects/%image_effect/delete'] = array( + 'title' => 'Delete image effect', + 'description' => 'Delete an existing effect from a style.', + 'load arguments' => array(5, (string) IMAGE_STORAGE_EDITABLE), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('image_effect_delete_form', 5, 7), + 'access arguments' => array('administer image styles'), + 'file' => 'image.admin.inc', + ); + $items['admin/config/media/image-styles/edit/%image_style/add/%image_effect_definition'] = array( + 'title' => 'Add image effect', + 'description' => 'Add a new effect to a style.', + 'load arguments' => array(5), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('image_effect_form', 5, 7), + 'access arguments' => array('administer image styles'), + 'file' => 'image.admin.inc', + ); + + return $items; +} + +/** + * Implements hook_theme(). + */ +function image_theme() { + return array( + // Theme functions in image.module. + 'image_style' => array( + 'variables' => array( + 'style_name' => NULL, + 'path' => NULL, + 'alt' => '', + 'title' => NULL, + 'attributes' => array(), + ), + ), + + // Theme functions in image.admin.inc. + 'image_style_list' => array( + 'variables' => array('styles' => NULL), + ), + 'image_style_effects' => array( + 'render element' => 'form', + ), + 'image_style_preview' => array( + 'variables' => array('style' => NULL), + ), + 'image_anchor' => array( + 'render element' => 'element', + ), + 'image_resize_summary' => array( + 'variables' => array('data' => NULL), + ), + 'image_scale_summary' => array( + 'variables' => array('data' => NULL), + ), + 'image_crop_summary' => array( + 'variables' => array('data' => NULL), + ), + 'image_rotate_summary' => array( + 'variables' => array('data' => NULL), + ), + + // Theme functions in image.field.inc. + 'image_widget' => array( + 'render element' => 'element', + ), + 'image_formatter' => array( + 'variables' => array('item' => NULL, 'path' => NULL, 'image_style' => NULL), + ), + ); +} + +/** + * Implements hook_permission(). + */ +function image_permission() { + return array( + 'administer image styles' => array( + 'title' => t('Administer image styles'), + 'description' => t('Create and modify styles for generating image modifications such as thumbnails.'), + ), + ); +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function image_form_system_file_system_settings_alter(&$form, &$form_state) { + $form['#submit'][] = 'image_system_file_system_settings_submit'; +} + +/** + * Submit handler for the file system settings form. + * + * Adds a menu rebuild after the public file path has been changed, so that the + * menu router item depending on that file path will be regenerated. + */ +function image_system_file_system_settings_submit($form, &$form_state) { + if ($form['file_public_path']['#default_value'] !== $form_state['values']['file_public_path']) { + variable_set('menu_rebuild_needed', TRUE); + } +} + +/** + * Implements hook_flush_caches(). + */ +function image_flush_caches() { + return array('cache_image'); +} + +/** + * Implements hook_file_download(). + * + * Control the access to files underneath the styles directory. + */ +function image_file_download($uri) { + $path = file_uri_target($uri); + + // Private file access for image style derivatives. + if (strpos($path, 'styles/') === 0) { + $args = explode('/', $path); + // Discard the first part of the path (styles). + array_shift($args); + // Get the style name from the second part. + $style_name = array_shift($args); + // Remove the scheme from the path. + array_shift($args); + + // Then the remaining parts are the path to the image. + $original_uri = file_uri_scheme($uri) . '://' . implode('/', $args); + + // Check that the file exists and is an image. + if ($info = image_get_info($uri)) { + // Check the permissions of the original to grant access to this image. + $headers = module_invoke_all('file_download', $original_uri); + if (!in_array(-1, $headers)) { + return array( + // Send headers describing the image's size, and MIME-type... + 'Content-Type' => $info['mime_type'], + 'Content-Length' => $info['file_size'], + // ...and allow the file to be cached for two weeks (matching the + // value we/ use for the mod_expires settings in .htaccess) and + // ensure that caching proxies do not share the image with other + // users. + 'Expires' => gmdate(DATE_RFC1123, REQUEST_TIME + 1209600), + 'Cache-Control' => 'max-age=1209600, private, must-revalidate', + ); + } + } + return -1; + } + + // Private file access for the original files. Note that we only + // check access for non-temporary images, since file.module will + // grant access for all temporary files. + $files = file_load_multiple(array(), array('uri' => $uri)); + if (count($files)) { + $file = reset($files); + if ($file->status) { + return file_file_download($uri, 'image'); + } + } +} + +/** + * Implements hook_file_move(). + */ +function image_file_move($file, $source) { + // Delete any image derivatives at the original image path. + image_path_flush($file->uri); +} + +/** + * Implements hook_file_delete(). + */ +function image_file_delete($file) { + // Delete any image derivatives of this image. + image_path_flush($file->uri); +} + +/** + * Implements hook_image_default_styles(). + */ +function image_image_default_styles() { + $styles = array(); + + $styles['thumbnail'] = array( + 'effects' => array( + array( + 'name' => 'image_scale', + 'data' => array('width' => 100, 'height' => 100, 'upscale' => 1), + 'weight' => 0, + ), + ) + ); + + $styles['medium'] = array( + 'effects' => array( + array( + 'name' => 'image_scale', + 'data' => array('width' => 220, 'height' => 220, 'upscale' => 1), + 'weight' => 0, + ), + ) + ); + + $styles['large'] = array( + 'effects' => array( + array( + 'name' => 'image_scale', + 'data' => array('width' => 480, 'height' => 480, 'upscale' => 0), + 'weight' => 0, + ), + ) + ); + + return $styles; +} + +/** + * Implements hook_image_style_save(). + */ +function image_image_style_save($style) { + if (isset($style['old_name']) && $style['old_name'] != $style['name']) { + $instances = field_read_instances(); + // Loop through all fields searching for image fields. + foreach ($instances as $instance) { + if ($instance['widget']['module'] == 'image') { + $instance_changed = FALSE; + foreach ($instance['display'] as $view_mode => $display) { + // Check if the formatter involves an image style. + if ($display['type'] == 'image' && $display['settings']['image_style'] == $style['old_name']) { + // Update display information for any instance using the image + // style that was just deleted. + $instance['display'][$view_mode]['settings']['image_style'] = $style['name']; + $instance_changed = TRUE; + } + } + if ($instance['widget']['settings']['preview_image_style'] == $style['old_name']) { + $instance['widget']['settings']['preview_image_style'] = $style['name']; + $instance_changed = TRUE; + } + if ($instance_changed) { + field_update_instance($instance); + } + } + } + } +} + +/** + * Implements hook_image_style_delete(). + */ +function image_image_style_delete($style) { + image_image_style_save($style); +} + +/** + * Implements hook_field_delete_field(). + */ +function image_field_delete_field($field) { + if ($field['type'] != 'image') { + return; + } + + // The value of a managed_file element can be an array if #extended == TRUE. + $fid = (is_array($field['settings']['default_image']) ? $field['settings']['default_image']['fid'] : $field['settings']['default_image']); + if ($fid && ($file = file_load($fid))) { + file_usage_delete($file, 'image', 'default_image', $field['id']); + } +} + +/** + * Implements hook_field_update_field(). + */ +function image_field_update_field($field, $prior_field, $has_data) { + if ($field['type'] != 'image') { + return; + } + + // The value of a managed_file element can be an array if #extended == TRUE. + $fid_new = (is_array($field['settings']['default_image']) ? $field['settings']['default_image']['fid'] : $field['settings']['default_image']); + $fid_old = (is_array($prior_field['settings']['default_image']) ? $prior_field['settings']['default_image']['fid'] : $prior_field['settings']['default_image']); + + $file_new = $fid_new ? file_load($fid_new) : FALSE; + + if ($fid_new != $fid_old) { + + // Is there a new file? + if ($file_new) { + $file_new->status = FILE_STATUS_PERMANENT; + file_save($file_new); + file_usage_add($file_new, 'image', 'default_image', $field['id']); + } + + // Is there an old file? + if ($fid_old && ($file_old = file_load($fid_old))) { + file_usage_delete($file_old, 'image', 'default_image', $field['id']); + } + } + + // If the upload destination changed, then move the file. + if ($file_new && (file_uri_scheme($file_new->uri) != $field['settings']['uri_scheme'])) { + $directory = $field['settings']['uri_scheme'] . '://default_images/'; + file_prepare_directory($directory, FILE_CREATE_DIRECTORY); + file_move($file_new, $directory . $file_new->filename); + } +} + +/** + * Clear cached versions of a specific file in all styles. + * + * @param $path + * The Drupal file path to the original image. + */ +function image_path_flush($path) { + $styles = image_styles(); + foreach ($styles as $style) { + $image_path = image_style_path($style['name'], $path); + if (file_exists($image_path)) { + file_unmanaged_delete($image_path); + } + } +} + +/** + * Get an array of all styles and their settings. + * + * @return + * An array of styles keyed by the image style ID (isid). + * @see image_style_load() + */ +function image_styles() { + $styles = &drupal_static(__FUNCTION__); + + // Grab from cache or build the array. + if (!isset($styles)) { + if ($cache = cache_get('image_styles', 'cache')) { + $styles = $cache->data; + } + else { + $styles = array(); + + // Select the module-defined styles. + foreach (module_implements('image_default_styles') as $module) { + $module_styles = module_invoke($module, 'image_default_styles'); + foreach ($module_styles as $style_name => $style) { + $style['name'] = $style_name; + $style['module'] = $module; + $style['storage'] = IMAGE_STORAGE_DEFAULT; + foreach ($style['effects'] as $key => $effect) { + $definition = image_effect_definition_load($effect['name']); + $effect = array_merge($definition, $effect); + $style['effects'][$key] = $effect; + } + $styles[$style_name] = $style; + } + } + + // Select all the user-defined styles. + $user_styles = db_select('image_styles', NULL, array('fetch' => PDO::FETCH_ASSOC)) + ->fields('image_styles') + ->orderBy('name') + ->execute() + ->fetchAllAssoc('name', PDO::FETCH_ASSOC); + + // Allow the user styles to override the module styles. + foreach ($user_styles as $style_name => $style) { + $style['module'] = NULL; + $style['storage'] = IMAGE_STORAGE_NORMAL; + $style['effects'] = image_style_effects($style); + if (isset($styles[$style_name]['module'])) { + $style['module'] = $styles[$style_name]['module']; + $style['storage'] = IMAGE_STORAGE_OVERRIDE; + } + $styles[$style_name] = $style; + } + + drupal_alter('image_styles', $styles); + cache_set('image_styles', $styles); + } + } + + return $styles; +} + +/** + * Load a style by style name or ID. May be used as a loader for menu items. + * + * @param $name + * The name of the style. + * @param $isid + * Optional. The numeric id of a style if the name is not known. + * @param $include + * If set, this loader will restrict to a specific type of image style, may be + * one of the defined Image style storage constants. + * @return + * An image style array containing the following keys: + * - "isid": The unique image style ID. + * - "name": The unique image style name. + * - "effects": An array of image effects within this image style. + * If the image style name or ID is not valid, an empty array is returned. + * @see image_effect_load() + */ +function image_style_load($name = NULL, $isid = NULL, $include = NULL) { + $styles = image_styles(); + + // If retrieving by name. + if (isset($name) && isset($styles[$name])) { + $style = $styles[$name]; + } + + // If retrieving by image style id. + if (!isset($name) && isset($isid)) { + foreach ($styles as $name => $database_style) { + if (isset($database_style['isid']) && $database_style['isid'] == $isid) { + $style = $database_style; + break; + } + } + } + + // Restrict to the specific type of flag. This bitwise operation basically + // states "if the storage is X, then allow". + if (isset($style) && (!isset($include) || ($style['storage'] & (int) $include))) { + return $style; + } + + // Otherwise the style was not found. + return FALSE; +} + +/** + * Save an image style. + * + * @param style + * An image style array. + * @return + * An image style array. In the case of a new style, 'isid' will be populated. + */ +function image_style_save($style) { + if (isset($style['isid']) && is_numeric($style['isid'])) { + // Load the existing style to make sure we account for renamed styles. + $old_style = image_style_load(NULL, $style['isid']); + image_style_flush($old_style); + drupal_write_record('image_styles', $style, 'isid'); + if ($old_style['name'] != $style['name']) { + $style['old_name'] = $old_style['name']; + } + } + else { + drupal_write_record('image_styles', $style); + $style['is_new'] = TRUE; + } + + // Let other modules update as necessary on save. + module_invoke_all('image_style_save', $style); + + // Clear all caches and flush. + image_style_flush($style); + + return $style; +} + +/** + * Delete an image style. + * + * @param $style + * An image style array. + * @param $replacement_style_name + * (optional) When deleting a style, specify a replacement style name so + * that existing settings (if any) may be converted to a new style. + * @return + * TRUE on success. + */ +function image_style_delete($style, $replacement_style_name = '') { + image_style_flush($style); + + db_delete('image_effects')->condition('isid', $style['isid'])->execute(); + db_delete('image_styles')->condition('isid', $style['isid'])->execute(); + + // Let other modules update as necessary on save. + $style['old_name'] = $style['name']; + $style['name'] = $replacement_style_name; + module_invoke_all('image_style_delete', $style); + + return TRUE; +} + +/** + * Load all the effects for an image style. + * + * @param $style + * An image style array. + * @return + * An array of image effects associated with specified image style in the + * format array('isid' => array()), or an empty array if the specified style + * has no effects. + */ +function image_style_effects($style) { + $effects = image_effects(); + $style_effects = array(); + foreach ($effects as $effect) { + if ($style['isid'] == $effect['isid']) { + $style_effects[$effect['ieid']] = $effect; + } + } + + return $style_effects; +} + +/** + * Get an array of image styles suitable for using as select list options. + * + * @param $include_empty + * If TRUE a option will be inserted in the options array. + * @return + * Array of image styles both key and value are set to style name. + */ +function image_style_options($include_empty = TRUE) { + $styles = image_styles(); + $options = array(); + if ($include_empty && !empty($styles)) { + $options[''] = t(''); + } + $options = array_merge($options, drupal_map_assoc(array_keys($styles))); + if (empty($options)) { + $options[''] = t('No defined styles'); + } + return $options; +} + +/** + * Menu callback; Given a style and image path, generate a derivative. + * + * After generating an image, transfer it to the requesting agent. + * + * @param $style + * The image style + */ +function image_style_deliver($style, $scheme) { + // Check that the style is defined and the scheme is valid. + if (!$style || !file_stream_wrapper_valid_scheme($scheme)) { + drupal_exit(); + } + + $args = func_get_args(); + array_shift($args); + array_shift($args); + $target = implode('/', $args); + + $image_uri = $scheme . '://' . $target; + $derivative_uri = image_style_path($style['name'], $image_uri); + + // If using the private scheme, let other modules provide headers and + // control access to the file. + if ($scheme == 'private') { + if (file_exists($derivative_uri)) { + file_download($scheme, file_uri_target($derivative_uri)); + } + else { + $headers = module_invoke_all('file_download', $image_uri); + if (in_array(-1, $headers) || empty($headers)) { + return drupal_access_denied(); + } + if (count($headers)) { + foreach ($headers as $name => $value) { + drupal_add_http_header($name, $value); + } + } + } + } + + // Don't start generating the image if the derivative already exists or if + // generation is in progress in another thread. + $lock_name = 'image_style_deliver:' . $style['name'] . ':' . drupal_hash_base64($image_uri); + if (!file_exists($derivative_uri)) { + $lock_acquired = lock_acquire($lock_name); + if (!$lock_acquired) { + // Tell client to retry again in 3 seconds. Currently no browsers are known + // to support Retry-After. + drupal_add_http_header('Status', '503 Service Unavailable'); + drupal_add_http_header('Retry-After', 3); + print t('Image generation in progress. Try again shortly.'); + drupal_exit(); + } + } + + // Try to generate the image, unless another thread just did it while we were + // acquiring the lock. + $success = file_exists($derivative_uri) || image_style_create_derivative($style, $image_uri, $derivative_uri); + + if (!empty($lock_acquired)) { + lock_release($lock_name); + } + + if ($success) { + $image = image_load($derivative_uri); + file_transfer($image->source, array('Content-Type' => $image->info['mime_type'], 'Content-Length' => $image->info['file_size'])); + } + else { + watchdog('image', 'Unable to generate the derived image located at %path.', array('%path' => $derivative_uri)); + drupal_add_http_header('Status', '500 Internal Server Error'); + print t('Error generating image.'); + drupal_exit(); + } +} + +/** + * Creates a new image derivative based on an image style. + * + * Generates an image derivative by creating the destination folder (if it does + * not already exist), applying all image effects defined in $style['effects'], + * and saving a cached version of the resulting image. + * + * @param $style + * An image style array. + * @param $source + * Path of the source file. + * @param $destination + * Path or URI of the destination file. + * + * @return + * TRUE if an image derivative was generated, or FALSE if the image derivative + * could not be generated. + * + * @see image_style_load() + */ +function image_style_create_derivative($style, $source, $destination) { + // Get the folder for the final location of this style. + $directory = drupal_dirname($destination); + + // Build the destination folder tree if it doesn't already exist. + if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) { + watchdog('image', 'Failed to create style directory: %directory', array('%directory' => $directory), LOG_ERR); + return FALSE; + } + + if (!$image = image_load($source)) { + return FALSE; + } + + foreach ($style['effects'] as $effect) { + image_effect_apply($image, $effect); + } + + if (!image_save($image, $destination)) { + if (file_exists($destination)) { + watchdog('image', 'Cached image file %destination already exists. There may be an issue with your rewrite configuration.', array('%destination' => $destination), LOG_ERR); + } + return FALSE; + } + + return TRUE; +} + +/** + * Flush cached media for a style. + * + * @param $style + * An image style array. + */ +function image_style_flush($style) { + $style_directory = drupal_realpath(file_default_scheme() . '://styles/' . $style['name']); + if (is_dir($style_directory)) { + file_unmanaged_delete_recursive($style_directory); + } + + // Let other modules update as necessary on flush. + module_invoke_all('image_style_flush', $style); + + // Clear image style and effect caches. + cache_clear_all('image_styles', 'cache'); + cache_clear_all('image_effects:', 'cache', TRUE); + drupal_static_reset('image_styles'); + drupal_static_reset('image_effects'); + + // Clear field caches so that formatters may be added for this style. + field_info_cache_clear(); + drupal_theme_rebuild(); + + // Clear page caches when flushing. + if (module_exists('block')) { + cache_clear_all('*', 'cache_block', TRUE); + } + cache_clear_all('*', 'cache_page', TRUE); +} + +/** + * Return the URL for an image derivative given a style and image path. + * + * @param $style_name + * The name of the style to be used with this image. + * @param $path + * The path to the image. + * @return + * The absolute URL where a style image can be downloaded, suitable for use + * in an tag. Requesting the URL will cause the image to be created. + * @see image_style_deliver() + */ +function image_style_url($style_name, $path) { + $uri = image_style_path($style_name, $path); + + // If not using clean URLs, the image derivative callback is only available + // with the query string. If the file does not exist, use url() to ensure + // that it is included. Once the file exists it's fine to fall back to the + // actual file path, this avoids bootstrapping PHP once the files are built. + if (!variable_get('clean_url') && file_uri_scheme($uri) == 'public' && !file_exists($uri)) { + $directory_path = file_stream_wrapper_get_instance_by_uri($uri)->getDirectoryPath(); + return url($directory_path . '/' . file_uri_target($uri), array('absolute' => TRUE)); + } + + return file_create_url($uri); +} + +/** + * Return the URI of an image when using a style. + * + * The path returned by this function may not exist. The default generation + * method only creates images when they are requested by a user's browser. + * + * @param $style_name + * The name of the style to be used with this image. + * @param $uri + * The URI or path to the image. + * @return + * The URI to an image style image. + * @see image_style_url() + */ +function image_style_path($style_name, $uri) { + $scheme = file_uri_scheme($uri); + if ($scheme) { + $path = file_uri_target($uri); + } + else { + $path = $uri; + $scheme = file_default_scheme(); + } + return $scheme . '://styles/' . $style_name . '/' . $scheme . '/' . $path; +} + +/** + * Save a default image style to the database. + * + * @param style + * An image style array provided by a module. + * @return + * An image style array. The returned style array will include the new 'isid' + * assigned to the style. + */ +function image_default_style_save($style) { + $style = image_style_save($style); + $effects = array(); + foreach ($style['effects'] as $effect) { + $effect['isid'] = $style['isid']; + $effect = image_effect_save($effect); + $effects[$effect['ieid']] = $effect; + } + $style['effects'] = $effects; + return $style; +} + +/** + * Revert the changes made by users to a default image style. + * + * @param style + * An image style array. + * @return + * Boolean TRUE if the operation succeeded. + */ +function image_default_style_revert($style) { + image_style_flush($style); + + db_delete('image_effects')->condition('isid', $style['isid'])->execute(); + db_delete('image_styles')->condition('isid', $style['isid'])->execute(); + + return TRUE; +} + +/** + * Pull in image effects exposed by modules implementing hook_image_effect_info(). + * + * @return + * An array of image effects to be used when transforming images. + * @see hook_image_effect_info() + * @see image_effect_definition_load() + */ +function image_effect_definitions() { + global $language; + + // hook_image_effect_info() includes translated strings, so each language is + // cached separately. + $langcode = $language->language; + + $effects = &drupal_static(__FUNCTION__); + + if (!isset($effects)) { + if ($cache = cache_get("image_effects:$langcode") && !empty($cache->data)) { + $effects = $cache->data; + } + else { + $effects = array(); + include_once DRUPAL_ROOT . '/core/modules/image/image.effects.inc'; + foreach (module_implements('image_effect_info') as $module) { + foreach (module_invoke($module, 'image_effect_info') as $name => $effect) { + // Ensure the current toolkit supports the effect. + $effect['module'] = $module; + $effect['name'] = $name; + $effect['data'] = isset($effect['data']) ? $effect['data'] : array(); + $effects[$name] = $effect; + } + } + uasort($effects, '_image_effect_definitions_sort'); + drupal_alter('image_effect_info', $effects); + cache_set("image_effects:$langcode", $effects); + } + } + + return $effects; +} + +/** + * Load the definition for an image effect. + * + * The effect definition is a set of core properties for an image effect, not + * containing any user-settings. The definition defines various functions to + * call when configuring or executing an image effect. This loader is mostly for + * internal use within image.module. Use image_effect_load() or + * image_style_load() to get image effects that contain configuration. + * + * @param $effect + * The name of the effect definition to load. + * @param $style + * An image style array to which this effect will be added. + * @return + * An array containing the image effect definition with the following keys: + * - "effect": The unique name for the effect being performed. Usually prefixed + * with the name of the module providing the effect. + * - "module": The module providing the effect. + * - "help": A description of the effect. + * - "function": The name of the function that will execute the effect. + * - "form": (optional) The name of a function to configure the effect. + * - "summary": (optional) The name of a theme function that will display a + * one-line summary of the effect. Does not include the "theme_" prefix. + */ +function image_effect_definition_load($effect, $style_name = NULL) { + $definitions = image_effect_definitions(); + + // If a style is specified, do not allow loading of default style + // effects. + if (isset($style_name)) { + $style = image_style_load($style_name, NULL); + if ($style['storage'] == IMAGE_STORAGE_DEFAULT) { + return FALSE; + } + } + + return isset($definitions[$effect]) ? $definitions[$effect] : FALSE; +} + +/** + * Load all image effects from the database. + * + * @return + * An array of all image effects. + * @see image_effect_load() + */ +function image_effects() { + $effects = &drupal_static(__FUNCTION__); + + if (!isset($effects)) { + $effects = array(); + + // Add database image effects. + $result = db_select('image_effects', NULL, array('fetch' => PDO::FETCH_ASSOC)) + ->fields('image_effects') + ->orderBy('image_effects.weight', 'ASC') + ->execute(); + foreach ($result as $effect) { + $effect['data'] = unserialize($effect['data']); + $definition = image_effect_definition_load($effect['name']); + // Do not load image effects whose definition cannot be found. + if ($definition) { + $effect = array_merge($definition, $effect); + $effects[$effect['ieid']] = $effect; + } + } + } + + return $effects; +} + +/** + * Load a single image effect. + * + * @param $ieid + * The image effect ID. + * @param $style_name + * The image style name. + * @param $include + * If set, this loader will restrict to a specific type of image style, may be + * one of the defined Image style storage constants. + * @return + * An image effect array, consisting of the following keys: + * - "ieid": The unique image effect ID. + * - "isid": The unique image style ID that contains this image effect. + * - "weight": The weight of this image effect within the image style. + * - "name": The name of the effect definition that powers this image effect. + * - "data": An array of configuration options for this image effect. + * Besides these keys, the entirety of the image definition is merged into + * the image effect array. Returns FALSE if the specified effect cannot be + * found. + * @see image_style_load() + * @see image_effect_definition_load() + */ +function image_effect_load($ieid, $style_name, $include = NULL) { + if (($style = image_style_load($style_name, NULL, $include)) && isset($style['effects'][$ieid])) { + return $style['effects'][$ieid]; + } + return FALSE; +} + +/** + * Save an image effect. + * + * @param $effect + * An image effect array. + * @return + * An image effect array. In the case of a new effect, 'ieid' will be set. + */ +function image_effect_save($effect) { + if (!empty($effect['ieid'])) { + drupal_write_record('image_effects', $effect, 'ieid'); + } + else { + drupal_write_record('image_effects', $effect); + } + $style = image_style_load(NULL, $effect['isid']); + image_style_flush($style); + return $effect; +} + +/** + * Delete an image effect. + * + * @param $effect + * An image effect array. + */ +function image_effect_delete($effect) { + db_delete('image_effects')->condition('ieid', $effect['ieid'])->execute(); + $style = image_style_load(NULL, $effect['isid']); + image_style_flush($style); +} + +/** + * Given an image object and effect, perform the effect on the file. + * + * @param $image + * An image object returned by image_load(). + * @param $effect + * An image effect array. + * @return + * TRUE on success. FALSE if unable to perform the image effect on the image. + */ +function image_effect_apply($image, $effect) { + module_load_include('inc', 'image', 'image.effects'); + $function = $effect['effect callback']; + if (function_exists($function)) { + return $function($image, $effect['data']); + } + return FALSE; +} + +/** + * Returns HTML for an image using a specific image style. + * + * @param $variables + * An associative array containing: + * - style_name: The name of the style to be used to alter the original image. + * - path: The path of the image file relative to the Drupal files directory. + * This function does not work with images outside the files directory nor + * with remotely hosted images. + * - alt: The alternative text for text-based browsers. + * - title: The title text is displayed when the image is hovered in some + * popular browsers. + * - attributes: Associative array of attributes to be placed in the img tag. + * + * @ingroup themeable + */ +function theme_image_style($variables) { + $variables['path'] = image_style_url($variables['style_name'], $variables['path']); + return theme('image', $variables); +} + +/** + * Accept a keyword (center, top, left, etc) and return it as a pixel offset. + * + * @param $value + * @param $current_pixels + * @param $new_pixels + */ +function image_filter_keyword($value, $current_pixels, $new_pixels) { + switch ($value) { + case 'top': + case 'left': + return 0; + + case 'bottom': + case 'right': + return $current_pixels - $new_pixels; + + case 'center': + return $current_pixels / 2 - $new_pixels / 2; + } + return $value; +} + +/** + * Internal function for sorting image effect definitions through uasort(). + * + * @see image_effect_definitions() + */ +function _image_effect_definitions_sort($a, $b) { + return strcasecmp($a['name'], $b['name']); +} diff --git a/modules/image/image.test b/core/modules/image/image.test similarity index 100% rename from modules/image/image.test rename to core/modules/image/image.test diff --git a/modules/image/sample.png b/core/modules/image/sample.png similarity index 100% rename from modules/image/sample.png rename to core/modules/image/sample.png diff --git a/modules/image/tests/image_module_test.info b/core/modules/image/tests/image_module_test.info similarity index 100% rename from modules/image/tests/image_module_test.info rename to core/modules/image/tests/image_module_test.info diff --git a/modules/image/tests/image_module_test.module b/core/modules/image/tests/image_module_test.module similarity index 100% rename from modules/image/tests/image_module_test.module rename to core/modules/image/tests/image_module_test.module diff --git a/core/modules/locale/locale.admin.inc b/core/modules/locale/locale.admin.inc new file mode 100644 index 0000000..a82a7e2 --- /dev/null +++ b/core/modules/locale/locale.admin.inc @@ -0,0 +1,1440 @@ + TRUE); + foreach ($languages as $langcode => $language) { + + $options[$langcode] = ''; + if ($language->enabled) { + $enabled[] = $langcode; + } + $form['weight'][$langcode] = array( + '#type' => 'weight', + '#title' => t('Weight for @title', array('@title' => $language->name)), + '#title_display' => 'invisible', + '#default_value' => $language->weight, + '#attributes' => array('class' => array('language-order-weight')), + ); + $form['name'][$langcode] = array('#markup' => check_plain($language->name)); + $form['native'][$langcode] = array('#markup' => check_plain($language->native)); + $form['direction'][$langcode] = array('#markup' => ($language->direction == LANGUAGE_RTL ? t('Right to left') : t('Left to right'))); + } + $form['enabled'] = array( + '#type' => 'checkboxes', + '#title' => t('Enabled languages'), + '#title_display' => 'invisible', + '#options' => $options, + '#default_value' => $enabled, + ); + $form['site_default'] = array( + '#type' => 'radios', + '#title' => t('Default language'), + '#title_display' => 'invisible', + '#options' => $options, + '#default_value' => language_default('language'), + ); + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration')); + $form['#theme'] = 'locale_languages_overview_form'; + + return $form; +} + +/** + * Returns HTML for the language overview form. + * + * @param $variables + * An associative array containing: + * - form: A render element representing the form. + * + * @ingroup themeable + */ +function theme_locale_languages_overview_form($variables) { + $form = $variables['form']; + $default = language_default(); + foreach ($form['name'] as $key => $element) { + // Do not take form control structures. + if (is_array($element) && element_child($key)) { + // Disable checkbox for the default language, because it cannot be disabled. + if ($key == $default->language) { + $form['enabled'][$key]['#attributes']['disabled'] = 'disabled'; + } + + // Add invisible labels for the checkboxes and radio buttons in the table + // for accessibility. These changes are only required and valid when the + // form is themed as a table, so it would be wrong to perform them in the + // form constructor. + $title = drupal_render($form['name'][$key]); + $form['enabled'][$key]['#title'] = t('Enable !title', array('!title' => $title)); + $form['enabled'][$key]['#title_display'] = 'invisible'; + $form['site_default'][$key]['#title'] = t('Set !title as default', array('!title' => $title)); + $form['site_default'][$key]['#title_display'] = 'invisible'; + $rows[] = array( + 'data' => array( + '' . $title . '', + drupal_render($form['native'][$key]), + check_plain($key), + drupal_render($form['direction'][$key]), + array('data' => drupal_render($form['enabled'][$key]), 'align' => 'center'), + drupal_render($form['site_default'][$key]), + drupal_render($form['weight'][$key]), + l(t('edit'), 'admin/config/regional/language/edit/' . $key) . (($key != 'en' && $key != $default->language) ? ' ' . l(t('delete'), 'admin/config/regional/language/delete/' . $key) : '') + ), + 'class' => array('draggable'), + ); + } + } + $header = array(array('data' => t('English name')), array('data' => t('Native name')), array('data' => t('Code')), array('data' => t('Direction')), array('data' => t('Enabled')), array('data' => t('Default')), array('data' => t('Weight')), array('data' => t('Operations'))); + $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'language-order'))); + $output .= drupal_render_children($form); + + drupal_add_tabledrag('language-order', 'order', 'sibling', 'language-order-weight'); + + return $output; +} + +/** + * Process language overview form submissions, updating existing languages. + */ +function locale_languages_overview_form_submit($form, &$form_state) { + $languages = language_list(); + $default = language_default(); + $url_prefixes = variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX) == LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX; + $enabled_count = 0; + + foreach ($languages as $langcode => $language) { + if ($form_state['values']['site_default'] == $langcode || $default->language == $langcode) { + // Automatically enable the default language and the language + // which was default previously (because we will not get the + // value from that disabled checkbox). + $form_state['values']['enabled'][$langcode] = 1; + } + + // If language URL prefixes are enabled we must clear language domains and + // assign a valid prefix to each non-default language. + if ($url_prefixes) { + $language->domain = ''; + if (empty($language->prefix) && $form_state['values']['site_default'] != $langcode) { + $language->prefix = $langcode; + } + } + + if ($form_state['values']['enabled'][$langcode]) { + $enabled_count++; + $language->enabled = 1; + } + else { + $language->enabled = 0; + } + + $language->weight = $form_state['values']['weight'][$langcode]; + + db_update('languages') + ->fields(array( + 'enabled' => $language->enabled, + 'weight' => $language->weight, + 'prefix' => $language->prefix, + 'domain' => $language->domain, + )) + ->condition('language', $langcode) + ->execute(); + + $languages[$langcode] = $language; + } + + variable_set('language_default', $languages[$form_state['values']['site_default']]); + variable_set('language_count', $enabled_count); + drupal_set_message(t('Configuration saved.')); + + // Changing the language settings impacts the interface. + cache_clear_all('*', 'cache_page', TRUE); + module_invoke_all('multilingual_settings_changed'); + + $form_state['redirect'] = 'admin/config/regional/language'; + return; +} + +/** + * User interface for the language addition screen. + */ +function locale_languages_add_screen() { + $build['predefined'] = drupal_get_form('locale_languages_predefined_form'); + $build['custom'] = drupal_get_form('locale_languages_custom_form'); + return $build; +} + +/** + * Predefined language setup form. + */ +function locale_languages_predefined_form($form) { + $predefined = _locale_prepare_predefined_list(); + $form['language list'] = array('#type' => 'fieldset', + '#title' => t('Predefined language'), + '#collapsible' => TRUE, + ); + $form['language list']['langcode'] = array('#type' => 'select', + '#title' => t('Language name'), + '#default_value' => key($predefined), + '#options' => $predefined, + '#description' => t('Use the Custom language section below if your desired language does not appear in this list.'), + ); + $form['language list']['actions'] = array('#type' => 'actions'); + $form['language list']['actions']['submit'] = array('#type' => 'submit', '#value' => t('Add language')); + return $form; +} + +/** + * Custom language addition form. + */ +function locale_languages_custom_form($form) { + $form['custom language'] = array('#type' => 'fieldset', + '#title' => t('Custom language'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + _locale_languages_common_controls($form['custom language']); + $form['custom language']['actions'] = array('#type' => 'actions'); + $form['custom language']['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Add custom language') + ); + // Reuse the validation and submit functions of the predefined language setup form. + $form['#submit'][] = 'locale_languages_predefined_form_submit'; + $form['#validate'][] = 'locale_languages_predefined_form_validate'; + return $form; +} + +/** + * Editing screen for a particular language. + * + * @param $langcode + * Language code of the language to edit. + */ +function locale_languages_edit_form($form, &$form_state, $langcode) { + if ($language = db_query("SELECT * FROM {languages} WHERE language = :language", array(':language' => $langcode))->fetchObject()) { + _locale_languages_common_controls($form, $language); + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save language') + ); + $form['#submit'][] = 'locale_languages_edit_form_submit'; + $form['#validate'][] = 'locale_languages_edit_form_validate'; + return $form; + } + else { + drupal_not_found(); + drupal_exit(); + } +} + +/** + * Common elements of the language addition and editing form. + * + * @param $form + * A parent form item (or empty array) to add items below. + * @param $language + * Language object to edit. + */ +function _locale_languages_common_controls(&$form, $language = NULL) { + if (!is_object($language)) { + $language = new stdClass(); + } + if (isset($language->language)) { + $form['langcode_view'] = array( + '#type' => 'item', + '#title' => t('Language code'), + '#markup' => $language->language + ); + $form['langcode'] = array( + '#type' => 'value', + '#value' => $language->language + ); + } + else { + $form['langcode'] = array('#type' => 'textfield', + '#title' => t('Language code'), + '#size' => 12, + '#maxlength' => 60, + '#required' => TRUE, + '#default_value' => @$language->language, + '#disabled' => (isset($language->language)), + '#description' => t('RFC 4646 compliant language identifier. Language codes typically use a country code, and optionally, a script or regional variant name. Examples: "en", "en-US" and "zh-Hant".', array('@rfc4646' => 'http://www.ietf.org/rfc/rfc4646.txt')), + ); + } + $form['name'] = array('#type' => 'textfield', + '#title' => t('Language name in English'), + '#maxlength' => 64, + '#default_value' => @$language->name, + '#required' => TRUE, + '#description' => t('Name of the language in English. Will be available for translation in all languages.'), + ); + $form['native'] = array('#type' => 'textfield', + '#title' => t('Native language name'), + '#maxlength' => 64, + '#default_value' => @$language->native, + '#required' => TRUE, + '#description' => t('Name of the language in the language being added.'), + ); + $form['prefix'] = array('#type' => 'textfield', + '#title' => t('Path prefix language code'), + '#maxlength' => 64, + '#default_value' => @$language->prefix, + '#description' => t('Language code or other custom text to use as a path prefix for URL language detection, if your Detection and selection settings use URL path prefixes. For the default language, this value may be left blank. Modifying this value may break existing URLs. Use with caution in a production environment. Example: Specifying "deutsch" as the path prefix code for German results in URLs like "example.com/deutsch/contact".') + ); + $form['domain'] = array('#type' => 'textfield', + '#title' => t('Language domain'), + '#maxlength' => 128, + '#default_value' => @$language->domain, + '#description' => t('URL including protocol to use for this language, if your Detection and selection settings use URL domains. For the default language, this value may be left blank. Modifying this value may break existing URLs. Use with caution in a production environment. Example: Specifying "http://example.de" or "http://de.example.com" as language domains for German results in URLs like "http://example.de/contact" and "http://de.example.com/contact", respectively.'), + ); + $form['direction'] = array('#type' => 'radios', + '#title' => t('Direction'), + '#required' => TRUE, + '#description' => t('Direction that text in this language is presented.'), + '#default_value' => @$language->direction, + '#options' => array(LANGUAGE_LTR => t('Left to right'), LANGUAGE_RTL => t('Right to left')) + ); + return $form; +} + +/** + * Validate the language addition form. + */ +function locale_languages_predefined_form_validate($form, &$form_state) { + $langcode = $form_state['values']['langcode']; + + if (($duplicate = db_query("SELECT COUNT(*) FROM {languages} WHERE language = :language", array(':language' => $langcode))->fetchField()) != 0) { + form_set_error('langcode', t('The language %language (%code) already exists.', array('%language' => $form_state['values']['name'], '%code' => $langcode))); + } + + if (!isset($form_state['values']['name'])) { + // Predefined language selection. + include_once DRUPAL_ROOT . '/core/includes/iso.inc'; + $predefined = _locale_get_predefined_list(); + if (!isset($predefined[$langcode])) { + form_set_error('langcode', t('Invalid language code.')); + } + } + else { + // Reuse the editing form validation routine if we add a custom language. + locale_languages_edit_form_validate($form, $form_state); + } +} + +/** + * Process the language addition form submission. + */ +function locale_languages_predefined_form_submit($form, &$form_state) { + $langcode = $form_state['values']['langcode']; + if (isset($form_state['values']['name'])) { + // Custom language form. + locale_add_language($langcode, $form_state['values']['name'], $form_state['values']['native'], $form_state['values']['direction'], $form_state['values']['domain'], $form_state['values']['prefix']); + drupal_set_message(t('The language %language has been created and can now be used. More information is available on the help screen.', array('%language' => t($form_state['values']['name']), '@locale-help' => url('admin/help/locale')))); + } + else { + // Predefined language selection. + include_once DRUPAL_ROOT . '/core/includes/iso.inc'; + $predefined = _locale_get_predefined_list(); + locale_add_language($langcode); + drupal_set_message(t('The language %language has been created and can now be used. More information is available on the help screen.', array('%language' => t($predefined[$langcode][0]), '@locale-help' => url('admin/help/locale')))); + } + + // See if we have language files to import for the newly added + // language, collect and import them. + if ($batch = locale_batch_by_language($langcode, '_locale_batch_language_finished')) { + batch_set($batch); + } + + $form_state['redirect'] = 'admin/config/regional/language'; +} + +/** + * Validate the language editing form. Reused for custom language addition too. + */ +function locale_languages_edit_form_validate($form, &$form_state) { + // Ensure sane field values for langcode, name, and native. + if (!isset($form['langcode_view']) && preg_match('@[^a-zA-Z_-]@', $form_state['values']['langcode'])) { + form_set_error('langcode', t('%field may only contain characters a-z, underscores, or hyphens.', array('%field' => $form['langcode']['#title']))); + } + if ($form_state['values']['name'] != check_plain($form_state['values']['name'])) { + form_set_error('name', t('%field cannot contain any markup.', array('%field' => $form['name']['#title']))); + } + if ($form_state['values']['native'] != check_plain($form_state['values']['native'])) { + form_set_error('native', t('%field cannot contain any markup.', array('%field' => $form['native']['#title']))); + } + + if (!empty($form_state['values']['domain']) && !empty($form_state['values']['prefix'])) { + form_set_error('prefix', t('Domain and path prefix values should not be set at the same time.')); + } + if (!empty($form_state['values']['domain']) && $duplicate = db_query("SELECT language FROM {languages} WHERE domain = :domain AND language <> :language", array(':domain' => $form_state['values']['domain'], ':language' => $form_state['values']['langcode']))->fetchField()) { + form_set_error('domain', t('The domain (%domain) is already tied to a language (%language).', array('%domain' => $form_state['values']['domain'], '%language' => $duplicate->language))); + } + if (empty($form_state['values']['prefix']) && language_default('language') != $form_state['values']['langcode'] && empty($form_state['values']['domain'])) { + form_set_error('prefix', t('Only the default language can have both the domain and prefix empty.')); + } + if (!empty($form_state['values']['prefix']) && $duplicate = db_query("SELECT language FROM {languages} WHERE prefix = :prefix AND language <> :language", array(':prefix' => $form_state['values']['prefix'], ':language' => $form_state['values']['langcode']))->fetchField()) { + form_set_error('prefix', t('The prefix (%prefix) is already tied to a language (%language).', array('%prefix' => $form_state['values']['prefix'], '%language' => $duplicate->language))); + } +} + +/** + * Process the language editing form submission. + */ +function locale_languages_edit_form_submit($form, &$form_state) { + db_update('languages') + ->fields(array( + 'name' => $form_state['values']['name'], + 'native' => $form_state['values']['native'], + 'domain' => $form_state['values']['domain'], + 'prefix' => $form_state['values']['prefix'], + 'direction' => $form_state['values']['direction'], + )) + ->condition('language', $form_state['values']['langcode']) + ->execute(); + $default = language_default(); + if ($default->language == $form_state['values']['langcode']) { + $properties = array('name', 'native', 'direction', 'enabled', 'plurals', 'formula', 'domain', 'prefix', 'weight'); + foreach ($properties as $keyname) { + if (isset($form_state['values'][$keyname])) { + $default->$keyname = $form_state['values'][$keyname]; + } + } + variable_set('language_default', $default); + } + $form_state['redirect'] = 'admin/config/regional/language'; + return; +} + +/** + * User interface for the language deletion confirmation screen. + */ +function locale_languages_delete_form($form, &$form_state, $langcode) { + + // Do not allow deletion of English locale. + if ($langcode == 'en') { + drupal_set_message(t('The English language cannot be deleted.')); + drupal_goto('admin/config/regional/language'); + } + + if (language_default('language') == $langcode) { + drupal_set_message(t('The default language cannot be deleted.')); + drupal_goto('admin/config/regional/language'); + } + + // For other languages, warn user that data loss is ahead. + $languages = language_list(); + + if (!isset($languages[$langcode])) { + drupal_not_found(); + drupal_exit(); + } + else { + $form['langcode'] = array('#type' => 'value', '#value' => $langcode); + return confirm_form($form, t('Are you sure you want to delete the language %name?', array('%name' => t($languages[$langcode]->name))), 'admin/config/regional/language', t('Deleting a language will remove all interface translations associated with it, and posts in this language will be set to be language neutral. This action cannot be undone.'), t('Delete'), t('Cancel')); + } +} + +/** + * Process language deletion submissions. + */ +function locale_languages_delete_form_submit($form, &$form_state) { + $languages = language_list(); + if (isset($languages[$form_state['values']['langcode']])) { + // Remove translations first. + db_delete('locales_target') + ->condition('language', $form_state['values']['langcode']) + ->execute(); + cache_clear_all('locale:' . $form_state['values']['langcode'], 'cache'); + // With no translations, this removes existing JavaScript translations file. + _locale_rebuild_js($form_state['values']['langcode']); + // Remove the language. + db_delete('languages') + ->condition('language', $form_state['values']['langcode']) + ->execute(); + db_update('node') + ->fields(array('language' => '')) + ->condition('language', $form_state['values']['langcode']) + ->execute(); + if ($languages[$form_state['values']['langcode']]->enabled) { + variable_set('language_count', variable_get('language_count', 1) - 1); + } + module_invoke_all('multilingual_settings_changed'); + $variables = array('%locale' => $languages[$form_state['values']['langcode']]->name); + drupal_set_message(t('The language %locale has been removed.', $variables)); + watchdog('locale', 'The language %locale has been removed.', $variables); + } + + // Changing the language settings impacts the interface: + cache_clear_all('*', 'cache_page', TRUE); + + $form_state['redirect'] = 'admin/config/regional/language'; + return; +} + +/** + * Setting for language negotiation options + */ +function locale_languages_configure_form() { + include_once DRUPAL_ROOT . '/core/includes/language.inc'; + + $form = array( + '#submit' => array('locale_languages_configure_form_submit'), + '#theme' => 'locale_languages_configure_form', + '#language_types' => language_types_configurable(FALSE), + '#language_types_info' => language_types_info(), + '#language_providers' => language_negotiation_info(), + ); + + foreach ($form['#language_types'] as $type) { + _locale_languages_configure_form_language_table($form, $type); + } + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save settings'), + ); + + return $form; +} + +/** + * Helper function to build a language provider table. + */ +function _locale_languages_configure_form_language_table(&$form, $type) { + $info = $form['#language_types_info'][$type]; + + $table_form = array( + '#title' => t('@type language detection', array('@type' => $info['name'])), + '#tree' => TRUE, + '#description' => $info['description'], + '#language_providers' => array(), + '#show_operations' => FALSE, + 'weight' => array('#tree' => TRUE), + 'enabled' => array('#tree' => TRUE), + ); + + $language_providers = $form['#language_providers']; + $enabled_providers = variable_get("language_negotiation_$type", array()); + $providers_weight = variable_get("locale_language_providers_weight_$type", array()); + + // Add missing data to the providers lists. + foreach ($language_providers as $id => $provider) { + if (!isset($providers_weight[$id])) { + $providers_weight[$id] = language_provider_weight($provider); + } + } + + // Order providers list by weight. + asort($providers_weight); + + foreach ($providers_weight as $id => $weight) { + // A language provider might be no more available if the defining module has + // been disabled after the last configuration saving. + if (!isset($language_providers[$id])) { + continue; + } + + $enabled = isset($enabled_providers[$id]); + $provider = $language_providers[$id]; + + // List the provider only if the current type is defined in its 'types' key. + // If it is not defined default to all the configurable language types. + $types = array_flip(isset($provider['types']) ? $provider['types'] : $form['#language_types']); + + if (isset($types[$type])) { + $table_form['#language_providers'][$id] = $provider; + $provider_name = check_plain($provider['name']); + + $table_form['weight'][$id] = array( + '#type' => 'weight', + '#title' => t('Weight for !title language detection method', array('!title' => drupal_strtolower($provider_name))), + '#title_display' => 'invisible', + '#default_value' => $weight, + '#attributes' => array('class' => array("language-provider-weight-$type")), + ); + + $table_form['title'][$id] = array('#markup' => $provider_name); + + $table_form['enabled'][$id] = array( + '#type' => 'checkbox', + '#title' => t('Enable !title language detection method', array('!title' => drupal_strtolower($provider_name))), + '#title_display' => 'invisible', + '#default_value' => $enabled, + ); + if ($id === LANGUAGE_NEGOTIATION_DEFAULT) { + $table_form['enabled'][$id]['#default_value'] = TRUE; + $table_form['enabled'][$id]['#attributes'] = array('disabled' => 'disabled'); + } + + $table_form['description'][$id] = array('#markup' => filter_xss_admin($provider['description'])); + + $config_op = array(); + if (isset($provider['config'])) { + $config_op = array('#type' => 'link', '#title' => t('Configure'), '#href' => $provider['config']); + // If there is at least one operation enabled show the operation column. + $table_form['#show_operations'] = TRUE; + } + $table_form['operation'][$id] = $config_op; + } + } + + $form[$type] = $table_form; +} + +/** + * Returns HTML for a language configuration form. + * + * @param $variables + * An associative array containing: + * - form: A render element representing the form. + * + * @ingroup themeable + */ +function theme_locale_languages_configure_form($variables) { + $form = $variables['form']; + $output = ''; + + foreach ($form['#language_types'] as $type) { + $rows = array(); + $info = $form['#language_types_info'][$type]; + $title = ''; + $description = '
' . $form[$type]['#description'] . '
'; + + foreach ($form[$type]['title'] as $id => $element) { + // Do not take form control structures. + if (is_array($element) && element_child($id)) { + $row = array( + 'data' => array( + '' . drupal_render($form[$type]['title'][$id]) . '', + drupal_render($form[$type]['description'][$id]), + drupal_render($form[$type]['enabled'][$id]), + drupal_render($form[$type]['weight'][$id]), + ), + 'class' => array('draggable'), + ); + if ($form[$type]['#show_operations']) { + $row['data'][] = drupal_render($form[$type]['operation'][$id]); + } + $rows[] = $row; + } + } + + $header = array( + array('data' => t('Detection method')), + array('data' => t('Description')), + array('data' => t('Enabled')), + array('data' => t('Weight')), + ); + + // If there is at least one operation enabled show the operation column. + if ($form[$type]['#show_operations']) { + $header[] = array('data' => t('Operations')); + } + + $variables = array( + 'header' => $header, + 'rows' => $rows, + 'attributes' => array('id' => "language-negotiation-providers-$type"), + ); + $table = theme('table', $variables); + $table .= drupal_render_children($form[$type]); + + drupal_add_tabledrag("language-negotiation-providers-$type", 'order', 'sibling', "language-provider-weight-$type"); + + $output .= '
' . $title . $description . $table . '
'; + } + + $output .= drupal_render_children($form); + return $output; +} + +/** + * Submit handler for language negotiation settings. + */ +function locale_languages_configure_form_submit($form, &$form_state) { + $configurable_types = $form['#language_types']; + + foreach ($configurable_types as $type) { + $negotiation = array(); + $enabled_providers = $form_state['values'][$type]['enabled']; + $enabled_providers[LANGUAGE_NEGOTIATION_DEFAULT] = TRUE; + $providers_weight = $form_state['values'][$type]['weight']; + + foreach ($providers_weight as $id => $weight) { + if ($enabled_providers[$id]) { + $provider = $form[$type]['#language_providers'][$id]; + $provider['weight'] = $weight; + $negotiation[$id] = $provider; + } + } + + language_negotiation_set($type, $negotiation); + variable_set("locale_language_providers_weight_$type", $providers_weight); + } + + // Update non-configurable language types and the related language negotiation + // configuration. + language_types_set(); + + $form_state['redirect'] = 'admin/config/regional/language/configure'; + drupal_set_message(t('Language negotiation configuration saved.')); +} + +/** + * The URL language provider configuration form. + */ +function locale_language_providers_url_form($form, &$form_state) { + $form['locale_language_negotiation_url_part'] = array( + '#title' => t('Part of the URL that determines language'), + '#type' => 'radios', + '#options' => array( + LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX => t('Path prefix'), + LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN => t('Domain'), + ), + '#default_value' => variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX), + '#description' => t('Path prefix: URLs like http://example.com/de/contact set language to German (de). Domain: URLs like http://de.example.com/contact set the language to German. Warning: Changing this setting may break incoming URLs. Use with caution on a production site.'), + ); + + $form_state['redirect'] = 'admin/config/regional/language/configure'; + + return system_settings_form($form); +} + +/** + * The URL language provider configuration form. + */ +function locale_language_providers_session_form($form, &$form_state) { + $form['locale_language_negotiation_session_param'] = array( + '#title' => t('Request/session parameter'), + '#type' => 'textfield', + '#default_value' => variable_get('locale_language_negotiation_session_param', 'language'), + '#description' => t('Name of the request/session parameter used to determine the desired language.'), + ); + + $form_state['redirect'] = 'admin/config/regional/language/configure'; + + return system_settings_form($form); +} + +/** + * @} End of "locale-language-administration" + */ + +/** + * @defgroup locale-translate-administration-screens Translation administration screens + * @{ + * Screens for translation administration. + * + * These functions provide various screens as administration interface + * to import, export and view translations. + */ + +/** + * Overview screen for translations. + */ +function locale_translate_overview_screen() { + drupal_static_reset('language_list'); + $languages = language_list('language'); + $groups = module_invoke_all('locale', 'groups'); + + // Build headers with all groups in order. + $headers = array_merge(array(t('Language')), array_values($groups)); + + // Collect summaries of all source strings in all groups. + $sums = db_query("SELECT COUNT(*) AS strings, textgroup FROM {locales_source} GROUP BY textgroup"); + $groupsums = array(); + foreach ($sums as $group) { + $groupsums[$group->textgroup] = $group->strings; + } + + // Set up overview table with default values, ensuring common order for values. + $rows = array(); + foreach ($languages as $langcode => $language) { + $rows[$langcode] = array('name' => ($langcode == 'en' ? t('English (built-in)') : t($language->name))); + foreach ($groups as $group => $name) { + $rows[$langcode][$group] = ($langcode == 'en' ? t('n/a') : '0/' . (isset($groupsums[$group]) ? $groupsums[$group] : 0) . ' (0%)'); + } + } + + // Languages with at least one record in the locale table. + $translations = db_query("SELECT COUNT(*) AS translation, t.language, s.textgroup FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid GROUP BY textgroup, language"); + foreach ($translations as $data) { + $ratio = (!empty($groupsums[$data->textgroup]) && $data->translation > 0) ? round(($data->translation/$groupsums[$data->textgroup]) * 100.0, 2) : 0; + $rows[$data->language][$data->textgroup] = $data->translation . '/' . $groupsums[$data->textgroup] . " ($ratio%)"; + } + + return theme('table', array('header' => $headers, 'rows' => $rows)); +} + +/** + * String search screen. + */ +function locale_translate_seek_screen() { + // Add CSS. + drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css'); + + $elements = drupal_get_form('locale_translation_filter_form'); + $output = drupal_render($elements); + $output .= _locale_translate_seek(); + return $output; +} + +/** + * List locale translation filters that can be applied. + */ +function locale_translation_filters() { + $filters = array(); + + // Get all languages, except English + drupal_static_reset('language_list'); + $languages = locale_language_list('name'); + unset($languages['en']); + + $filters['string'] = array( + 'title' => t('String contains'), + 'description' => t('Leave blank to show all strings. The search is case sensitive.'), + ); + + $filters['language'] = array( + 'title' => t('Language'), + 'options' => array_merge(array('all' => t('All languages'), 'en' => t('English (provided by Drupal)')), $languages), + ); + + $filters['translation'] = array( + 'title' => t('Search in'), + 'options' => array('all' => t('Both translated and untranslated strings'), 'translated' => t('Only translated strings'), 'untranslated' => t('Only untranslated strings')), + ); + + $groups = module_invoke_all('locale', 'groups'); + $filters['group'] = array( + 'title' => t('Limit search to'), + 'options' => array_merge(array('all' => t('All text groups')), $groups), + ); + + return $filters; +} + +/** + * Return form for locale translation filters. + * + * @ingroup forms + */ +function locale_translation_filter_form() { + $filters = locale_translation_filters(); + + $form['filters'] = array( + '#type' => 'fieldset', + '#title' => t('Filter translatable strings'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + ); + foreach ($filters as $key => $filter) { + // Special case for 'string' filter. + if ($key == 'string') { + $form['filters']['status']['string'] = array( + '#type' => 'textfield', + '#title' => $filter['title'], + '#description' => $filter['description'], + ); + } + else { + $form['filters']['status'][$key] = array( + '#title' => $filter['title'], + '#type' => 'select', + '#empty_value' => 'all', + '#empty_option' => $filter['options']['all'], + '#size' => 0, + '#options' => $filter['options'], + ); + } + if (!empty($_SESSION['locale_translation_filter'][$key])) { + $form['filters']['status'][$key]['#default_value'] = $_SESSION['locale_translation_filter'][$key]; + } + } + + $form['filters']['actions'] = array( + '#type' => 'actions', + '#attributes' => array('class' => array('container-inline')), + ); + $form['filters']['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Filter'), + ); + if (!empty($_SESSION['locale_translation_filter'])) { + $form['filters']['actions']['reset'] = array( + '#type' => 'submit', + '#value' => t('Reset') + ); + } + + return $form; +} + +/** + * Validate result from locale translation filter form. + */ +function locale_translation_filter_form_validate($form, &$form_state) { + if ($form_state['values']['op'] == t('Filter') && empty($form_state['values']['language']) && empty($form_state['values']['group'])) { + form_set_error('type', t('You must select something to filter by.')); + } +} + +/** + * Process result from locale translation filter form. + */ +function locale_translation_filter_form_submit($form, &$form_state) { + $op = $form_state['values']['op']; + $filters = locale_translation_filters(); + switch ($op) { + case t('Filter'): + foreach ($filters as $name => $filter) { + if (isset($form_state['values'][$name])) { + $_SESSION['locale_translation_filter'][$name] = $form_state['values'][$name]; + } + } + break; + case t('Reset'): + $_SESSION['locale_translation_filter'] = array(); + break; + } + + $form_state['redirect'] = 'admin/config/regional/translate/translate'; +} + +/** + * User interface for the translation import screen. + */ +function locale_translate_import_form($form) { + // Get all languages, except English + drupal_static_reset('language_list'); + $names = locale_language_list('name'); + unset($names['en']); + + if (!count($names)) { + $languages = _locale_prepare_predefined_list(); + $default = key($languages); + } + else { + $languages = array( + t('Already added languages') => $names, + t('Languages not yet added') => _locale_prepare_predefined_list() + ); + $default = key($names); + } + + $form['import'] = array('#type' => 'fieldset', + '#title' => t('Import translation'), + ); + $form['import']['file'] = array('#type' => 'file', + '#title' => t('Language file'), + '#size' => 50, + '#description' => t('A Gettext Portable Object (.po) file.'), + ); + $form['import']['langcode'] = array('#type' => 'select', + '#title' => t('Import into'), + '#options' => $languages, + '#default_value' => $default, + '#description' => t('Choose the language you want to add strings into. If you choose a language which is not yet set up, it will be added.'), + ); + $form['import']['group'] = array('#type' => 'radios', + '#title' => t('Text group'), + '#default_value' => 'default', + '#options' => module_invoke_all('locale', 'groups'), + '#description' => t('Imported translations will be added to this text group.'), + ); + $form['import']['mode'] = array('#type' => 'radios', + '#title' => t('Mode'), + '#default_value' => LOCALE_IMPORT_KEEP, + '#options' => array( + LOCALE_IMPORT_OVERWRITE => t('Strings in the uploaded file replace existing ones, new ones are added. The plural format is updated.'), + LOCALE_IMPORT_KEEP => t('Existing strings and the plural format are kept, only new strings are added.') + ), + ); + $form['import']['submit'] = array('#type' => 'submit', '#value' => t('Import')); + + return $form; +} + +/** + * Process the locale import form submission. + */ +function locale_translate_import_form_submit($form, &$form_state) { + $validators = array('file_validate_extensions' => array('po')); + // Ensure we have the file uploaded + if ($file = file_save_upload('file', $validators)) { + + // Add language, if not yet supported + drupal_static_reset('language_list'); + $languages = language_list('language'); + $langcode = $form_state['values']['langcode']; + if (!isset($languages[$langcode])) { + include_once DRUPAL_ROOT . '/core/includes/iso.inc'; + $predefined = _locale_get_predefined_list(); + locale_add_language($langcode); + drupal_set_message(t('The language %language has been created.', array('%language' => t($predefined[$langcode][0])))); + } + + // Now import strings into the language + if ($return = _locale_import_po($file, $langcode, $form_state['values']['mode'], $form_state['values']['group']) == FALSE) { + $variables = array('%filename' => $file->filename); + drupal_set_message(t('The translation import of %filename failed.', $variables), 'error'); + watchdog('locale', 'The translation import of %filename failed.', $variables, LOG_ERR); + } + } + else { + drupal_set_message(t('File to import not found.'), 'error'); + $form_state['redirect'] = 'admin/config/regional/translate/import'; + return; + } + + $form_state['redirect'] = 'admin/config/regional/translate'; + return; +} + +/** + * User interface for the translation export screen. + */ +function locale_translate_export_screen() { + // Get all languages, except English + drupal_static_reset('language_list'); + $names = locale_language_list('name'); + unset($names['en']); + $output = ''; + // Offer translation export if any language is set up. + if (count($names)) { + $elements = drupal_get_form('locale_translate_export_po_form', $names); + $output = drupal_render($elements); + } + $elements = drupal_get_form('locale_translate_export_pot_form'); + $output .= drupal_render($elements); + return $output; +} + +/** + * Form to export PO files for the languages provided. + * + * @param $names + * An associate array with localized language names + */ +function locale_translate_export_po_form($form, &$form_state, $names) { + $form['export_title'] = array('#type' => 'item', + '#title' => t('Export translation'), + ); + $form['langcode'] = array('#type' => 'select', + '#title' => t('Language name'), + '#options' => $names, + '#description' => t('Select the language to export in Gettext Portable Object (.po) format.'), + ); + $form['group'] = array('#type' => 'radios', + '#title' => t('Text group'), + '#default_value' => 'default', + '#options' => module_invoke_all('locale', 'groups'), + ); + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Export')); + return $form; +} + +/** + * Translation template export form. + */ +function locale_translate_export_pot_form() { + // Complete template export of the strings + $form['export_title'] = array('#type' => 'item', + '#title' => t('Export template'), + '#description' => t('Generate a Gettext Portable Object Template (.pot) file with all strings from the Drupal locale database.'), + ); + $form['group'] = array('#type' => 'radios', + '#title' => t('Text group'), + '#default_value' => 'default', + '#options' => module_invoke_all('locale', 'groups'), + ); + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Export')); + // Reuse PO export submission callback. + $form['#submit'][] = 'locale_translate_export_po_form_submit'; + return $form; +} + +/** + * Process a translation (or template) export form submission. + */ +function locale_translate_export_po_form_submit($form, &$form_state) { + // If template is required, language code is not given. + $language = NULL; + if (isset($form_state['values']['langcode'])) { + $languages = language_list(); + $language = $languages[$form_state['values']['langcode']]; + } + _locale_export_po($language, _locale_export_po_generate($language, _locale_export_get_strings($language, $form_state['values']['group']))); +} +/** + * @} End of "locale-translate-administration-screens" + */ + +/** + * @defgroup locale-translate-edit-delete Translation editing/deletion interface + * @{ + * Edit and delete translation strings. + * + * These functions provide the user interface to edit and delete + * translation strings. + */ + +/** + * User interface for string editing. + */ +function locale_translate_edit_form($form, &$form_state, $lid) { + // Fetch source string, if possible. + $source = db_query('SELECT source, context, textgroup, location FROM {locales_source} WHERE lid = :lid', array(':lid' => $lid))->fetchObject(); + if (!$source) { + drupal_set_message(t('String not found.'), 'error'); + drupal_goto('admin/config/regional/translate/translate'); + } + + // Add original text to the top and some values for form altering. + $form['original'] = array( + '#type' => 'item', + '#title' => t('Original text'), + '#markup' => check_plain(wordwrap($source->source, 0)), + ); + if (!empty($source->context)) { + $form['context'] = array( + '#type' => 'item', + '#title' => t('Context'), + '#markup' => check_plain($source->context), + ); + } + $form['lid'] = array( + '#type' => 'value', + '#value' => $lid + ); + $form['textgroup'] = array( + '#type' => 'value', + '#value' => $source->textgroup, + ); + $form['location'] = array( + '#type' => 'value', + '#value' => $source->location + ); + + // Include default form controls with empty values for all languages. + // This ensures that the languages are always in the same order in forms. + $languages = language_list(); + $default = language_default(); + // We don't need the default language value, that value is in $source. + $omit = $source->textgroup == 'default' ? 'en' : $default->language; + unset($languages[($omit)]); + $form['translations'] = array('#tree' => TRUE); + // Approximate the number of rows to use in the default textarea. + $rows = min(ceil(str_word_count($source->source) / 12), 10); + foreach ($languages as $langcode => $language) { + $form['translations'][$langcode] = array( + '#type' => 'textarea', + '#title' => t($language->name), + '#rows' => $rows, + '#default_value' => '', + ); + } + + // Fetch translations and fill in default values in the form. + $result = db_query("SELECT DISTINCT translation, language FROM {locales_target} WHERE lid = :lid AND language <> :omit", array(':lid' => $lid, ':omit' => $omit)); + foreach ($result as $translation) { + $form['translations'][$translation->language]['#default_value'] = $translation->translation; + } + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save translations')); + return $form; +} + +/** + * Validate string editing form submissions. + */ +function locale_translate_edit_form_validate($form, &$form_state) { + // Locale string check is needed for default textgroup only. + $safe_check_needed = $form_state['values']['textgroup'] == 'default'; + foreach ($form_state['values']['translations'] as $key => $value) { + if ($safe_check_needed && !locale_string_is_safe($value)) { + form_set_error('translations', t('The submitted string contains disallowed HTML: %string', array('%string' => $value))); + watchdog('locale', 'Attempted submission of a translation string with disallowed HTML: %string', array('%string' => $value), LOG_WARNING); + } + } +} + +/** + * Process string editing form submissions. + * + * Saves all translations of one string submitted from a form. + */ +function locale_translate_edit_form_submit($form, &$form_state) { + $lid = $form_state['values']['lid']; + foreach ($form_state['values']['translations'] as $key => $value) { + $translation = db_query("SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $key))->fetchField(); + if (!empty($value)) { + // Only update or insert if we have a value to use. + if (!empty($translation)) { + db_update('locales_target') + ->fields(array( + 'translation' => $value, + )) + ->condition('lid', $lid) + ->condition('language', $key) + ->execute(); + } + else { + db_insert('locales_target') + ->fields(array( + 'lid' => $lid, + 'translation' => $value, + 'language' => $key, + )) + ->execute(); + } + } + elseif (!empty($translation)) { + // Empty translation entered: remove existing entry from database. + db_delete('locales_target') + ->condition('lid', $lid) + ->condition('language', $key) + ->execute(); + } + + // Force JavaScript translation file recreation for this language. + _locale_invalidate_js($key); + } + + drupal_set_message(t('The string has been saved.')); + + // Clear locale cache. + _locale_invalidate_js(); + cache_clear_all('locale:', 'cache', TRUE); + + $form_state['redirect'] = 'admin/config/regional/translate/translate'; + return; +} + +/** + * String deletion confirmation page. + */ +function locale_translate_delete_page($lid) { + if ($source = db_query('SELECT lid, source FROM {locales_source} WHERE lid = :lid', array(':lid' => $lid))->fetchObject()) { + return drupal_get_form('locale_translate_delete_form', $source); + } + else { + return drupal_not_found(); + } +} + +/** + * User interface for the string deletion confirmation screen. + */ +function locale_translate_delete_form($form, &$form_state, $source) { + $form['lid'] = array('#type' => 'value', '#value' => $source->lid); + return confirm_form($form, t('Are you sure you want to delete the string "%source"?', array('%source' => $source->source)), 'admin/config/regional/translate/translate', t('Deleting the string will remove all translations of this string in all languages. This action cannot be undone.'), t('Delete'), t('Cancel')); +} + +/** + * Process string deletion submissions. + */ +function locale_translate_delete_form_submit($form, &$form_state) { + db_delete('locales_source') + ->condition('lid', $form_state['values']['lid']) + ->execute(); + db_delete('locales_target') + ->condition('lid', $form_state['values']['lid']) + ->execute(); + // Force JavaScript translation file recreation for all languages. + _locale_invalidate_js(); + cache_clear_all('locale:', 'cache', TRUE); + drupal_set_message(t('The string has been removed.')); + $form_state['redirect'] = 'admin/config/regional/translate/translate'; +} +/** + * @} End of "locale-translate-edit-delete" + */ + +/** + * Returns HTML for a locale date format form. + * + * @param $variables + * An associative array containing: + * - form: A render element representing the form. + * + * @ingroup themeable + */ +function theme_locale_date_format_form($variables) { + $form = $variables['form']; + $header = array( + t('Date type'), + t('Format'), + ); + + foreach (element_children($form['date_formats']) as $key) { + $row = array(); + $row[] = $form['date_formats'][$key]['#title']; + unset($form['date_formats'][$key]['#title']); + $row[] = array('data' => drupal_render($form['date_formats'][$key])); + $rows[] = $row; + } + + $output = drupal_render($form['language']); + $output .= theme('table', array('header' => $header, 'rows' => $rows)); + $output .= drupal_render_children($form); + + return $output; +} + +/** + * Display edit date format links for each language. + */ +function locale_date_format_language_overview_page() { + $header = array( + t('Language'), + array('data' => t('Operations'), 'colspan' => '2'), + ); + + // Get list of languages. + $languages = locale_language_list('native'); + + foreach ($languages as $langcode => $info) { + $row = array(); + $row[] = $languages[$langcode]; + $row[] = l(t('edit'), 'admin/config/regional/date-time/locale/' . $langcode . '/edit'); + $row[] = l(t('reset'), 'admin/config/regional/date-time/locale/' . $langcode . '/reset'); + $rows[] = $row; + } + + return theme('table', array('header' => $header, 'rows' => $rows)); +} + +/** + * Provide date localization configuration options to users. + */ +function locale_date_format_form($form, &$form_state, $langcode) { + $languages = locale_language_list('native'); + $language_name = $languages[$langcode]; + + // Display the current language name. + $form['language'] = array( + '#type' => 'item', + '#title' => t('Language'), + '#markup' => check_plain($language_name), + '#weight' => -10, + ); + $form['langcode'] = array( + '#type' => 'value', + '#value' => $langcode, + ); + + // Get list of date format types. + $types = system_get_date_types(); + + // Get list of available formats. + $formats = system_get_date_formats(); + $choices = array(); + foreach ($formats as $type => $list) { + foreach ($list as $f => $format) { + $choices[$f] = format_date(REQUEST_TIME, 'custom', $f); + } + } + reset($formats); + + // Get configured formats for each language. + $locale_formats = system_date_format_locale($langcode); + // Display a form field for each format type. + foreach ($types as $type => $type_info) { + if (!empty($locale_formats) && in_array($type, array_keys($locale_formats))) { + $default = $locale_formats[$type]; + } + else { + $default = variable_get('date_format_' . $type, key($formats)); + } + + // Show date format select list. + $form['date_formats']['date_format_' . $type] = array( + '#type' => 'select', + '#title' => check_plain($type_info['title']), + '#attributes' => array('class' => array('date-format')), + '#default_value' => (isset($choices[$default]) ? $default : 'custom'), + '#options' => $choices, + ); + } + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save configuration'), + ); + + return $form; +} + +/** + * Submit handler for configuring localized date formats on the locale_date_format_form. + */ +function locale_date_format_form_submit($form, &$form_state) { + include_once DRUPAL_ROOT . '/core/includes/locale.inc'; + $langcode = $form_state['values']['langcode']; + + // Get list of date format types. + $types = system_get_date_types(); + foreach ($types as $type => $type_info) { + $format = $form_state['values']['date_format_' . $type]; + if ($format == 'custom') { + $format = $form_state['values']['date_format_' . $type . '_custom']; + } + locale_date_format_save($langcode, $type, $format); + } + drupal_set_message(t('Configuration saved.')); + $form_state['redirect'] = 'admin/config/regional/date-time/locale'; +} + +/** + * Reset locale specific date formats to the global defaults. + * + * @param $langcode + * Language code, e.g. 'en'. + */ +function locale_date_format_reset_form($form, &$form_state, $langcode) { + $form['langcode'] = array('#type' => 'value', '#value' => $langcode); + $languages = language_list(); + return confirm_form($form, + t('Are you sure you want to reset the date formats for %language to the global defaults?', array('%language' => $languages[$langcode]->name)), + 'admin/config/regional/date-time/locale', + t('Resetting will remove all localized date formats for this language. This action cannot be undone.'), + t('Reset'), t('Cancel')); +} + +/** + * Reset date formats for a specific language to global defaults. + */ +function locale_date_format_reset_form_submit($form, &$form_state) { + db_delete('date_format_locale') + ->condition('language', $form_state['values']['langcode']) + ->execute(); + $form_state['redirect'] = 'admin/config/regional/date-time/locale'; +} diff --git a/modules/locale/locale.api.php b/core/modules/locale/locale.api.php similarity index 100% rename from modules/locale/locale.api.php rename to core/modules/locale/locale.api.php diff --git a/modules/locale/locale.css b/core/modules/locale/locale.css similarity index 100% rename from modules/locale/locale.css rename to core/modules/locale/locale.css diff --git a/modules/locale/locale.datepicker.js b/core/modules/locale/locale.datepicker.js similarity index 100% rename from modules/locale/locale.datepicker.js rename to core/modules/locale/locale.datepicker.js diff --git a/modules/locale/locale.info b/core/modules/locale/locale.info similarity index 100% rename from modules/locale/locale.info rename to core/modules/locale/locale.info diff --git a/modules/locale/locale.install b/core/modules/locale/locale.install similarity index 100% rename from modules/locale/locale.install rename to core/modules/locale/locale.install diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module new file mode 100644 index 0000000..2e5fb4e --- /dev/null +++ b/core/modules/locale/locale.module @@ -0,0 +1,1038 @@ +' . t('About') . ''; + $output .= '

' . t('The Locale module allows your Drupal site to be presented in languages other than the default English, and to be multilingual. The Locale module works by maintaining a database of translations, and examining text as it is about to be displayed. When a translation of the text is available in the language to be displayed, the translation is displayed rather than the original text. When a translation is unavailable, the original text is displayed, and then stored for review by a translator. For more information, see the online handbook entry for Locale module.', array('@locale' => 'http://drupal.org/handbook/modules/locale/')) . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '
'; + $output .= '
' . t('Translating interface text') . '
'; + $output .= '
' . t('Translations of text in the Drupal interface may be provided by:'); + $output .= '
    '; + $output .= '
  • ' . t("Translating within your site, using the Locale module's integrated translation interface.", array('@translate' => url('admin/config/regional/translate'))) . '
  • '; + $output .= '
  • ' . t('Importing files from a set of existing translations, known as a translation package. A translation package enables the display of a specific version of Drupal in a specific language, and contains files in the Gettext Portable Object (.po) format. Although not all languages are available for every version of Drupal, translation packages for many languages are available for download from the Drupal translations page.', array('@translations' => 'http://localize.drupal.org')) . '
  • '; + $output .= '
  • ' . t("If an existing translation package does not meet your needs, the Gettext Portable Object (.po) files within a package may be modified, or new .po files may be created, using a desktop Gettext editor. The Locale module's import feature allows the translated strings from a new or modified .po file to be added to your site. The Locale module's export feature generates files from your site's translated strings, that can either be shared with others or edited offline by a Gettext translation editor.", array('@import' => url('admin/config/regional/translate/import'), '@export' => url('admin/config/regional/translate/export'))) . '
  • '; + $output .= '
'; + $output .= '
' . t('Configuring a multilingual site') . '
'; + $output .= '
' . t("Language negotiation allows your site to automatically change language based on the domain or path used for each request. Users may (optionally) select their preferred language on their My account page, and your site can be configured to honor a web browser's preferred language settings. Site content can be translated using the Content translation module.", array('@content-help' => url('admin/help/translation'))) . '
'; + $output .= '
'; + return $output; + case 'admin/config/regional/language': + $output = '

' . t('With multiple languages enabled, interface text can be translated, registered users may select their preferred language, and authors can assign a specific language to content. Download contributed translations from Drupal.org.', array('@translations' => 'http://localize.drupal.org')) . '

'; + return $output; + case 'admin/config/regional/language/add': + return '

' . t('Add a language to be supported by your site. If your desired language is not available in the Language name drop-down, click Custom language and provide a language code and other details manually. When providing a language code manually, be sure to enter a standardized language code, since this code may be used by browsers to determine an appropriate display language.') . '

'; + case 'admin/config/regional/language/configure': + $output = '

' . t("Define how to decide which language is used to display page elements (primarily text provided by Drupal and modules, such as field labels and help text). This decision is made by evaluating a series of detection methods for languages; the first detection method that gets a result will determine which language is used for that type of text. Define the order of evaluation of language detection methods on this page.") . '

'; + return $output; + case 'admin/config/regional/language/configure/session': + $output = '

' . t('Determine the language from a request/session parameter. Example: "http://example.com?language=de" sets language to German based on the use of "de" within the "language" parameter.') . '

'; + return $output; + case 'admin/config/regional/translate': + $output = '

' . t('This page provides an overview of available translatable strings. Drupal displays translatable strings in text groups; modules may define additional text groups containing other translatable strings. Because text groups provide a method of grouping related strings, they are often used to focus translation efforts on specific areas of the Drupal interface.') . '

'; + $output .= '

' . t('See the Languages page for more information on adding support for additional languages.', array('@languages' => url('admin/config/regional/language'))) . '

'; + return $output; + case 'admin/config/regional/translate/import': + $output = '

' . t('This page imports the translated strings contained in an individual Gettext Portable Object (.po) file. Normally distributed as part of a translation package (each translation package may contain several .po files), a .po file may need to be imported after offline editing in a Gettext translation editor. Importing an individual .po file may be a lengthy process.') . '

'; + $output .= '

' . t('Note that the .po files within a translation package are imported automatically (if available) when new modules or themes are enabled, or as new languages are added. Since this page only allows the import of one .po file at a time, it may be simpler to download and extract a translation package into your Drupal installation directory and add the language (which automatically imports all .po files within the package). Translation packages are available for download on the Drupal translation page.', array('@language-add' => url('admin/config/regional/language/add'), '@translations' => 'http://localize.drupal.org')) . '

'; + return $output; + case 'admin/config/regional/translate/export': + return '

' . t('This page exports the translated strings used by your site. An export file may be in Gettext Portable Object (.po) form, which includes both the original string and the translation (used to share translations with others), or in Gettext Portable Object Template (.pot) form, which includes the original strings only (used to create new translations with a Gettext translation editor).') . '

'; + case 'admin/config/regional/translate/translate': + return '

' . t('This page allows a translator to search for specific translated and untranslated strings, and is used when creating or editing translations. (Note: For translation tasks involving many strings, it may be more convenient to export strings for offline editing in a desktop Gettext translation editor.) Searches may be limited to strings found within a specific text group or in a specific language.', array('@export' => url('admin/config/regional/translate/export'))) . '

'; + case 'admin/structure/block/manage/%/%': + if ($arg[4] == 'locale' && $arg[5] == 'language') { + return '

' . t('This block is only shown if at least two languages are enabled and language negotiation is set to URL or Session.', array('@languages' => url('admin/config/regional/language'), '@configuration' => url('admin/config/regional/language/configure'))) . '

'; + } + break; + } +} + +/** + * Implements hook_menu(). + */ +function locale_menu() { + // Manage languages + $items['admin/config/regional/language'] = array( + 'title' => 'Languages', + 'description' => 'Configure languages for content and the user interface.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_languages_overview_form'), + 'access arguments' => array('administer languages'), + 'file' => 'locale.admin.inc', + 'weight' => -10, + ); + $items['admin/config/regional/language/overview'] = array( + 'title' => 'List', + 'weight' => 0, + 'type' => MENU_DEFAULT_LOCAL_TASK, + ); + $items['admin/config/regional/language/add'] = array( + 'title' => 'Add language', + 'page callback' => 'locale_languages_add_screen', // two forms concatenated + 'access arguments' => array('administer languages'), + 'weight' => 5, + 'type' => MENU_LOCAL_ACTION, + 'file' => 'locale.admin.inc', + ); + $items['admin/config/regional/language/configure'] = array( + 'title' => 'Detection and selection', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_languages_configure_form'), + 'access arguments' => array('administer languages'), + 'weight' => 10, + 'file' => 'locale.admin.inc', + 'type' => MENU_LOCAL_TASK, + ); + $items['admin/config/regional/language/configure/url'] = array( + 'title' => 'URL language detection configuration', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_language_providers_url_form'), + 'access arguments' => array('administer languages'), + 'file' => 'locale.admin.inc', + 'type' => MENU_VISIBLE_IN_BREADCRUMB, + ); + $items['admin/config/regional/language/configure/session'] = array( + 'title' => 'Session language detection configuration', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_language_providers_session_form'), + 'access arguments' => array('administer languages'), + 'file' => 'locale.admin.inc', + 'type' => MENU_VISIBLE_IN_BREADCRUMB, + ); + $items['admin/config/regional/language/edit/%'] = array( + 'title' => 'Edit language', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_languages_edit_form', 5), + 'access arguments' => array('administer languages'), + 'file' => 'locale.admin.inc', + ); + $items['admin/config/regional/language/delete/%'] = array( + 'title' => 'Confirm', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_languages_delete_form', 5), + 'access arguments' => array('administer languages'), + 'file' => 'locale.admin.inc', + ); + + // Translation functionality + $items['admin/config/regional/translate'] = array( + 'title' => 'Translate interface', + 'description' => 'Translate the built in interface and optionally other text.', + 'page callback' => 'locale_translate_overview_screen', + 'access arguments' => array('translate interface'), + 'file' => 'locale.admin.inc', + 'weight' => -5, + ); + $items['admin/config/regional/translate/overview'] = array( + 'title' => 'Overview', + 'weight' => 0, + 'type' => MENU_DEFAULT_LOCAL_TASK, + ); + $items['admin/config/regional/translate/translate'] = array( + 'title' => 'Translate', + 'weight' => 10, + 'type' => MENU_LOCAL_TASK, + 'page callback' => 'locale_translate_seek_screen', // search results and form concatenated + 'access arguments' => array('translate interface'), + 'file' => 'locale.admin.inc', + ); + $items['admin/config/regional/translate/import'] = array( + 'title' => 'Import', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_translate_import_form'), + 'access arguments' => array('translate interface'), + 'weight' => 20, + 'type' => MENU_LOCAL_TASK, + 'file' => 'locale.admin.inc', + ); + $items['admin/config/regional/translate/export'] = array( + 'title' => 'Export', + 'page callback' => 'locale_translate_export_screen', // possibly multiple forms concatenated + 'access arguments' => array('translate interface'), + 'weight' => 30, + 'type' => MENU_LOCAL_TASK, + 'file' => 'locale.admin.inc', + ); + $items['admin/config/regional/translate/edit/%'] = array( + 'title' => 'Edit string', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_translate_edit_form', 5), + 'access arguments' => array('translate interface'), + 'file' => 'locale.admin.inc', + ); + $items['admin/config/regional/translate/delete/%'] = array( + 'title' => 'Delete string', + 'page callback' => 'locale_translate_delete_page', + 'page arguments' => array(5), + 'access arguments' => array('translate interface'), + 'file' => 'locale.admin.inc', + ); + + // Localize date formats. + $items['admin/config/regional/date-time/locale'] = array( + 'title' => 'Localize', + 'description' => 'Configure date formats for each locale', + 'page callback' => 'locale_date_format_language_overview_page', + 'access arguments' => array('administer site configuration'), + 'type' => MENU_LOCAL_TASK, + 'weight' => -8, + 'file' => 'locale.admin.inc', + ); + $items['admin/config/regional/date-time/locale/%/edit'] = array( + 'title' => 'Localize date formats', + 'description' => 'Configure date formats for each locale', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_date_format_form', 5), + 'access arguments' => array('administer site configuration'), + 'file' => 'locale.admin.inc', + ); + $items['admin/config/regional/date-time/locale/%/reset'] = array( + 'title' => 'Reset date formats', + 'description' => 'Reset localized date formats to global defaults', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_date_format_reset_form', 5), + 'access arguments' => array('administer site configuration'), + 'file' => 'locale.admin.inc', + ); + + return $items; +} + +/** + * Implements hook_init(). + * + * Initialize date formats according to the user's current locale. + */ +function locale_init() { + global $conf, $language; + include_once DRUPAL_ROOT . '/core/includes/locale.inc'; + + // For each date type (e.g. long, short), get the localized date format + // for the user's current language and override the default setting for it + // in $conf. This should happen on all pages except the date and time formats + // settings page, where we want to display the site default and not the + // localized version. + if (strpos($_GET['q'], 'admin/config/regional/date-time/formats') !== 0) { + $languages = array($language->language); + + // Setup appropriate date formats for this locale. + $formats = locale_get_localized_date_format($languages); + foreach ($formats as $format_type => $format) { + $conf[$format_type] = $format; + } + } +} + +/** + * Implements hook_permission(). + */ +function locale_permission() { + return array( + 'administer languages' => array( + 'title' => t('Administer languages'), + ), + 'translate interface' => array( + 'title' => t('Translate interface texts'), + ), + ); +} + +/** + * Implements hook_locale(). + */ +function locale_locale($op = 'groups') { + switch ($op) { + case 'groups': + return array('default' => t('Built-in interface')); + } +} + +/** + * Form builder callback to display language selection widget. + * + * @ingroup forms + * @see locale_form_alter() + */ +function locale_language_selector_form(&$form, &$form_state, $user) { + global $language; + $languages = language_list('enabled'); + $languages = $languages[1]; + + // If the user is being created, we set the user language to the page language. + $user_preferred_language = $user->uid ? user_preferred_language($user) : $language; + + $names = array(); + foreach ($languages as $langcode => $item) { + $name = t($item->name); + $names[$langcode] = $name . ($item->native != $name ? ' (' . $item->native . ')' : ''); + } + $form['locale'] = array( + '#type' => 'fieldset', + '#title' => t('Language settings'), + '#weight' => 1, + '#access' => ($form['#user_category'] == 'account' || ($form['#user_category'] == 'register' && user_access('administer users'))), + ); + + // Get language negotiation settings. + $mode = language_negotiation_get(LANGUAGE_TYPE_INTERFACE) != LANGUAGE_NEGOTIATION_DEFAULT; + $form['locale']['language'] = array( + '#type' => (count($names) <= 5 ? 'radios' : 'select'), + '#title' => t('Language'), + '#default_value' => $user_preferred_language->language, + '#options' => $names, + '#description' => $mode ? t("This account's default language for e-mails, and preferred language for site presentation.") : t("This account's default language for e-mails."), + ); +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function locale_form_path_admin_form_alter(&$form, &$form_state) { + $form['language'] = array( + '#type' => 'select', + '#title' => t('Language'), + '#options' => array(LANGUAGE_NONE => t('All languages')) + locale_language_list('name'), + '#default_value' => $form['language']['#value'], + '#weight' => -10, + '#description' => t('A path alias set for a specific language will always be used when displaying this page in that language, and takes precedence over path aliases set for All languages.'), + ); +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function locale_form_node_type_form_alter(&$form, &$form_state) { + if (isset($form['type'])) { + $form['workflow']['language_content_type'] = array( + '#type' => 'radios', + '#title' => t('Multilingual support'), + '#default_value' => variable_get('language_content_type_' . $form['#node_type']->type, 0), + '#options' => array(t('Disabled'), t('Enabled')), + '#description' => t('Enable multilingual support for this content type. If enabled, a language selection field will be added to the editing form, allowing you to select from one of the enabled languages. If disabled, new posts are saved with the default language. Existing content will not be affected by changing this option.', array('!languages' => url('admin/config/regional/language'))), + ); + } +} + +/** + * Return whether the given content type has multilingual support. + * + * @return + * True if multilingual support is enabled. + */ +function locale_multilingual_node_type($type_name) { + return (bool) variable_get('language_content_type_' . $type_name, 0); +} + +/** + * Implements hook_form_alter(). + * + * Adds language fields to user forms. + */ +function locale_form_alter(&$form, &$form_state, $form_id) { + // Only alter user forms if there is more than one language. + if (drupal_multilingual()) { + // Display language selector when either creating a user on the admin + // interface or editing a user account. + if ($form_id == 'user_register_form' || ($form_id == 'user_profile_form' && $form['#user_category'] == 'account')) { + locale_language_selector_form($form, $form_state, $form['#user']); + } + } +} + +/** + * Implements hook_form_BASE_FORM_ID_alter(). + */ +function locale_form_node_form_alter(&$form, &$form_state) { + if (isset($form['#node']->type) && locale_multilingual_node_type($form['#node']->type)) { + $form['language'] = array( + '#type' => 'select', + '#title' => t('Language'), + '#default_value' => (isset($form['#node']->language) ? $form['#node']->language : ''), + '#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'), + ); + } + // Node type without language selector: assign the default for new nodes + elseif (!isset($form['#node']->nid)) { + $default = language_default(); + $form['language'] = array( + '#type' => 'value', + '#value' => $default->language + ); + } + $form['#submit'][] = 'locale_field_node_form_submit'; +} + +/** + * Form submit handler for node_form(). + * + * Checks if Locale is registered as a translation handler and handle possible + * node language changes. + * + * This submit handler needs to run before entity_form_submit_build_entity() + * is invoked by node_form_submit_build_node(), because it alters the values of + * attached fields. Therefore, it cannot be a hook_node_submit() implementation. + */ +function locale_field_node_form_submit($form, &$form_state) { + if (field_has_translation_handler('node', 'locale')) { + $node = (object) $form_state['values']; + $available_languages = field_content_languages(); + list(, , $bundle) = entity_extract_ids('node', $node); + + foreach (field_info_instances('node', $bundle) as $instance) { + $field_name = $instance['field_name']; + $field = field_info_field($field_name); + $previous_language = $form[$field_name]['#language']; + + // Handle a possible language change: new language values are inserted, + // previous ones are deleted. + if ($field['translatable'] && $previous_language != $node->language) { + $form_state['values'][$field_name][$node->language] = $node->{$field_name}[$previous_language]; + $form_state['values'][$field_name][$previous_language] = array(); + } + } + } +} + +/** + * Implements hook_theme(). + */ +function locale_theme() { + return array( + 'locale_languages_overview_form' => array( + 'render element' => 'form', + ), + 'locale_languages_configure_form' => array( + 'render element' => 'form', + ), + 'locale_date_format_form' => array( + 'render element' => 'form', + ), + ); +} + +/** + * Implements hook_field_language_alter(). + */ +function locale_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']); + } +} + +/** + * Applies language fallback rules to the fields attached to the given entity. + * + * Core language fallback rules simply check if fields have a field translation + * for the requested language code. If so the requested language is returned, + * otherwise all the fallback candidates are inspected to see if there is a + * field translation available in another language. + * By default this is called by locale_field_language_alter(), but this + * behavior can be disabled by setting the 'locale_field_language_fallback' + * variable to FALSE. + * + * @param $display_language + * A reference to an array of language codes keyed by field name. + * @param $entity + * The entity to be displayed. + * @param $langcode + * The language code $entity has to be displayed in. + */ +function locale_field_language_fallback(&$display_language, $entity, $langcode) { + // Lazily init fallback candidates to avoid unnecessary calls. + $fallback_candidates = NULL; + $field_languages = array(); + + foreach ($display_language as $field_name => $field_language) { + // If the requested language is defined for the current field use it, + // otherwise search for a fallback value among the fallback candidates. + if (isset($entity->{$field_name}[$langcode])) { + $display_language[$field_name] = $langcode; + } + elseif (!empty($entity->{$field_name})) { + if (!isset($fallback_candidates)) { + require_once DRUPAL_ROOT . '/core/includes/language.inc'; + $fallback_candidates = language_fallback_get_candidates(); + } + foreach ($fallback_candidates as $fallback_language) { + if (isset($entity->{$field_name}[$fallback_language])) { + $display_language[$field_name] = $fallback_language; + break; + } + } + } + } +} + +/** + * Implements hook_entity_info_alter(). + */ +function locale_entity_info_alter(&$entity_info) { + $entity_info['node']['translation']['locale'] = TRUE; +} + +/** + * Implements hook_language_types_info(). + * + * Defines the three core language types: + * - Interface language is the only configurable language type in core. It is + * used by t() as the default language if none is specified. + * - Content language is by default non-configurable and inherits the interface + * language negotiated value. It is used by the Field API to determine the + * display language for fields if no explicit value is specified. + * - URL language is by default non-configurable and is determined through the + * URL language provider or the URL fallback provider if no language can be + * detected. It is used by l() as the default language if none is specified. + */ +function locale_language_types_info() { + require_once DRUPAL_ROOT . '/core/includes/locale.inc'; + return array( + LANGUAGE_TYPE_INTERFACE => array( + 'name' => t('User interface text'), + 'description' => t('Order of language detection methods for user interface text. If a translation of user interface text is available in the detected language, it will be displayed.'), + ), + LANGUAGE_TYPE_CONTENT => array( + 'name' => t('Content'), + 'description' => t('Order of language detection methods for content. If a version of content is available in the detected language, it will be displayed.'), + 'fixed' => array(LOCALE_LANGUAGE_NEGOTIATION_INTERFACE), + ), + LANGUAGE_TYPE_URL => array( + 'fixed' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_URL_FALLBACK), + ), + ); +} + +/** + * Implements hook_language_negotiation_info(). + */ +function locale_language_negotiation_info() { + $file = 'core/includes/locale.inc'; + $providers = array(); + + $providers[LOCALE_LANGUAGE_NEGOTIATION_URL] = array( + 'types' => array(LANGUAGE_TYPE_CONTENT, LANGUAGE_TYPE_INTERFACE, LANGUAGE_TYPE_URL), + 'callbacks' => array( + 'language' => 'locale_language_from_url', + 'switcher' => 'locale_language_switcher_url', + 'url_rewrite' => 'locale_language_url_rewrite_url', + ), + 'file' => $file, + 'weight' => -8, + 'name' => t('URL'), + 'description' => t('Determine the language from the URL (Path prefix or domain).'), + 'config' => 'admin/config/regional/language/configure/url', + ); + + $providers[LOCALE_LANGUAGE_NEGOTIATION_SESSION] = array( + 'callbacks' => array( + 'language' => 'locale_language_from_session', + 'switcher' => 'locale_language_switcher_session', + 'url_rewrite' => 'locale_language_url_rewrite_session', + ), + 'file' => $file, + 'weight' => -6, + 'name' => t('Session'), + 'description' => t('Determine the language from a request/session parameter.'), + 'config' => 'admin/config/regional/language/configure/session', + ); + + $providers[LOCALE_LANGUAGE_NEGOTIATION_USER] = array( + 'callbacks' => array('language' => 'locale_language_from_user'), + 'file' => $file, + 'weight' => -4, + 'name' => t('User'), + 'description' => t("Follow the user's language preference."), + ); + + $providers[LOCALE_LANGUAGE_NEGOTIATION_BROWSER] = array( + 'callbacks' => array('language' => 'locale_language_from_browser'), + 'file' => $file, + 'weight' => -2, + 'cache' => 0, + 'name' => t('Browser'), + 'description' => t("Determine the language from the browser's language settings."), + ); + + $providers[LOCALE_LANGUAGE_NEGOTIATION_INTERFACE] = array( + 'types' => array(LANGUAGE_TYPE_CONTENT), + 'callbacks' => array('language' => 'locale_language_from_interface'), + 'file' => $file, + 'weight' => 8, + 'name' => t('Interface'), + 'description' => t('Use the detected interface language.'), + ); + + $providers[LOCALE_LANGUAGE_NEGOTIATION_URL_FALLBACK] = array( + 'types' => array(LANGUAGE_TYPE_URL), + 'callbacks' => array('language' => 'locale_language_url_fallback'), + 'file' => $file, + 'weight' => 8, + 'name' => t('URL fallback'), + 'description' => t('Use an already detected language for URLs if none is found.'), + ); + + return $providers; +} + +/** + * Implements hook_modules_enabled(). + */ +function locale_modules_enabled($modules) { + include_once DRUPAL_ROOT . '/includes/language.inc'; + language_types_set(); + language_negotiation_purge(); +} + +/** + * Implements hook_modules_disabled(). + */ +function locale_modules_disabled($modules) { + locale_modules_enabled($modules); +} + +// --------------------------------------------------------------------------------- +// Locale core functionality + +/** + * Provides interface translation services. + * + * This function is called from t() to translate a string if needed. + * + * @param $string + * A string to look up translation for. If omitted, all the + * cached strings will be returned in all languages already + * used on the page. + * @param $context + * The context of this string. + * @param $langcode + * Language code to use for the lookup. + */ +function locale($string = NULL, $context = NULL, $langcode = NULL) { + global $language; + $locale_t = &drupal_static(__FUNCTION__); + + if (!isset($string)) { + // Return all cached strings if no string was specified + return $locale_t; + } + + $langcode = isset($langcode) ? $langcode : $language->language; + + // Store database cached translations in a static variable. Only build the + // cache after $language has been set to avoid an unnecessary cache rebuild. + if (!isset($locale_t[$langcode]) && isset($language)) { + $locale_t[$langcode] = array(); + // Disabling the usage of string caching allows a module to watch for + // the exact list of strings used on a page. From a performance + // perspective that is a really bad idea, so we have no user + // interface for this. Be careful when turning this option off! + if (variable_get('locale_cache_strings', 1) == 1) { + if ($cache = cache_get('locale:' . $langcode, 'cache')) { + $locale_t[$langcode] = $cache->data; + } + elseif (lock_acquire('locale_cache_' . $langcode)) { + // Refresh database stored cache of translations for given language. + // We only store short strings used in current version, to improve + // performance and consume less memory. + $result = db_query("SELECT s.source, s.context, t.translation, t.language FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.textgroup = 'default' AND s.version = :version AND LENGTH(s.source) < :length", array(':language' => $langcode, ':version' => VERSION, ':length' => variable_get('locale_cache_length', 75))); + foreach ($result as $data) { + $locale_t[$langcode][$data->context][$data->source] = (empty($data->translation) ? TRUE : $data->translation); + } + cache_set('locale:' . $langcode, $locale_t[$langcode]); + lock_release('locale_cache_' . $langcode); + } + } + } + + // If we have the translation cached, skip checking the database + if (!isset($locale_t[$langcode][$context][$string])) { + + // We do not have this translation cached, so get it from the DB. + $translation = db_query("SELECT s.lid, t.translation, s.version FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.source = :source AND s.context = :context AND s.textgroup = 'default'", array( + ':language' => $langcode, + ':source' => $string, + ':context' => (string) $context, + ))->fetchObject(); + if ($translation) { + // We have the source string at least. + // Cache translation string or TRUE if no translation exists. + $locale_t[$langcode][$context][$string] = (empty($translation->translation) ? TRUE : $translation->translation); + + if ($translation->version != VERSION) { + // This is the first use of this string under current Drupal version. Save version + // and clear cache, to include the string into caching next time. Saved version is + // also a string-history information for later pruning of the tables. + db_update('locales_source') + ->fields(array('version' => VERSION)) + ->condition('lid', $translation->lid) + ->execute(); + cache_clear_all('locale:', 'cache', TRUE); + } + } + else { + // We don't have the source string, cache this as untranslated. + db_insert('locales_source') + ->fields(array( + 'location' => request_uri(), + 'source' => $string, + 'context' => (string) $context, + 'textgroup' => 'default', + 'version' => VERSION, + )) + ->execute(); + $locale_t[$langcode][$context][$string] = TRUE; + // Clear locale cache so this string can be added in a later request. + cache_clear_all('locale:', 'cache', TRUE); + } + } + + return ($locale_t[$langcode][$context][$string] === TRUE ? $string : $locale_t[$langcode][$context][$string]); +} + +/** + * Reset static variables used by locale(). + */ +function locale_reset() { + drupal_static_reset('locale'); +} + +/** + * Returns plural form index for a specific number. + * + * The index is computed from the formula of this language. + * + * @param $count + * Number to return plural for. + * @param $langcode + * Optional language code to translate to a language other than + * what is used to display the page. + */ +function locale_get_plural($count, $langcode = NULL) { + global $language; + $locale_formula = &drupal_static(__FUNCTION__, array()); + $plurals = &drupal_static(__FUNCTION__ . ':plurals', array()); + + $langcode = $langcode ? $langcode : $language->language; + + if (!isset($plurals[$langcode][$count])) { + if (empty($locale_formula)) { + $language_list = language_list(); + $locale_formula[$langcode] = $language_list[$langcode]->formula; + } + if ($locale_formula[$langcode]) { + $n = $count; + $plurals[$langcode][$count] = @eval('return intval(' . $locale_formula[$langcode] . ');'); + return $plurals[$langcode][$count]; + } + else { + $plurals[$langcode][$count] = -1; + return -1; + } + } + return $plurals[$langcode][$count]; +} + + +/** + * Returns a language name + */ +function locale_language_name($lang) { + $list = &drupal_static(__FUNCTION__); + if (!isset($list)) { + $list = locale_language_list(); + } + return ($lang && isset($list[$lang])) ? $list[$lang] : t('All'); +} + +/** + * Returns array of language names + * + * @param $field + * 'name' => names in current language, localized + * 'native' => native names + * @param $all + * Boolean to return all languages or only enabled ones + */ +function locale_language_list($field = 'name', $all = FALSE) { + if ($all) { + $languages = language_list(); + } + else { + $languages = language_list('enabled'); + $languages = $languages[1]; + } + $list = array(); + foreach ($languages as $language) { + $list[$language->language] = ($field == 'name') ? t($language->name) : $language->$field; + } + return $list; +} + +/** + * Implements hook_modules_installed(). + */ +function locale_modules_installed($modules) { + locale_system_update($modules); +} + +/** + * Implements hook_themes_enabled(). + * + * @todo This is technically wrong. We must not import upon enabling, but upon + * initial installation. The theme system is missing an installation hook. + */ +function locale_themes_enabled($themes) { + locale_system_update($themes); +} + +/** + * Imports translations when new modules or themes are installed. + * + * This function will either import translation for the component change + * right away, or start a batch if more files need to be imported. + * + * @param $components + * An array of component (theme and/or module) names to import + * translations for. + */ +function locale_system_update($components) { + include_once DRUPAL_ROOT . '/core/includes/locale.inc'; + if ($batch = locale_batch_by_component($components)) { + batch_set($batch); + } +} + +/** + * Implements hook_js_alter(). + * + * This function checks all JavaScript files currently added via drupal_add_js() + * and invokes parsing if they have not yet been parsed for Drupal.t() + * and Drupal.formatPlural() calls. Also refreshes the JavaScript translation + * file if necessary, and adds it to the page. + */ +function locale_js_alter(&$javascript) { + global $language; + + $dir = 'public://' . variable_get('locale_js_directory', 'languages'); + $parsed = variable_get('javascript_parsed', array()); + $files = $new_files = FALSE; + + // Require because locale_js_alter() could be called without locale_init(). + require_once DRUPAL_ROOT . '/core/includes/locale.inc'; + + foreach ($javascript as $item) { + if ($item['type'] == 'file') { + $files = TRUE; + $filepath = $item['data']; + if (!in_array($filepath, $parsed)) { + // Don't parse our own translations files. + if (substr($filepath, 0, strlen($dir)) != $dir) { + _locale_parse_js_file($filepath); + $parsed[] = $filepath; + $new_files = TRUE; + } + } + } + } + + // If there are any new source files we parsed, invalidate existing + // JavaScript translation files for all languages, adding the refresh + // flags into the existing array. + if ($new_files) { + $parsed += _locale_invalidate_js(); + } + + // If necessary, rebuild the translation file for the current language. + if (!empty($parsed['refresh:' . $language->language])) { + // Don't clear the refresh flag on failure, so that another try will + // be performed later. + if (_locale_rebuild_js()) { + unset($parsed['refresh:' . $language->language]); + } + // Store any changes after refresh was attempted. + variable_set('javascript_parsed', $parsed); + } + // If no refresh was attempted, but we have new source files, we need + // to store them too. This occurs if current page is in English. + elseif ($new_files) { + variable_set('javascript_parsed', $parsed); + } + + // Add the translation JavaScript file to the page. + if ($files && !empty($language->javascript)) { + // Add the translation JavaScript file to the page. + $file = $dir . '/' . $language->language . '_' . $language->javascript . '.js'; + $javascript[$file] = drupal_js_defaults($file); + } +} + +/** + * Implements hook_css_alter(). + * + * This function checks all CSS files currently added via drupal_add_css() and + * and checks to see if a related right to left CSS file should be included. + */ +function locale_css_alter(&$css) { + global $language; + + // If the current language is RTL, add the CSS file with the RTL overrides. + if ($language->direction == LANGUAGE_RTL) { + foreach ($css as $data => $item) { + // Only provide RTL overrides for files. + if ($item['type'] == 'file') { + $rtl_path = str_replace('.css', '-rtl.css', $item['data']); + if (file_exists($rtl_path) && !isset($css[$rtl_path])) { + // Replicate the same item, but with the RTL path and a little larger + // weight so that it appears directly after the original CSS file. + $item['data'] = $rtl_path; + $item['weight'] += 0.01; + $css[$rtl_path] = $item; + } + } + } + } +} + + /** + * Implement hook_library_alter(). + * + * Provides the language support for the jQuery UI Date Picker. + */ +function locale_library_alter(&$libraries, $module) { + global $language; + if ($module == 'system' && isset($libraries['system']['ui.datepicker'])) { + $datepicker = drupal_get_path('module', 'locale') . '/locale.datepicker.js'; + $libraries['system']['ui.datepicker']['js'][$datepicker] = array('group' => JS_THEME); + $libraries['system']['ui.datepicker']['js'][] = array( + 'data' => array( + 'jqueryuidatepicker' => array( + 'rtl' => $language->direction == LANGUAGE_RTL, + 'firstDay' => variable_get('date_first_day', 0), + ), + ), + 'type' => 'setting', + ); + } +} + +// --------------------------------------------------------------------------------- +// Language switcher block + +/** + * Implements hook_block_info(). + */ +function locale_block_info() { + include_once DRUPAL_ROOT . '/core/includes/language.inc'; + $block = array(); + $info = language_types_info(); + foreach (language_types_configurable(FALSE) as $type) { + $block[$type] = array( + 'info' => t('Language switcher (@type)', array('@type' => $info[$type]['name'])), + // Not worth caching. + 'cache' => DRUPAL_NO_CACHE, + ); + } + return $block; +} + +/** + * Implements hook_block_view(). + * + * Displays a language switcher. Only show if we have at least two languages. + */ +function locale_block_view($type) { + if (drupal_multilingual()) { + $path = drupal_is_front_page() ? '' : $_GET['q']; + $links = language_negotiation_get_switch_links($type, $path); + + if (isset($links->links)) { + drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css'); + $class = "language-switcher-{$links->provider}"; + $variables = array('links' => $links->links, 'attributes' => array('class' => array($class))); + $block['content'] = theme('links__locale_block', $variables); + $block['subject'] = t('Languages'); + return $block; + } + } +} + +/** + * Implements hook_url_outbound_alter(). + * + * Rewrite outbound URLs with language based prefixes. + */ +function locale_url_outbound_alter(&$path, &$options, $original_path) { + // Only modify internal URLs. + if (!$options['external'] && drupal_multilingual()) { + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['callbacks'] = &drupal_static(__FUNCTION__); + } + $callbacks = &$drupal_static_fast['callbacks']; + + if (!isset($callbacks)) { + $callbacks = array(); + include_once DRUPAL_ROOT . '/core/includes/language.inc'; + + foreach (language_types_configurable() as $type) { + // Get url rewriter callbacks only from enabled language providers. + $negotiation = variable_get("language_negotiation_$type", array()); + + foreach ($negotiation as $id => $provider) { + if (isset($provider['file'])) { + require_once DRUPAL_ROOT . '/' . $provider['file']; + } + + // Avoid duplicate callback entries. + if (isset($provider['callbacks']['url_rewrite'])) { + $callbacks[$provider['callbacks']['url_rewrite']] = NULL; + } + } + } + + $callbacks = array_keys($callbacks); + } + + foreach ($callbacks as $callback) { + $callback($path, $options); + } + + // No language dependent path allowed in this mode. + if (empty($callbacks)) { + unset($options['language']); + } + } +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function locale_form_comment_form_alter(&$form, &$form_state, $form_id) { + // If a content type has multilingual support we set the content language as + // comment language. + if ($form['language']['#value'] == LANGUAGE_NONE && locale_multilingual_node_type($form['#node']->type)) { + global $language_content; + $form['language']['#value'] = $language_content->language; + } +} diff --git a/core/modules/locale/locale.test b/core/modules/locale/locale.test new file mode 100644 index 0000000..d53275b --- /dev/null +++ b/core/modules/locale/locale.test @@ -0,0 +1,2534 @@ + 'Language configuration', + 'description' => 'Adds a new locale and tests changing its status and the default language.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale'); + } + + /** + * Functional tests for adding, editing and deleting languages. + */ + function testLanguageConfiguration() { + global $base_url; + + // User to add and remove language. + $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages')); + $this->drupalLogin($admin_user); + + // Add predefined language. + $edit = array( + 'langcode' => 'fr', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + $this->assertText('fr', t('Language added successfully.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + + // Add custom language. + // Code for the language. + $langcode = 'xx'; + // The English name for the language. + $name = $this->randomName(16); + // The native name for the language. + $native = $this->randomName(16); + // The domain prefix. + $prefix = $langcode; + $edit = array( + 'langcode' => $langcode, + 'name' => $name, + 'native' => $native, + 'prefix' => $prefix, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertText($langcode, t('Language code found.')); + $this->assertText($name, t('Name found.')); + $this->assertText($native, t('Native found.')); + $this->assertText($native, t('Test language added.')); + + // Check if we can change the default language. + $path = 'admin/config/regional/language'; + $this->drupalGet($path); + $this->assertFieldChecked('edit-site-default-en', t('English is the default language.')); + // Change the default language. + $edit = array( + 'site_default' => $langcode, + ); + $this->drupalPost(NULL, $edit, t('Save configuration')); + $this->assertNoFieldChecked('edit-site-default-en', t('Default language updated.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + + // Check if a valid language prefix is added after changing the default + // language. + $this->drupalGet('admin/config/regional/language/edit/en'); + $this->assertFieldByXPath('//input[@name="prefix"]', 'en', t('A valid path prefix has been added to the previous default language.')); + + // Ensure we can't delete the default language. + $this->drupalGet('admin/config/regional/language/delete/' . $langcode); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertText(t('The default language cannot be deleted.'), t('Failed to delete the default language.')); + + // Check if we can disable a language. + $edit = array( + 'enabled[en]' => FALSE, + ); + $this->drupalPost($path, $edit, t('Save configuration')); + $this->assertNoFieldChecked('edit-enabled-en', t('Language disabled.')); + + // Set disabled language to be the default and ensure it is re-enabled. + $edit = array( + 'site_default' => 'en', + ); + $this->drupalPost(NULL, $edit, t('Save configuration')); + $this->assertFieldChecked('edit-enabled-en', t('Default language re-enabled.')); + + // Ensure 'edit' link works. + $this->clickLink(t('edit')); + $this->assertTitle(t('Edit language | Drupal'), t('Page title is "Edit language".')); + // Edit a language. + $name = $this->randomName(16); + $edit = array( + 'name' => $name, + ); + $this->drupalPost('admin/config/regional/language/edit/' . $langcode, $edit, t('Save language')); + $this->assertRaw($name, t('The language has been updated.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + + // Ensure 'delete' link works. + $this->drupalGet('admin/config/regional/language'); + $this->clickLink(t('delete')); + $this->assertText(t('Are you sure you want to delete the language'), t('"delete" link is correct.')); + // Delete an enabled language. + $this->drupalGet('admin/config/regional/language/delete/' . $langcode); + // First test the 'cancel' link. + $this->clickLink(t('Cancel')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertRaw($name, t('The language was not deleted.')); + // Delete the language for real. This a confirm form, we do not need any + // fields changed. + $this->drupalPost('admin/config/regional/language/delete/' . $langcode, array(), t('Delete')); + // We need raw here because %locale will add HTML. + $this->assertRaw(t('The language %locale has been removed.', array('%locale' => $name)), t('The test language has been removed.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + // Verify that language is no longer found. + $this->drupalGet('admin/config/regional/language/delete/' . $langcode); + $this->assertResponse(404, t('Language no longer found.')); + // Make sure the "language_count" variable has been updated correctly. + drupal_static_reset('language_list'); + $enabled = language_list('enabled'); + $this->assertEqual(variable_get('language_count', 1), count($enabled[1]), t('Language count is correct.')); + // Delete a disabled language. + // Disable an enabled language. + $edit = array( + 'enabled[fr]' => FALSE, + ); + $this->drupalPost($path, $edit, t('Save configuration')); + $this->assertNoFieldChecked('edit-enabled-fr', t('French language disabled.')); + // Get the count of enabled languages. + drupal_static_reset('language_list'); + $enabled = language_list('enabled'); + // Delete the disabled language. + $this->drupalPost('admin/config/regional/language/delete/fr', array(), t('Delete')); + // We need raw here because %locale will add HTML. + $this->assertRaw(t('The language %locale has been removed.', array('%locale' => 'French')), t('Disabled language has been removed.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + // Verify that language is no longer found. + $this->drupalGet('admin/config/regional/language/delete/fr'); + $this->assertResponse(404, t('Language no longer found.')); + // Make sure the "language_count" variable has not changed. + $this->assertEqual(variable_get('language_count', 1), count($enabled[1]), t('Language count is correct.')); + + + // Ensure we can't delete the English language. + $this->drupalGet('admin/config/regional/language/delete/en'); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertText(t('The English language cannot be deleted.'), t('Failed to delete English language.')); + } + +} + +/** + * Functional test for string translation and validation. + */ +class LocaleTranslationFunctionalTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'String translate, search and validate', + 'description' => 'Adds a new locale and translates its name. Checks the validation of translation strings and search results.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale'); + } + + /** + * Adds a language and tests string translation by users with the appropriate permissions. + */ + function testStringTranslation() { + global $base_url; + + // User to add and remove language. + $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages')); + // User to translate and delete string. + $translate_user = $this->drupalCreateUser(array('translate interface', 'access administration pages')); + // Code for the language. + $langcode = 'xx'; + // The English name for the language. This will be translated. + $name = $this->randomName(16); + // The native name for the language. + $native = $this->randomName(16); + // The domain prefix. + $prefix = $langcode; + // This is the language indicator on the translation search screen for + // untranslated strings. Copied straight from locale.inc. + $language_indicator = "$langcode "; + // This will be the translation of $name. + $translation = $this->randomName(16); + + // Add custom language. + $this->drupalLogin($admin_user); + $edit = array( + 'langcode' => $langcode, + 'name' => $name, + 'native' => $native, + 'prefix' => $prefix, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + // Add string. + t($name, array(), array('langcode' => $langcode)); + // Reset locale cache. + locale_reset(); + $this->assertText($langcode, t('Language code found.')); + $this->assertText($name, t('Name found.')); + $this->assertText($native, t('Native found.')); + // No t() here, we do not want to add this string to the database and it's + // surely not translated yet. + $this->assertText($native, t('Test language added.')); + $this->drupalLogout(); + + // Search for the name and translate it. + $this->drupalLogin($translate_user); + $search = array( + 'string' => $name, + 'language' => 'all', + 'translation' => 'all', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + // assertText() seems to remove the input field where $name always could be + // found, so this is not a false assert. See how assertNoText succeeds + // later. + $this->assertText($name, t('Search found the name.')); + $this->assertRaw($language_indicator, t('Name is untranslated.')); + // Assume this is the only result, given the random name. + $this->clickLink(t('edit')); + // We save the lid from the path. + $matches = array(); + preg_match('!admin/config/regional/translate/edit/(\d+)!', $this->getUrl(), $matches); + $lid = $matches[1]; + // No t() here, it's surely not translated yet. + $this->assertText($name, t('name found on edit screen.')); + $edit = array( + "translations[$langcode]" => $translation, + ); + $this->drupalPost(NULL, $edit, t('Save translations')); + $this->assertText(t('The string has been saved.'), t('The string has been saved.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertTrue($name != $translation && t($name, array(), array('langcode' => $langcode)) == $translation, t('t() works.')); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + // The indicator should not be here. + $this->assertNoRaw($language_indicator, t('String is translated.')); + + // Try to edit a non-existent string and ensure we're redirected correctly. + // Assuming we don't have 999,999 strings already. + $random_lid = 999999; + $this->drupalGet('admin/config/regional/translate/edit/' . $random_lid); + $this->assertText(t('String not found'), t('String not found.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->drupalLogout(); + + // Delete the language. + $this->drupalLogin($admin_user); + $path = 'admin/config/regional/language/delete/' . $langcode; + // This a confirm form, we do not need any fields changed. + $this->drupalPost($path, array(), t('Delete')); + // We need raw here because %locale will add HTML. + $this->assertRaw(t('The language %locale has been removed.', array('%locale' => $name)), t('The test language has been removed.')); + // Reload to remove $name. + $this->drupalGet($path); + $this->assertNoText($langcode, t('Language code not found.')); + $this->assertNoText($name, t('Name not found.')); + $this->assertNoText($native, t('Native not found.')); + $this->drupalLogout(); + + // Delete the string. + $this->drupalLogin($translate_user); + $search = array( + 'string' => $name, + 'language' => 'all', + 'translation' => 'all', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + // Assume this is the only result, given the random name. + $this->clickLink(t('delete')); + $this->assertText(t('Are you sure you want to delete the string'), t('"delete" link is correct.')); + // Delete the string. + $path = 'admin/config/regional/translate/delete/' . $lid; + $this->drupalGet($path); + // First test the 'cancel' link. + $this->clickLink(t('Cancel')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertRaw($name, t('The string was not deleted.')); + // Delete the name string. + $this->drupalPost('admin/config/regional/translate/delete/' . $lid, array(), t('Delete')); + $this->assertText(t('The string has been removed.'), t('The string has been removed message.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertNoText($name, t('Search now can not find the name.')); + } + + /* + * Adds a language and checks that the JavaScript translation files are + * properly created and rebuilt on deletion. + */ + function testJavaScriptTranslation() { + $user = $this->drupalCreateUser(array('translate interface', 'administer languages', 'access administration pages')); + $this->drupalLogin($user); + + $langcode = 'xx'; + // The English name for the language. This will be translated. + $name = $this->randomName(16); + // The native name for the language. + $native = $this->randomName(16); + // The domain prefix. + $prefix = $langcode; + + // Add custom language. + $edit = array( + 'langcode' => $langcode, + 'name' => $name, + 'native' => $native, + 'prefix' => $prefix, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + drupal_static_reset('language_list'); + + // Build the JavaScript translation file. + $this->drupalGet('admin/config/regional/translate/translate'); + + // Retrieve the id of the first string available in the {locales_source} + // table and translate it. + $query = db_select('locales_source', 'l'); + $query->addExpression('min(l.lid)', 'lid'); + $result = $query->condition('l.location', '%.js%', 'LIKE') + ->condition('l.textgroup', 'default') + ->execute(); + $url = 'admin/config/regional/translate/edit/' . $result->fetchObject()->lid; + $edit = array('translations['. $langcode .']' => $this->randomName()); + $this->drupalPost($url, $edit, t('Save translations')); + + // Trigger JavaScript translation parsing and building. + require_once DRUPAL_ROOT . '/core/includes/locale.inc'; + _locale_rebuild_js($langcode); + + // Retrieve the JavaScript translation hash code for the custom language to + // check that the translation file has been properly built. + $file = db_select('languages', 'l') + ->fields('l', array('javascript')) + ->condition('language', $langcode) + ->execute() + ->fetchObject(); + $js_file = 'public://' . variable_get('locale_js_directory', 'languages') . '/' . $langcode . '_' . $file->javascript . '.js'; + $this->assertTrue($result = file_exists($js_file), t('JavaScript file created: %file', array('%file' => $result ? $js_file : t('not found')))); + + // Test JavaScript translation rebuilding. + file_unmanaged_delete($js_file); + $this->assertTrue($result = !file_exists($js_file), t('JavaScript file deleted: %file', array('%file' => $result ? $js_file : t('found')))); + cache_clear_all(); + _locale_rebuild_js($langcode); + $this->assertTrue($result = file_exists($js_file), t('JavaScript file rebuilt: %file', array('%file' => $result ? $js_file : t('not found')))); + } + + /** + * Tests the validation of the translation input. + */ + function testStringValidation() { + global $base_url; + + // User to add language and strings. + $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'translate interface')); + $this->drupalLogin($admin_user); + $langcode = 'xx'; + // The English name for the language. This will be translated. + $name = $this->randomName(16); + // The native name for the language. + $native = $this->randomName(16); + // The domain prefix. + $prefix = $langcode; + // This is the language indicator on the translation search screen for + // untranslated strings. Copied straight from locale.inc. + $language_indicator = "$langcode "; + // These will be the invalid translations of $name. + $key = $this->randomName(16); + $bad_translations[$key] = "" . $key; + $key = $this->randomName(16); + $bad_translations[$key] = '' . $key; + $key = $this->randomName(16); + $bad_translations[$key] = '<' . $key; + $key = $this->randomName(16); + $bad_translations[$key] ="" . $key; + + // Add custom language. + $edit = array( + 'langcode' => $langcode, + 'name' => $name, + 'native' => $native, + 'prefix' => $prefix, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + // Add string. + t($name, array(), array('langcode' => $langcode)); + // Reset locale cache. + $search = array( + 'string' => $name, + 'language' => 'all', + 'translation' => 'all', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + // Find the edit path. + $content = $this->drupalGetContent(); + $this->assertTrue(preg_match('@(admin/config/regional/translate/edit/[0-9]+)@', $content, $matches), t('Found the edit path.')); + $path = $matches[0]; + foreach ($bad_translations as $key => $translation) { + $edit = array( + "translations[$langcode]" => $translation, + ); + $this->drupalPost($path, $edit, t('Save translations')); + // Check for a form error on the textarea. + $form_class = $this->xpath('//form[@id="locale-translate-edit-form"]//textarea/@class'); + $this->assertNotIdentical(FALSE, strpos($form_class[0], 'error'), t('The string was rejected as unsafe.')); + $this->assertNoText(t('The string has been saved.'), t('The string was not saved.')); + } + } + + /** + * Tests translation search form. + */ + function testStringSearch() { + global $base_url; + + // User to add and remove language. + $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages')); + // User to translate and delete string. + $translate_user = $this->drupalCreateUser(array('translate interface', 'access administration pages')); + + // Code for the language. + $langcode = 'xx'; + // The English name for the language. This will be translated. + $name = $this->randomName(16); + // The native name for the language. + $native = $this->randomName(16); + // The domain prefix. + $prefix = $langcode; + // This is the language indicator on the translation search screen for + // untranslated strings. Copied straight from locale.inc. + $language_indicator = "$langcode "; + // This will be the translation of $name. + $translation = $this->randomName(16); + + // Add custom language. + $this->drupalLogin($admin_user); + $edit = array( + 'langcode' => $langcode, + 'name' => $name, + 'native' => $native, + 'prefix' => $prefix, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + // Add string. + t($name, array(), array('langcode' => $langcode)); + // Reset locale cache. + locale_reset(); + $this->drupalLogout(); + + // Search for the name. + $this->drupalLogin($translate_user); + $search = array( + 'string' => $name, + 'language' => 'all', + 'translation' => 'all', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + // assertText() seems to remove the input field where $name always could be + // found, so this is not a false assert. See how assertNoText succeeds + // later. + $this->assertText($name, t('Search found the string.')); + + // Ensure untranslated string doesn't appear if searching on 'only + // translated strings'. + $search = array( + 'string' => $name, + 'language' => 'all', + 'translation' => 'translated', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertText(t('No strings available.'), t("Search didn't find the string.")); + + // Ensure untranslated string appears if searching on 'only untranslated + // strings'. + $search = array( + 'string' => $name, + 'language' => 'all', + 'translation' => 'untranslated', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertNoText(t('No strings available.'), t('Search found the string.')); + + // Add translation. + // Assume this is the only result, given the random name. + $this->clickLink(t('edit')); + // We save the lid from the path. + $matches = array(); + preg_match('!admin/config/regional/translate/edit/(\d)+!', $this->getUrl(), $matches); + $lid = $matches[1]; + $edit = array( + "translations[$langcode]" => $translation, + ); + $this->drupalPost(NULL, $edit, t('Save translations')); + + // Ensure translated string does appear if searching on 'only + // translated strings'. + $search = array( + 'string' => $translation, + 'language' => 'all', + 'translation' => 'translated', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertNoText(t('No strings available.'), t('Search found the translation.')); + + // Ensure translated source string doesn't appear if searching on 'only + // untranslated strings'. + $search = array( + 'string' => $name, + 'language' => 'all', + 'translation' => 'untranslated', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertText(t('No strings available.'), t("Search didn't find the source string.")); + + // Ensure translated string doesn't appear if searching on 'only + // untranslated strings'. + $search = array( + 'string' => $translation, + 'language' => 'all', + 'translation' => 'untranslated', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertText(t('No strings available.'), t("Search didn't find the translation.")); + + // Ensure translated string does appear if searching on the custom language. + $search = array( + 'string' => $translation, + 'language' => $langcode, + 'translation' => 'all', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertNoText(t('No strings available.'), t('Search found the translation.')); + + // Ensure translated string doesn't appear if searching on English. + $search = array( + 'string' => $translation, + 'language' => 'en', + 'translation' => 'all', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertText(t('No strings available.'), t("Search didn't find the translation.")); + + // Search for a string that isn't in the system. + $unavailable_string = $this->randomName(16); + $search = array( + 'string' => $unavailable_string, + 'language' => 'all', + 'translation' => 'all', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertText(t('No strings available.'), t("Search didn't find the invalid string.")); + } +} + +/** + * Functional tests for the import of translation files. + */ +class LocaleImportFunctionalTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Translation import', + 'description' => 'Tests the import of locale files.', + 'group' => 'Locale', + ); + } + + /** + * A user able to create languages and import translations. + */ + protected $admin_user = NULL; + + function setUp() { + parent::setUp('locale', 'locale_test'); + + $this->admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages')); + $this->drupalLogin($this->admin_user); + } + + /** + * Test import of standalone .po files. + */ + function testStandalonePoFile() { + // Try importing a .po file. + $this->importPoFile($this->getPoFile(), array( + 'langcode' => 'fr', + )); + + // The import should automatically create the corresponding language. + $this->assertRaw(t('The language %language has been created.', array('%language' => 'French')), t('The language has been automatically created.')); + + // The import should have created 7 strings. + $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 9, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); + + // This import should have saved plural forms to have 2 variants. + $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, t('Plural number initialized.')); + + // Ensure we were redirected correctly. + $this->assertEqual($this->getUrl(), url('admin/config/regional/translate', array('absolute' => TRUE)), t('Correct page redirection.')); + + + // Try importing a .po file with invalid tags in the default text group. + $this->importPoFile($this->getBadPoFile(), array( + 'langcode' => 'fr', + )); + + // The import should have created 1 string and rejected 2. + $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); + $skip_message = format_plural(2, 'One translation string was skipped because it contains disallowed HTML.', '@count translation strings were skipped because they contain disallowed HTML.'); + $this->assertRaw($skip_message, t('Unsafe strings were skipped.')); + + + // Try importing a .po file with invalid tags in a non default text group. + $this->importPoFile($this->getBadPoFile(), array( + 'langcode' => 'fr', + 'group' => 'custom', + )); + + // The import should have created 3 strings. + $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 3, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); + + + // Try importing a .po file which doesn't exist. + $name = $this->randomName(16); + $this->drupalPost('admin/config/regional/translate/import', array( + 'langcode' => 'fr', + 'files[file]' => $name, + 'group' => 'custom', + ), t('Import')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/import', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertText(t('File to import not found.'), t('File to import not found message.')); + + + // Try importing a .po file with overriding strings, and ensure existing + // strings are kept. + $this->importPoFile($this->getOverwritePoFile(), array( + 'langcode' => 'fr', + 'mode' => 1, // Existing strings are kept, only new strings are added. + )); + + // The import should have created 1 string. + $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); + // Ensure string wasn't overwritten. + $search = array( + 'string' => 'Montag', + 'language' => 'fr', + 'translation' => 'translated', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertText(t('No strings available.'), t('String not overwritten by imported string.')); + + // This import should not have changed number of plural forms. + $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, t('Plural numbers untouched.')); + + // Try importing a .po file with overriding strings, and ensure existing + // strings are overwritten. + $this->importPoFile($this->getOverwritePoFile(), array( + 'langcode' => 'fr', + 'mode' => 0, // Strings in the uploaded file replace existing ones, new ones are added. + )); + + // The import should have updated 2 strings. + $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The translation file was successfully imported.')); + // Ensure string was overwritten. + $search = array( + 'string' => 'Montag', + 'language' => 'fr', + 'translation' => 'translated', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertNoText(t('No strings available.'), t('String overwritten by imported string.')); + // This import should have changed number of plural forms. + $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 3, t('Plural numbers changed.')); + } + + /** + * Test automatic import of a module's translation files when a language is + * enabled. + */ + function testAutomaticModuleTranslationImportLanguageEnable() { + // Code for the language - manually set to match the test translation file. + $langcode = 'xx'; + // The English name for the language. + $name = $this->randomName(16); + // The native name for the language. + $native = $this->randomName(16); + // The domain prefix. + $prefix = $langcode; + + // Create a custom language. + $edit = array( + 'langcode' => $langcode, + 'name' => $name, + 'native' => $native, + 'prefix' => $prefix, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + + // Ensure the translation file was automatically imported when language was + // added. + $this->assertText(t('One translation file imported for the enabled modules.'), t('Language file automatically imported.')); + + // Ensure strings were successfully imported. + $search = array( + 'string' => 'lundi', + 'language' => $langcode, + 'translation' => 'translated', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertNoText(t('No strings available.'), t('String successfully imported.')); + } + + /** + * Test msgctxt context support. + */ + function testLanguageContext() { + // Try importing a .po file. + $this->importPoFile($this->getPoFileWithContext(), array( + 'langcode' => 'hr', + )); + + $this->assertIdentical(t('May', array(), array('langcode' => 'hr', 'context' => 'Long month name')), 'Svibanj', t('Long month name context is working.')); + $this->assertIdentical(t('May', array(), array('langcode' => 'hr')), 'Svi.', t('Default context is working.')); + } + + /** + * Test empty msgstr at end of .po file see #611786. + */ + function testEmptyMsgstr() { + $langcode = 'hu'; + + // Try importing a .po file. + $this->importPoFile($this->getPoFileWithMsgstr(), array( + 'langcode' => $langcode, + )); + + $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); + $this->assertIdentical(t('Operations', array(), array('langcode' => $langcode)), 'Műveletek', t('String imported and translated.')); + + // Try importing a .po file. + $this->importPoFile($this->getPoFileWithEmptyMsgstr(), array( + 'langcode' => $langcode, + 'mode' => 0, + )); + $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 0, '%delete' => 1)), t('The translation file was successfully imported.')); + // This is the language indicator on the translation search screen for + // untranslated strings. Copied straight from locale.inc. + $language_indicator = "$langcode "; + $str = "Operations"; + $search = array( + 'string' => $str, + 'language' => 'all', + 'translation' => 'all', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + // assertText() seems to remove the input field where $str always could be + // found, so this is not a false assert. + $this->assertText($str, t('Search found the string.')); + $this->assertRaw($language_indicator, t('String is untranslated again.')); + } + + /** + * Helper function: import a standalone .po file in a given language. + * + * @param $contents + * Contents of the .po file to import. + * @param $options + * Additional options to pass to the translation import form. + */ + function importPoFile($contents, array $options = array()) { + $name = tempnam('temporary://', "po_") . '.po'; + file_put_contents($name, $contents); + $options['files[file]'] = $name; + $this->drupalPost('admin/config/regional/translate/import', $options, t('Import')); + drupal_unlink($name); + } + + /** + * Helper function that returns a proper .po file. + */ + function getPoFile() { + return <<< EOF +msgid "" +msgstr "" +"Project-Id-Version: Drupal 7\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\\n" + +msgid "One sheep" +msgid_plural "@count sheep" +msgstr[0] "un mouton" +msgstr[1] "@count moutons" + +msgid "Monday" +msgstr "lundi" + +msgid "Tuesday" +msgstr "mardi" + +msgid "Wednesday" +msgstr "mercredi" + +msgid "Thursday" +msgstr "jeudi" + +msgid "Friday" +msgstr "vendredi" + +msgid "Saturday" +msgstr "samedi" + +msgid "Sunday" +msgstr "dimanche" +EOF; + } + + /** + * Helper function that returns a bad .po file. + */ + function getBadPoFile() { + return <<< EOF +msgid "" +msgstr "" +"Project-Id-Version: Drupal 7\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\\n" + +msgid "Save configuration" +msgstr "Enregistrer la configuration" + +msgid "edit" +msgstr "modifier" + +msgid "delete" +msgstr "supprimer" + +EOF; + } + + /** + * Helper function that returns a proper .po file, for testing overwriting + * existing translations. + */ + function getOverwritePoFile() { + return <<< EOF +msgid "" +msgstr "" +"Project-Id-Version: Drupal 7\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n" + +msgid "Monday" +msgstr "Montag" + +msgid "Day" +msgstr "Jour" +EOF; + } + + /** + * Helper function that returns a .po file with context. + */ + function getPoFileWithContext() { + // Croatian (code hr) is one the the languages that have a different + // form for the full name and the abbreviated name for the month May. + return <<< EOF +msgid "" +msgstr "" +"Project-Id-Version: Drupal 7\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n" + +msgctxt "Long month name" +msgid "May" +msgstr "Svibanj" + +msgid "May" +msgstr "Svi." +EOF; + } + + /** + * Helper function that returns a .po file with an empty last item. + */ + function getPoFileWithEmptyMsgstr() { + return <<< EOF +msgid "" +msgstr "" +"Project-Id-Version: Drupal 7\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\\n" + +msgid "Operations" +msgstr "" + +EOF; + } + /** + * Helper function that returns a .po file with an empty last item. + */ + function getPoFileWithMsgstr() { + return <<< EOF +msgid "" +msgstr "" +"Project-Id-Version: Drupal 7\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\\n" + +msgid "Operations" +msgstr "Műveletek" + +msgid "Will not appear in Drupal core, so we can ensure the test passes" +msgstr "" + +EOF; + } + +} + +/** + * Functional tests for the export of translation files. + */ +class LocaleExportFunctionalTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Translation export', + 'description' => 'Tests the exportation of locale files.', + 'group' => 'Locale', + ); + } + + /** + * A user able to create languages and export translations. + */ + protected $admin_user = NULL; + + function setUp() { + parent::setUp('locale', 'locale_test'); + + $this->admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages')); + $this->drupalLogin($this->admin_user); + } + + /** + * Test exportation of translations. + */ + function testExportTranslation() { + // First import some known translations. + // This will also automatically enable the 'fr' language. + $name = tempnam('temporary://', "po_") . '.po'; + file_put_contents($name, $this->getPoFile()); + $this->drupalPost('admin/config/regional/translate/import', array( + 'langcode' => 'fr', + 'files[file]' => $name, + ), t('Import')); + drupal_unlink($name); + + // Get the French translations. + $this->drupalPost('admin/config/regional/translate/export', array( + 'langcode' => 'fr', + ), t('Export')); + + // Ensure we have a translation file. + $this->assertRaw('# French translation of Drupal', t('Exported French translation file.')); + // Ensure our imported translations exist in the file. + $this->assertRaw('msgstr "lundi"', t('French translations present in exported file.')); + } + + /** + * Test exportation of translation template file. + */ + function testExportTranslationTemplateFile() { + // Get the translation template file. + // There are two 'Export' buttons on this page, but it somehow works. It'd + // be better if we could use the submit button id like documented but that + // doesn't work. + $this->drupalPost('admin/config/regional/translate/export', array(), t('Export')); + // Ensure we have a translation file. + $this->assertRaw('# LANGUAGE translation of PROJECT', t('Exported translation template file.')); + } + + /** + * Helper function that returns a proper .po file. + */ + function getPoFile() { + return <<< EOF +msgid "" +msgstr "" +"Project-Id-Version: Drupal 6\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\\n" + +msgid "Monday" +msgstr "lundi" +EOF; + } + +} + +/** + * Tests for the st() function. + */ +class LocaleInstallTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'String translation using st()', + 'description' => 'Tests that st() works like t().', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale'); + + // st() lives in install.inc, so ensure that it is loaded for all tests. + require_once DRUPAL_ROOT . '/core/includes/install.inc'; + } + + /** + * Verify that function signatures of t() and st() are equal. + */ + function testFunctionSignatures() { + $reflector_t = new ReflectionFunction('t'); + $reflector_st = new ReflectionFunction('st'); + $this->assertEqual($reflector_t->getParameters(), $reflector_st->getParameters(), t('Function signatures of t() and st() are equal.')); + } +} + +/** + * Locale uninstall with English UI functional test. + */ +class LocaleUninstallFunctionalTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Locale uninstall (EN)', + 'description' => 'Tests the uninstall process using the built-in UI language.', + 'group' => 'Locale', + ); + } + + /** + * The default language set for the UI before uninstall. + */ + protected $language; + + function setUp() { + parent::setUp('locale'); + $this->language = 'en'; + } + + /** + * Check if the values of the Locale variables are correct after uninstall. + */ + function testUninstallProcess() { + $locale_module = array('locale'); + + // Add a new language and optionally set it as default. + require_once DRUPAL_ROOT . '/core/includes/locale.inc'; + locale_add_language('fr', 'French', 'Français', LANGUAGE_LTR, '', '', TRUE, $this->language == 'fr'); + + // Check the UI language. + drupal_language_initialize(); + global $language; + $this->assertEqual($language->language, $this->language, t('Current language: %lang', array('%lang' => $language->language))); + + // Enable multilingual workflow option for articles. + variable_set('language_content_type_article', 1); + + // Change JavaScript translations directory. + variable_set('locale_js_directory', 'js_translations'); + + // Build the JavaScript translation file for French. + $user = $this->drupalCreateUser(array('translate interface', 'access administration pages')); + $this->drupalLogin($user); + $this->drupalGet('admin/config/regional/translate/translate'); + $string = db_query('SELECT min(lid) AS lid FROM {locales_source} WHERE location LIKE :location AND textgroup = :textgroup', array( + ':location' => '%.js%', + ':textgroup' => 'default', + ))->fetchObject(); + $edit = array('translations[fr]' => 'french translation'); + $this->drupalPost('admin/config/regional/translate/edit/' . $string->lid, $edit, t('Save translations')); + _locale_rebuild_js('fr'); + $file = db_query('SELECT javascript FROM {languages} WHERE language = :language', array(':language' => 'fr'))->fetchObject(); + $js_file = 'public://' . variable_get('locale_js_directory', 'languages') . '/fr_' . $file->javascript . '.js'; + $this->assertTrue($result = file_exists($js_file), t('JavaScript file created: %file', array('%file' => $result ? $js_file : t('none')))); + + // Disable string caching. + variable_set('locale_cache_strings', 0); + + // Change language negotiation options. + drupal_load('module', 'locale'); + variable_set('language_types', drupal_language_types() + array('language_custom' => TRUE)); + variable_set('language_negotiation_' . LANGUAGE_TYPE_INTERFACE, locale_language_negotiation_info()); + variable_set('language_negotiation_' . LANGUAGE_TYPE_CONTENT, locale_language_negotiation_info()); + variable_set('language_negotiation_' . LANGUAGE_TYPE_URL, locale_language_negotiation_info()); + + // Change language providers settings. + variable_set('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX); + variable_set('locale_language_negotiation_session_param', TRUE); + + // Uninstall Locale. + module_disable($locale_module); + drupal_uninstall_modules($locale_module); + + // Visit the front page. + $this->drupalGet(''); + + // Check the init language logic. + drupal_language_initialize(); + $this->assertEqual($language->language, 'en', t('Language after uninstall: %lang', array('%lang' => $language->language))); + + // Check JavaScript files deletion. + $this->assertTrue($result = !file_exists($js_file), t('JavaScript file deleted: %file', array('%file' => $result ? $js_file : t('found')))); + + // Check language count. + $language_count = variable_get('language_count', 1); + $this->assertEqual($language_count, 1, t('Language count: %count', array('%count' => $language_count))); + + // Check language negotiation. + require_once DRUPAL_ROOT . '/core/includes/language.inc'; + $this->assertTrue(count(language_types()) == count(drupal_language_types()), t('Language types reset')); + $language_negotiation = language_negotiation_get(LANGUAGE_TYPE_INTERFACE) == LANGUAGE_NEGOTIATION_DEFAULT; + $this->assertTrue($language_negotiation, t('Interface language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set')))); + $language_negotiation = language_negotiation_get(LANGUAGE_TYPE_CONTENT) == LANGUAGE_NEGOTIATION_DEFAULT; + $this->assertTrue($language_negotiation, t('Content language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set')))); + $language_negotiation = language_negotiation_get(LANGUAGE_TYPE_URL) == LANGUAGE_NEGOTIATION_DEFAULT; + $this->assertTrue($language_negotiation, t('URL language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set')))); + + // Check language providers settings. + $this->assertFalse(variable_get('locale_language_negotiation_url_part', FALSE), t('URL language provider indicator settings cleared.')); + $this->assertFalse(variable_get('locale_language_negotiation_session_param', FALSE), t('Visit language provider settings cleared.')); + + // Check JavaScript parsed. + $javascript_parsed_count = count(variable_get('javascript_parsed', array())); + $this->assertEqual($javascript_parsed_count, 0, t('JavaScript parsed count: %count', array('%count' => $javascript_parsed_count))); + + // Check multilingual workflow option for articles. + $multilingual = variable_get('language_content_type_article', 0); + $this->assertEqual($multilingual, 0, t('Multilingual workflow option: %status', array('%status' => t($multilingual ? 'enabled': 'disabled')))); + + // Check JavaScript translations directory. + $locale_js_directory = variable_get('locale_js_directory', 'languages'); + $this->assertEqual($locale_js_directory, 'languages', t('JavaScript translations directory: %dir', array('%dir' => $locale_js_directory))); + + // Check string caching. + $locale_cache_strings = variable_get('locale_cache_strings', 1); + $this->assertEqual($locale_cache_strings, 1, t('String caching: %status', array('%status' => t($locale_cache_strings ? 'enabled': 'disabled')))); + } +} + +/** + * Locale uninstall with French UI functional test. + * + * Because this class extends LocaleUninstallFunctionalTest, it doesn't require a new + * test of its own. Rather, it switches the default UI language in setUp and then + * runs the testUninstallProcess (which it inherits from LocaleUninstallFunctionalTest) + * to test with this new language. + */ +class LocaleUninstallFrenchFunctionalTest extends LocaleUninstallFunctionalTest { + public static function getInfo() { + return array( + 'name' => 'Locale uninstall (FR)', + 'description' => 'Tests the uninstall process using French as interface language.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp(); + $this->language = 'fr'; + } +} + + +/** + * Functional tests for the language switching feature. + */ +class LocaleLanguageSwitchingFunctionalTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Language switching', + 'description' => 'Tests for the language switching feature.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale'); + + // Create and login user. + $admin_user = $this->drupalCreateUser(array('administer blocks', 'administer languages', 'translate interface', 'access administration pages')); + $this->drupalLogin($admin_user); + } + + /** + * Functional tests for the language switcher block. + */ + function testLanguageBlock() { + // Enable the language switching block. + $language_type = LANGUAGE_TYPE_INTERFACE; + $edit = array( + "blocks[locale_{$language_type}][region]" => 'sidebar_first', + ); + $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); + + // Add language. + $edit = array( + 'langcode' => 'fr', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + + // Enable URL language detection and selection. + $edit = array('language[enabled][locale-url]' => '1'); + $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); + + // Assert that the language switching block is displayed on the frontpage. + $this->drupalGet(''); + $this->assertText(t('Languages'), t('Language switcher block found.')); + + // Assert that only the current language is marked as active. + list($language_switcher) = $this->xpath('//div[@id=:id]/div[@class="content"]', array(':id' => 'block-locale-' . $language_type)); + $links = array( + 'active' => array(), + 'inactive' => array(), + ); + $anchors = array( + 'active' => array(), + 'inactive' => array(), + ); + foreach ($language_switcher->ul->li as $link) { + $classes = explode(" ", (string) $link['class']); + list($language) = array_intersect($classes, array('en', 'fr')); + if (in_array('active', $classes)) { + $links['active'][] = $language; + } + else { + $links['inactive'][] = $language; + } + $anchor_classes = explode(" ", (string) $link->a['class']); + if (in_array('active', $anchor_classes)) { + $anchors['active'][] = $language; + } + else { + $anchors['inactive'][] = $language; + } + } + $this->assertIdentical($links, array('active' => array('en'), 'inactive' => array('fr')), t('Only the current language list item is marked as active on the language switcher block.')); + $this->assertIdentical($anchors, array('active' => array('en'), 'inactive' => array('fr')), t('Only the current language anchor is marked as active on the language switcher block.')); + } +} + +/** + * Functional tests for a user's ability to change their default language. + */ +class LocaleUserLanguageFunctionalTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'User language settings', + 'description' => "Tests user's ability to change their default language.", + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale'); + } + + /** + * Test if user can change their default language. + */ + function testUserLanguageConfiguration() { + global $base_url; + + // User to add and remove language. + $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages')); + // User to change their default language. + $web_user = $this->drupalCreateUser(); + + // Add custom language. + $this->drupalLogin($admin_user); + // Code for the language. + $langcode = 'xx'; + // The English name for the language. + $name = $this->randomName(16); + // The native name for the language. + $native = $this->randomName(16); + // The domain prefix. + $prefix = 'xx'; + $edit = array( + 'langcode' => $langcode, + 'name' => $name, + 'native' => $native, + 'prefix' => $prefix, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + + // Add custom language and disable it. + // Code for the language. + $langcode_disabled = 'xx-yy'; + // The English name for the language. This will be translated. + $name_disabled = $this->randomName(16); + // The native name for the language. + $native_disabled = $this->randomName(16); + // The domain prefix. + $prefix_disabled = $langcode_disabled; + $edit = array( + 'langcode' => $langcode_disabled, + 'name' => $name_disabled, + 'native' => $native_disabled, + 'prefix' => $prefix_disabled, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + // Disable the language. + $edit = array( + 'enabled[' . $langcode_disabled . ']' => FALSE, + ); + $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration')); + $this->drupalLogout(); + + // Login as normal user and edit account settings. + $this->drupalLogin($web_user); + $path = 'user/' . $web_user->uid . '/edit'; + $this->drupalGet($path); + // Ensure language settings fieldset is available. + $this->assertText(t('Language settings'), t('Language settings available.')); + // Ensure custom language is present. + $this->assertText($name, t('Language present on form.')); + // Ensure disabled language isn't present. + $this->assertNoText($name_disabled, t('Disabled language not present on form.')); + // Switch to our custom language. + $edit = array( + 'language' => $langcode, + ); + $this->drupalPost($path, $edit, t('Save')); + // Ensure form was submitted successfully. + $this->assertText(t('The changes have been saved.'), t('Changes were saved.')); + // Check if language was changed. + $elements = $this->xpath('//input[@id=:id]', array(':id' => 'edit-language-' . $langcode)); + $this->assertTrue(isset($elements[0]) && !empty($elements[0]['checked']), t('Default language successfully updated.')); + + $this->drupalLogout(); + } +} + +/** + * Functional test for language handling during user creation. + */ +class LocaleUserCreationTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'User creation', + 'description' => 'Tests whether proper language is stored for new users and access to language selector.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale'); + variable_set('user_register', USER_REGISTER_VISITORS); + } + + /** + * Functional test for language handling during user creation. + */ + function testLocalUserCreation() { + // User to add and remove language and create new users. + $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'administer users')); + $this->drupalLogin($admin_user); + + // Add predefined language. + $langcode = 'fr'; + $edit = array( + 'langcode' => 'fr', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + $this->assertText($langcode, t('Language added successfully.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + + // Set language negotiation. + $edit = array( + 'language[enabled][locale-url]' => TRUE, + ); + $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); + $this->assertText(t('Language negotiation configuration saved.'), t('Set language negotiation.')); + + // Check if the language selector is available on admin/people/create and + // set to the currently active language. + $this->drupalGet($langcode . '/admin/people/create'); + $this->assertFieldChecked("edit-language-$langcode", t('Global language set in the language selector.')); + + // Create a user with the admin/people/create form and check if the correct + // language is set. + $username = $this->randomName(10); + $edit = array( + 'name' => $username, + 'mail' => $this->randomName(4) . '@example.com', + 'pass[pass1]' => $username, + 'pass[pass2]' => $username, + ); + + $this->drupalPost($langcode . '/admin/people/create', $edit, t('Create new account')); + + $user = user_load_by_name($username); + $this->assertEqual($user->language, $langcode, t('New user has correct language set.')); + + // Register a new user and check if the language selector is hidden. + $this->drupalLogout(); + + $this->drupalGet($langcode . '/user/register'); + $this->assertNoFieldByName('language[fr]', t('Language selector is not accessible.')); + + $username = $this->randomName(10); + $edit = array( + 'name' => $username, + 'mail' => $this->randomName(4) . '@example.com', + ); + + $this->drupalPost($langcode . '/user/register', $edit, t('Create new account')); + + $user = user_load_by_name($username); + $this->assertEqual($user->language, $langcode, t('New user has correct language set.')); + + // Test if the admin can use the language selector and if the + // correct language is was saved. + $user_edit = $langcode . '/user/' . $user->uid . '/edit'; + + $this->drupalLogin($admin_user); + $this->drupalGet($user_edit); + $this->assertFieldChecked("edit-language-$langcode", t('Language selector is accessible and correct language is selected.')); + + // Set pass_raw so we can login the new user. + $user->pass_raw = $this->randomName(10); + $edit = array( + 'pass[pass1]' => $user->pass_raw, + 'pass[pass2]' => $user->pass_raw, + ); + + $this->drupalPost($user_edit, $edit, t('Save')); + + $this->drupalLogin($user); + $this->drupalGet($user_edit); + $this->assertFieldChecked("edit-language-$langcode", t('Language selector is accessible and correct language is selected.')); + } +} + +/** + * Functional tests for configuring a different path alias per language. + */ +class LocalePathFunctionalTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Path language settings', + 'description' => 'Checks you can configure a language for individual url aliases.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale', 'path'); + } + + /** + * Test if a language can be associated with a path alias. + */ + function testPathLanguageConfiguration() { + global $base_url; + + // User to add and remove language. + $admin_user = $this->drupalCreateUser(array('administer languages', 'create page content', 'administer url aliases', 'create url aliases', 'access administration pages')); + + // Add custom language. + $this->drupalLogin($admin_user); + // Code for the language. + $langcode = 'xx'; + // The English name for the language. + $name = $this->randomName(16); + // The native name for the language. + $native = $this->randomName(16); + // The domain prefix. + $prefix = $langcode; + $edit = array( + 'langcode' => $langcode, + 'name' => $name, + 'native' => $native, + 'prefix' => $prefix, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + + // Check that the "xx" front page is not available when path prefixes are + // not enabled yet. + $this->drupalPost('admin/config/regional/language/configure', array(), t('Save settings')); + $this->drupalGet($prefix); + $this->assertResponse(404, t('The "xx" front page is not available yet.')); + + // Enable URL language detection and selection. + $edit = array('language[enabled][locale-url]' => 1); + $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); + + // Create a node. + $node = $this->drupalCreateNode(array('type' => 'page')); + + // Create a path alias in default language (English). + $path = 'admin/config/search/path/add'; + $english_path = $this->randomName(8); + $edit = array( + 'source' => 'node/' . $node->nid, + 'alias' => $english_path, + 'language' => 'en', + ); + $this->drupalPost($path, $edit, t('Save')); + + // Create a path alias in new custom language. + $custom_language_path = $this->randomName(8); + $edit = array( + 'source' => 'node/' . $node->nid, + 'alias' => $custom_language_path, + 'language' => $langcode, + ); + $this->drupalPost($path, $edit, t('Save')); + + // Confirm English language path alias works. + $this->drupalGet($english_path); + $this->assertText($node->title, t('English alias works.')); + + // Confirm custom language path alias works. + $this->drupalGet($prefix . '/' . $custom_language_path); + $this->assertText($node->title, t('Custom language alias works.')); + + // Create a custom path. + $custom_path = $this->randomName(8); + + // Check priority of language for alias by source path. + $edit = array( + 'source' => 'node/' . $node->nid, + 'alias' => $custom_path, + 'language' => LANGUAGE_NONE, + ); + path_save($edit); + $lookup_path = drupal_lookup_path('alias', 'node/' . $node->nid, 'en'); + $this->assertEqual($english_path, $lookup_path, t('English language alias has priority.')); + // Same check for language 'xx'. + $lookup_path = drupal_lookup_path('alias', 'node/' . $node->nid, $prefix); + $this->assertEqual($custom_language_path, $lookup_path, t('Custom language alias has priority.')); + path_delete($edit); + + // Create language nodes to check priority of aliases. + $first_node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); + $second_node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); + + // Assign a custom path alias to the first node with the English language. + $edit = array( + 'source' => 'node/' . $first_node->nid, + 'alias' => $custom_path, + 'language' => 'en', + ); + path_save($edit); + + // Assign a custom path alias to second node with LANGUAGE_NONE. + $edit = array( + 'source' => 'node/' . $second_node->nid, + 'alias' => $custom_path, + 'language' => LANGUAGE_NONE, + ); + path_save($edit); + + // Test that both node titles link to our path alias. + $this->drupalGet(''); + $custom_path_url = base_path() . (variable_get('clean_url', 0) ? $custom_path : '?q=' . $custom_path); + $elements = $this->xpath('//a[@href=:href and .=:title]', array(':href' => $custom_path_url, ':title' => $first_node->title)); + $this->assertTrue(!empty($elements), t('First node links to the path alias.')); + $elements = $this->xpath('//a[@href=:href and .=:title]', array(':href' => $custom_path_url, ':title' => $second_node->title)); + $this->assertTrue(!empty($elements), t('Second node links to the path alias.')); + + // Confirm that the custom path leads to the first node. + $this->drupalGet($custom_path); + $this->assertText($first_node->title, t('Custom alias returns first node.')); + + // Confirm that the custom path with prefix leads to the second node. + $this->drupalGet($prefix . '/' . $custom_path); + $this->assertText($second_node->title, t('Custom alias with prefix returns second node.')); + } +} + +/** + * Functional tests for multilingual support on nodes. + */ +class LocaleContentFunctionalTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Content language settings', + 'description' => 'Checks you can enable multilingual support on content types and configure a language for a node.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale'); + } + + /** + * Test if a content type can be set to multilingual and language setting is + * present on node add and edit forms. + */ + function testContentTypeLanguageConfiguration() { + global $base_url; + + // User to add and remove language. + $admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages')); + // User to create a node. + $web_user = $this->drupalCreateUser(array('create article content', 'create page content', 'edit any page content')); + + // Add custom language. + $this->drupalLogin($admin_user); + // Code for the language. + $langcode = 'xx'; + // The English name for the language. + $name = $this->randomName(16); + // The native name for the language. + $native = $this->randomName(16); + // The domain prefix. + $prefix = $langcode; + $edit = array( + 'langcode' => $langcode, + 'name' => $name, + 'native' => $native, + 'prefix' => $prefix, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + + // Add disabled custom language. + // Code for the language. + $langcode_disabled = 'xx-yy'; + // The English name for the language. + $name_disabled = $this->randomName(16); + // The native name for the language. + $native_disabled = $this->randomName(16); + // The domain prefix. + $prefix_disabled = $langcode_disabled; + $edit = array( + 'langcode' => $langcode_disabled, + 'name' => $name_disabled, + 'native' => $native_disabled, + 'prefix' => $prefix_disabled, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + // Disable second custom language. + $path = 'admin/config/regional/language'; + $edit = array( + 'enabled[' . $langcode_disabled . ']' => FALSE, + ); + $this->drupalPost($path, $edit, t('Save configuration')); + + // Set "Basic page" content type to use multilingual support. + $this->drupalGet('admin/structure/types/manage/page'); + $this->assertText(t('Multilingual support'), t('Multilingual support fieldset present on content type configuration form.')); + $edit = array( + 'language_content_type' => 1, + ); + $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type')); + $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), t('Basic page content type has been updated.')); + $this->drupalLogout(); + + // Verify language selection is not present on add article form. + $this->drupalLogin($web_user); + $this->drupalGet('node/add/article'); + // Verify language select list is not present. + $this->assertNoFieldByName('language', NULL, t('Language select not present on add article form.')); + + // Verify language selection appears on add "Basic page" form. + $this->drupalGet('node/add/page'); + // Verify language select list is present. + $this->assertFieldByName('language', NULL, t('Language select present on add Basic page form.')); + // Ensure enabled language appears. + $this->assertText($name, t('Enabled language present.')); + // Ensure disabled language doesn't appear. + $this->assertNoText($name_disabled, t('Disabled language not present.')); + + // Create "Basic page" content. + $node_title = $this->randomName(); + $node_body = $this->randomName(); + $edit = array( + 'type' => 'page', + 'title' => $node_title, + 'body' => array($langcode => array(array('value' => $node_body))), + 'language' => $langcode, + ); + $node = $this->drupalCreateNode($edit); + // Edit the content and ensure correct language is selected. + $path = 'node/' . $node->nid . '/edit'; + $this->drupalGet($path); + $this->assertRaw('', t('Correct language selected.')); + // Ensure we can change the node language. + $edit = array( + 'language' => 'en', + ); + $this->drupalPost($path, $edit, t('Save')); + $this->assertRaw(t('%title has been updated.', array('%title' => $node_title)), t('Basic page content updated.')); + + $this->drupalLogout(); + } +} + +/** + * Test UI language negotiation + * 1. URL (PATH) > DEFAULT + * UI Language base on URL prefix, browser language preference has no + * influence: + * admin/config + * UI in site default language + * zh-hans/admin/config + * UI in Chinese + * blah-blah/admin/config + * 404 + * 2. URL (PATH) > BROWSER > DEFAULT + * admin/config + * UI in user's browser language preference if the site has that + * language enabled, if not, the default language + * zh-hans/admin/config + * UI in Chinese + * blah-blah/admin/config + * 404 + * 3. URL (DOMAIN) > DEFAULT + * http://example.com/admin/config + * UI language in site default + * http://example.cn/admin/config + * UI language in Chinese + */ +class LocaleUILanguageNegotiationTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'UI language negotiation', + 'description' => 'Test UI language switching by url path prefix and domain.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale', 'locale_test'); + require_once DRUPAL_ROOT . '/core/includes/language.inc'; + drupal_load('module', 'locale'); + $admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages', 'administer blocks')); + $this->drupalLogin($admin_user); + } + + /** + * Tests for language switching by URL path. + */ + function testUILanguageNegotiation() { + // A few languages to switch to. + // This one is unknown, should get the default lang version. + $language_unknown = 'blah-blah'; + // For testing browser lang preference. + $language_browser_fallback = 'vi'; + // For testing path prefix. + $language = 'zh-hans'; + // For setting browser language preference to 'vi'. + $http_header_browser_fallback = array("Accept-Language: $language_browser_fallback;q=1"); + // For setting browser language preference to some unknown. + $http_header_blah = array("Accept-Language: blah;q=1"); + + // This domain should switch the UI to Chinese. + $language_domain = 'example.cn'; + + // Setup the site languages by installing two languages. + require_once DRUPAL_ROOT . '/core/includes/locale.inc'; + locale_add_language($language_browser_fallback); + locale_add_language($language); + + // We will look for this string in the admin/config screen to see if the + // corresponding translated string is shown. + $default_string = 'Configure languages for content and the user interface'; + + // Set the default language in order for the translated string to be registered + // into database when seen by t(). Without doing this, our target string + // is for some reason not found when doing translate search. This might + // be some bug. + drupal_static_reset('language_list'); + $languages = language_list('enabled'); + variable_set('language_default', $languages[1]['vi']); + // First visit this page to make sure our target string is searchable. + $this->drupalGet('admin/config'); + // Now the t()'ed string is in db so switch the language back to default. + variable_del('language_default'); + + // Translate the string. + $language_browser_fallback_string = "In $language_browser_fallback In $language_browser_fallback In $language_browser_fallback"; + $language_string = "In $language In $language In $language"; + // Do a translate search of our target string. + $edit = array( 'string' => $default_string); + $this->drupalPost('admin/config/regional/translate/translate', $edit, t('Filter')); + // Should find the string and now click edit to post translated string. + $this->clickLink('edit'); + $edit = array( + "translations[$language_browser_fallback]" => $language_browser_fallback_string, + "translations[$language]" => $language_string, + ); + $this->drupalPost(NULL, $edit, t('Save translations')); + + // Configure URL language rewrite. + variable_set('locale_language_negotiation_url_type', LANGUAGE_TYPE_INTERFACE); + + $tests = array( + // Default, browser preference should have no influence. + array( + 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT), + 'path' => 'admin/config', + 'expect' => $default_string, + 'http_header' => $http_header_browser_fallback, + 'message' => 'URL (PATH) > DEFAULT: no language prefix, UI language is default and the browser language preference setting is not used.', + ), + // Language prefix. + array( + 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT), + 'path' => "$language/admin/config", + 'expect' => $language_string, + 'http_header' => $http_header_browser_fallback, + 'message' => 'URL (PATH) > DEFAULT: with language prefix, UI language is switched based on path prefix', + ), + // Default, go by browser preference. + array( + 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_BROWSER), + 'path' => 'admin/config', + 'expect' => $language_browser_fallback_string, + 'http_header' => $http_header_browser_fallback, + 'message' => 'URL (PATH) > BROWSER: no language prefix, UI language is determined by browser language preference', + ), + // Prefix, switch to the language. + array( + 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_BROWSER), + 'path' => "$language/admin/config", + 'expect' => $language_string, + 'http_header' => $http_header_browser_fallback, + 'message' => 'URL (PATH) > BROWSER: with langage prefix, UI language is based on path prefix', + ), + // Default, browser language preference is not one of site's lang. + array( + 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_BROWSER, LANGUAGE_NEGOTIATION_DEFAULT), + 'path' => 'admin/config', + 'expect' => $default_string, + 'http_header' => $http_header_blah, + 'message' => 'URL (PATH) > BROWSER > DEFAULT: no language prefix and browser language preference set to unknown language should use default language', + ), + ); + + foreach ($tests as $test) { + $this->runTest($test); + } + + // Unknown language prefix should return 404. + variable_set('language_negotiation_' . LANGUAGE_TYPE_INTERFACE, locale_language_negotiation_info()); + $this->drupalGet("$language_unknown/admin/config", array(), $http_header_browser_fallback); + $this->assertResponse(404, "Unknown language path prefix should return 404"); + + // Setup for domain negotiation, first configure the language to have domain + // URL. + $edit = array('prefix' => '', 'domain' => "http://$language_domain"); + $this->drupalPost("admin/config/regional/language/edit/$language", $edit, t('Save language')); + // Set the site to use domain language negotiation. + + $tests = array( + // Default domain, browser preference should have no influence. + array( + 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT), + 'locale_language_negotiation_url_part' => LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN, + 'path' => 'admin/config', + 'expect' => $default_string, + 'http_header' => $http_header_browser_fallback, + 'message' => 'URL (DOMAIN) > DEFAULT: default domain should get default language', + ), + // Language domain specific URL, we set the $_SERVER['HTTP_HOST'] in + // locale_test.module hook_boot() to simulate this. + array( + 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT), + 'locale_language_negotiation_url_part' => LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN, + 'locale_test_domain' => $language_domain, + 'path' => 'admin/config', + 'expect' => $language_string, + 'http_header' => $http_header_browser_fallback, + 'message' => 'URL (DOMAIN) > DEFAULT: domain example.cn should switch to Chinese', + ), + ); + + foreach ($tests as $test) { + $this->runTest($test); + } + } + + private function runTest($test) { + if (!empty($test['language_negotiation'])) { + $negotiation = array_flip($test['language_negotiation']); + language_negotiation_set(LANGUAGE_TYPE_INTERFACE, $negotiation); + } + if (!empty($test['locale_language_negotiation_url_part'])) { + variable_set('locale_language_negotiation_url_part', $test['locale_language_negotiation_url_part']); + } + if (!empty($test['locale_test_domain'])) { + variable_set('locale_test_domain', $test['locale_test_domain']); + } + $this->drupalGet($test['path'], array(), $test['http_header']); + $this->assertText($test['expect'], $test['message']); + } + + /** + * Test URL language detection when the requested URL has no language. + */ + function testUrlLanguageFallback() { + // Add the Italian language. + $language_browser_fallback = 'it'; + locale_add_language($language_browser_fallback); + $languages = language_list(); + + // Enable the path prefix for the default language: this way any unprefixed + // URL must have a valid fallback value. + $edit = array('prefix' => 'en'); + $this->drupalPost('admin/config/regional/language/edit/en', $edit, t('Save language')); + + // Enable browser and URL language detection. + $edit = array( + 'language[enabled][locale-browser]' => TRUE, + 'language[enabled][locale-url]' => TRUE, + 'language[weight][locale-browser]' => -8, + 'language[weight][locale-url]' => -10, + ); + $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); + $this->drupalGet('admin/config/regional/language/configure'); + + // Enable the language switcher block. + $edit = array('blocks[locale_language][region]' => 'sidebar_first'); + $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); + + // Access the front page without specifying any valid URL language prefix + // and having as browser language preference a non-default language. + $http_header = array("Accept-Language: $language_browser_fallback;q=1"); + $this->drupalGet('', array(), $http_header); + + // Check that the language switcher active link matches the given browser + // language. + $args = array(':url' => base_path() . (!empty($GLOBALS['conf']['clean_url']) ? $language_browser_fallback : "?q=$language_browser_fallback")); + $fields = $this->xpath('//div[@id="block-locale-language"]//a[@class="language-link active" and @href=:url]', $args); + $this->assertTrue($fields[0] == $languages[$language_browser_fallback]->native, t('The browser language is the URL active language')); + + // Check that URLs are rewritten using the given browser language. + $fields = $this->xpath('//div[@id="site-name"]//a[@rel="home" and @href=:url]//span', $args); + $this->assertTrue($fields[0] == 'Drupal', t('URLs are rewritten using the browser language.')); + } +} + +/** + * Test that URL rewriting works as expected. + */ +class LocaleUrlRewritingTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'URL rewriting', + 'description' => 'Test that URL rewriting works as expected.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale'); + + // Create and login user. + $this->web_user = $this->drupalCreateUser(array('administer languages', 'access administration pages')); + $this->drupalLogin($this->web_user); + + // Install French language. + $edit = array(); + $edit['langcode'] = 'fr'; + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + + // Install Italian language. + $edit = array(); + $edit['langcode'] = 'it'; + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + + // Disable Italian language. + $edit = array('enabled[it]' => FALSE); + $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration')); + + // Enable URL language detection and selection. + $edit = array('language[enabled][locale-url]' => 1); + $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); + + // Reset static caching. + drupal_static_reset('language_list'); + drupal_static_reset('locale_url_outbound_alter'); + drupal_static_reset('locale_language_url_rewrite_url'); + } + + /** + * Check that disabled or non-installed languages are not considered. + */ + function testUrlRewritingEdgeCases() { + // Check URL rewriting with a disabled language. + $languages = language_list(); + $this->checkUrl($languages['it'], t('Path language is ignored if language is disabled.'), t('URL language negotiation does not work with disabled languages')); + + // Check URL rewriting with a non-installed language. + $non_existing = language_default(); + $non_existing->language = $this->randomName(); + $non_existing->prefix = $this->randomName(); + $this->checkUrl($non_existing, t('Path language is ignored if language is not installed.'), t('URL language negotiation does not work with non-installed languages')); + } + + /** + * Check URL rewriting for the given language. + * + * The test is performed with a fixed URL (the default front page) to simply + * check that language prefixes are not added to it and that the prefixed URL + * is actually not working. + */ + private function checkUrl($language, $message1, $message2) { + $options = array('language' => $language); + $base_path = trim(base_path(), '/'); + $rewritten_path = trim(str_replace(array('?q=', $base_path), '', url('node', $options)), '/'); + $segments = explode('/', $rewritten_path, 2); + $prefix = $segments[0]; + $path = isset($segments[1]) ? $segments[1] : $prefix; + // If the rewritten URL has not a language prefix we pick the right one from + // the language object so we can always check the prefixed URL. + if ($this->assertNotEqual($language->prefix, $prefix, $message1)) { + $prefix = $language->prefix; + } + $this->drupalGet("$prefix/$path"); + $this->assertResponse(404, $message2); + } +} + +/** + * Functional test for multilingual fields. + */ +class LocaleMultilingualFieldsFunctionalTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Multilingual fields', + 'description' => 'Test multilingual support for fields.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale'); + // Setup users. + $admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages', 'create page content', 'edit own page content')); + $this->drupalLogin($admin_user); + + // Add a new language. + require_once DRUPAL_ROOT . '/core/includes/locale.inc'; + locale_add_language('it', 'Italian', 'Italiano', LANGUAGE_LTR, '', '', TRUE, FALSE); + + // Enable URL language detection and selection. + $edit = array('language[enabled][locale-url]' => '1'); + $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); + + // Set "Basic page" content type to use multilingual support. + $edit = array( + 'language_content_type' => 1, + ); + $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type')); + $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), t('Basic page content type has been updated.')); + } + + /** + * Test if field languages are correctly set through the node form. + */ + function testMultilingualNodeForm() { + // Create "Basic page" content. + $langcode = LANGUAGE_NONE; + $title_key = "title"; + $title_value = $this->randomName(8); + $body_key = "body[$langcode][0][value]"; + $body_value = $this->randomName(16); + + // Create node to edit. + $edit = array(); + $edit[$title_key] = $title_value; + $edit[$body_key] = $body_value; + $edit['language'] = 'en'; + $this->drupalPost('node/add/page', $edit, t('Save')); + + // Check that the node exists in the database. + $node = $this->drupalGetNodeByTitle($edit[$title_key]); + $this->assertTrue($node, t('Node found in database.')); + + $assert = isset($node->body['en']) && !isset($node->body[LANGUAGE_NONE]) && $node->body['en'][0]['value'] == $body_value; + $this->assertTrue($assert, t('Field language correctly set.')); + + // Change node language. + $this->drupalGet("node/$node->nid/edit"); + $edit = array( + $title_key => $this->randomName(8), + 'language' => 'it' + ); + $this->drupalPost(NULL, $edit, t('Save')); + $node = $this->drupalGetNodeByTitle($edit[$title_key]); + $this->assertTrue($node, t('Node found in database.')); + + $assert = isset($node->body['it']) && !isset($node->body['en']) && $node->body['it'][0]['value'] == $body_value; + $this->assertTrue($assert, t('Field language correctly changed.')); + + // Enable content language URL detection. + language_negotiation_set(LANGUAGE_TYPE_CONTENT, array(LOCALE_LANGUAGE_NEGOTIATION_URL => 0)); + + // Test multilingual field language fallback logic. + $this->drupalGet("it/node/$node->nid"); + $this->assertRaw($body_value, t('Body correctly displayed using Italian as requested language')); + + $this->drupalGet("node/$node->nid"); + $this->assertRaw($body_value, t('Body correctly displayed using English as requested language')); + } + + /* + * Test multilingual field display settings. + */ + function testMultilingualDisplaySettings() { + // Create "Basic page" content. + $langcode = LANGUAGE_NONE; + $title_key = "title"; + $title_value = $this->randomName(8); + $body_key = "body[$langcode][0][value]"; + $body_value = $this->randomName(16); + + // Create node to edit. + $edit = array(); + $edit[$title_key] = $title_value; + $edit[$body_key] = $body_value; + $edit['language'] = 'en'; + $this->drupalPost('node/add/page', $edit, t('Save')); + + // Check that the node exists in the database. + $node = $this->drupalGetNodeByTitle($edit[$title_key]); + $this->assertTrue($node, t('Node found in database.')); + + // Check if node body is showed. + $this->drupalGet("node/$node->nid"); + $body = $this->xpath('//div[@id=:id]//div[@property="content:encoded"]/p', array(':id' => 'node-' . $node->nid)); + $this->assertEqual(current($body), $node->body['en'][0]['value'], 'Node body is correctly showed.'); + } +} + +/** + * Functional tests for comment language. + */ +class LocaleCommentLanguageFunctionalTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Comment language', + 'description' => 'Tests for comment language.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale', 'locale_test'); + + // Create and login user. + $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'administer content types', 'create article content')); + $this->drupalLogin($admin_user); + + // Add language. + $edit = array('langcode' => 'fr'); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + + // Set "Article" content type to use multilingual support. + $edit = array('language_content_type' => 1); + $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type')); + + // Enable content language negotiation UI. + variable_set('locale_test_content_language_type', TRUE); + + // Set interface language detection to user and content language detection + // to URL. Disable inheritance from interface language to ensure content + // language will fall back to the default language if no URL language can be + // detected. + $edit = array( + 'language[enabled][locale-user]' => TRUE, + 'language_content[enabled][locale-url]' => TRUE, + 'language_content[enabled][locale-interface]' => FALSE, + ); + $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); + + // Change user language preference, this way interface language is always + // French no matter what path prefix the URLs have. + $edit = array('language' => 'fr'); + $this->drupalPost("user/{$admin_user->uid}/edit", $edit, t('Save')); + } + + /** + * Test that comment language is properly set. + */ + function testCommentLanguage() { + drupal_static_reset('language_list'); + + // Create two nodes, one for english and one for french, and comment each + // node using both english and french as content language by changing URL + // language prefixes. Meanwhile interface language is always French, which + // is the user language preference. This way we can ensure that node + // language and interface language do not influence comment language, as + // only content language has to. + foreach (language_list() as $node_langcode => $node_language) { + $language_none = LANGUAGE_NONE; + + // Create "Article" content. + $title = $this->randomName(); + $edit = array( + "title" => $title, + "body[$language_none][0][value]" => $this->randomName(), + "language" => $node_langcode, + ); + $this->drupalPost("node/add/article", $edit, t('Save')); + $node = $this->drupalGetNodeByTitle($title); + + foreach (language_list() as $langcode => $language) { + // Post a comment with content language $langcode. + $prefix = empty($language->prefix) ? '' : $language->prefix . '/'; + $edit = array("comment_body[$language_none][0][value]" => $this->randomName()); + $this->drupalPost("{$prefix}node/{$node->nid}", $edit, t('Save')); + + // Check that comment language matches the current content language. + $comment = db_select('comment', 'c') + ->fields('c') + ->condition('nid', $node->nid) + ->orderBy('cid', 'DESC') + ->execute() + ->fetchObject(); + $args = array('%node_language' => $node_langcode, '%comment_language' => $comment->language, '%langcode' => $langcode); + $this->assertEqual($comment->language, $langcode, t('The comment posted with content language %langcode and belonging to the node with language %node_language has language %comment_language', $args)); + } + } + } +} +/** + * Functional tests for localizing date formats. + */ +class LocaleDateFormatsFunctionalTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Localize date formats', + 'description' => 'Tests for the localization of date formats.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale'); + + // Create and login user. + $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'create article content')); + $this->drupalLogin($admin_user); + } + + /** + * Functional tests for localizing date formats. + */ + function testLocalizeDateFormats() { + // Add language. + $edit = array( + 'langcode' => 'fr', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + + // Set language negotiation. + $language_type = LANGUAGE_TYPE_INTERFACE; + $edit = array( + "{$language_type}[enabled][locale-url]" => TRUE, + ); + $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); + + // Configure date formats. + $this->drupalGet('admin/config/regional/date-time/locale'); + $this->assertText('Français', 'Configured languages appear.'); + $edit = array( + 'date_format_long' => 'd.m.Y - H:i', + 'date_format_medium' => 'd.m.Y - H:i', + 'date_format_short' => 'd.m.Y - H:i', + ); + $this->drupalPost('admin/config/regional/date-time/locale/fr/edit', $edit, t('Save configuration')); + $this->assertText(t('Configuration saved.'), 'French date formats updated.'); + $edit = array( + 'date_format_long' => 'j M Y - g:ia', + 'date_format_medium' => 'j M Y - g:ia', + 'date_format_short' => 'j M Y - g:ia', + ); + $this->drupalPost('admin/config/regional/date-time/locale/en/edit', $edit, t('Save configuration')); + $this->assertText(t('Configuration saved.'), 'English date formats updated.'); + + // Create node content. + $node = $this->drupalCreateNode(array('type' => 'article')); + + // Configure format for the node posted date changes with the language. + $this->drupalGet('node/' . $node->nid); + $english_date = format_date($node->created, 'custom', 'j M Y'); + $this->assertText($english_date, t('English date format appears')); + $this->drupalGet('fr/node/' . $node->nid); + $french_date = format_date($node->created, 'custom', 'd.m.Y'); + $this->assertText($french_date, t('French date format appears')); + } +} + +/** + * Functional test for language types/negotiation info. + */ +class LocaleLanguageNegotiationInfoFunctionalTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Language negotiation info', + 'description' => 'Tests alterations to language types/negotiation info.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale'); + require_once DRUPAL_ROOT .'/includes/language.inc'; + $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'view the administration theme')); + $this->drupalLogin($admin_user); + $this->drupalPost('admin/config/regional/language/add', array('langcode' => 'it'), t('Add language')); + } + + /** + * Tests alterations to language types/negotiation info. + */ + function testInfoAlterations() { + // Enable language type/negotiation info alterations. + variable_set('locale_test_language_types', TRUE); + variable_set('locale_test_language_negotiation_info', TRUE); + $this->languageNegotiationUpdate(); + + // Check that fixed language types are properly configured without the need + // of saving the language negotiation settings. + $this->checkFixedLanguageTypes(); + + // Make the content language type configurable by updating the language + // negotiation settings with the proper flag enabled. + variable_set('locale_test_content_language_type', TRUE); + $this->languageNegotiationUpdate(); + $type = LANGUAGE_TYPE_CONTENT; + $language_types = variable_get('language_types', drupal_language_types()); + $this->assertTrue($language_types[$type], t('Content language type is configurable.')); + + // Enable some core and custom language providers. The test language type is + // supposed to be configurable. + $test_type = 'test_language_type'; + $provider = LOCALE_LANGUAGE_NEGOTIATION_INTERFACE; + $test_provider = 'test_language_provider'; + $form_field = $type . '[enabled]['. $provider .']'; + $edit = array( + $form_field => TRUE, + $type . '[enabled][' . $test_provider . ']' => TRUE, + $test_type . '[enabled][' . $test_provider . ']' => TRUE, + ); + $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); + + // Remove the interface language provider by updating the language + // negotiation settings with the proper flag enabled. + variable_set('locale_test_language_negotiation_info_alter', TRUE); + $this->languageNegotiationUpdate(); + $negotiation = variable_get("language_negotiation_$type", array()); + $this->assertFalse(isset($negotiation[$provider]), t('Interface language provider removed from the stored settings.')); + $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, t('Interface language provider unavailable.')); + + // Check that type-specific language providers can be assigned only to the + // corresponding language types. + foreach (language_types_configurable() as $type) { + $form_field = $type . '[enabled][test_language_provider_ts]'; + if ($type == $test_type) { + $this->assertFieldByXPath("//input[@name=\"$form_field\"]", NULL, t('Type-specific test language provider available for %type.', array('%type' => $type))); + } + else { + $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, t('Type-specific test language provider unavailable for %type.', array('%type' => $type))); + } + } + + // Check language negotiation results. + $this->drupalGet(''); + $last = variable_get('locale_test_language_negotiation_last', array()); + foreach (language_types() as $type) { + $langcode = $last[$type]; + $value = $type == LANGUAGE_TYPE_CONTENT || strpos($type, 'test') !== FALSE ? 'it' : 'en'; + $this->assertEqual($langcode, $value, t('The negotiated language for %type is %language', array('%type' => $type, '%language' => $langcode))); + } + + // Disable locale_test and check that everything is set back to the original + // status. + $this->languageNegotiationUpdate('disable'); + + // Check that only the core language types are available. + foreach (language_types() as $type) { + $this->assertTrue(strpos($type, 'test') === FALSE, t('The %type language is still available', array('%type' => $type))); + } + + // Check that fixed language types are properly configured, even those + // previously set to configurable. + $this->checkFixedLanguageTypes(); + + // Check that unavailable language providers are not present in the + // negotiation settings. + $negotiation = variable_get("language_negotiation_$type", array()); + $this->assertFalse(isset($negotiation[$test_provider]), t('The disabled test language provider is not part of the content language negotiation settings.')); + + // Check that configuration page presents the correct options and settings. + $this->assertNoRaw(t('Test language detection'), t('No test language type configuration available.')); + $this->assertNoRaw(t('This is a test language provider'), t('No test language provider available.')); + } + + /** + * Update language types/negotiation information. + * + * Manually invoke locale_modules_enabled()/locale_modules_disabled() since + * they would not be invoked after enabling/disabling locale_test the first + * time. + */ + private function languageNegotiationUpdate($op = 'enable') { + static $last_op = NULL; + $modules = array('locale_test'); + + // Enable/disable locale_test only if we did not already before. + if ($last_op != $op) { + $function = "module_{$op}"; + $function($modules); + // Reset hook implementation cache. + module_implements(NULL, FALSE, TRUE); + } + + drupal_static_reset('language_types_info'); + drupal_static_reset('language_negotiation_info'); + $function = "locale_modules_{$op}d"; + if (function_exists($function)) { + $function($modules); + } + + $this->drupalGet('admin/config/regional/language/configure'); + } + + /** + * Check that language negotiation for fixed types matches the stored one. + */ + private function checkFixedLanguageTypes() { + drupal_static_reset('language_types_info'); + foreach (language_types_info() as $type => $info) { + if (isset($info['fixed'])) { + $negotiation = variable_get("language_negotiation_$type", array()); + $equal = count($info['fixed']) == count($negotiation); + while ($equal && list($id) = each($negotiation)) { + list(, $info_id) = each($info['fixed']); + $equal = $info_id == $id; + } + $this->assertTrue($equal, t('language negotiation for %type is properly set up', array('%type' => $type))); + } + } + } +} diff --git a/modules/locale/tests/locale_test.info b/core/modules/locale/tests/locale_test.info similarity index 100% rename from modules/locale/tests/locale_test.info rename to core/modules/locale/tests/locale_test.info diff --git a/modules/locale/tests/locale_test.module b/core/modules/locale/tests/locale_test.module similarity index 100% rename from modules/locale/tests/locale_test.module rename to core/modules/locale/tests/locale_test.module diff --git a/modules/locale/tests/translations/test.xx.po b/core/modules/locale/tests/translations/test.xx.po similarity index 100% rename from modules/locale/tests/translations/test.xx.po rename to core/modules/locale/tests/translations/test.xx.po diff --git a/modules/menu/menu.admin.inc b/core/modules/menu/menu.admin.inc similarity index 100% rename from modules/menu/menu.admin.inc rename to core/modules/menu/menu.admin.inc diff --git a/modules/menu/menu.admin.js b/core/modules/menu/menu.admin.js similarity index 100% rename from modules/menu/menu.admin.js rename to core/modules/menu/menu.admin.js diff --git a/modules/menu/menu.api.php b/core/modules/menu/menu.api.php similarity index 100% rename from modules/menu/menu.api.php rename to core/modules/menu/menu.api.php diff --git a/modules/menu/menu.css b/core/modules/menu/menu.css similarity index 100% rename from modules/menu/menu.css rename to core/modules/menu/menu.css diff --git a/modules/menu/menu.info b/core/modules/menu/menu.info similarity index 100% rename from modules/menu/menu.info rename to core/modules/menu/menu.info diff --git a/modules/menu/menu.install b/core/modules/menu/menu.install similarity index 100% rename from modules/menu/menu.install rename to core/modules/menu/menu.install diff --git a/modules/menu/menu.js b/core/modules/menu/menu.js similarity index 100% rename from modules/menu/menu.js rename to core/modules/menu/menu.js diff --git a/modules/menu/menu.module b/core/modules/menu/menu.module similarity index 100% rename from modules/menu/menu.module rename to core/modules/menu/menu.module diff --git a/modules/menu/menu.test b/core/modules/menu/menu.test similarity index 100% rename from modules/menu/menu.test rename to core/modules/menu/menu.test diff --git a/modules/node/content_types.inc b/core/modules/node/content_types.inc similarity index 100% rename from modules/node/content_types.inc rename to core/modules/node/content_types.inc diff --git a/modules/node/content_types.js b/core/modules/node/content_types.js similarity index 100% rename from modules/node/content_types.js rename to core/modules/node/content_types.js diff --git a/modules/node/node-rtl.css b/core/modules/node/node-rtl.css similarity index 100% rename from modules/node/node-rtl.css rename to core/modules/node/node-rtl.css diff --git a/core/modules/node/node.admin.inc b/core/modules/node/node.admin.inc new file mode 100644 index 0000000..f0d1fe5 --- /dev/null +++ b/core/modules/node/node.admin.inc @@ -0,0 +1,600 @@ + array( + 'label' => t('Publish selected content'), + 'callback' => 'node_mass_update', + 'callback arguments' => array('updates' => array('status' => NODE_PUBLISHED)), + ), + 'unpublish' => array( + 'label' => t('Unpublish selected content'), + 'callback' => 'node_mass_update', + 'callback arguments' => array('updates' => array('status' => NODE_NOT_PUBLISHED)), + ), + 'promote' => array( + 'label' => t('Promote selected content to front page'), + 'callback' => 'node_mass_update', + 'callback arguments' => array('updates' => array('status' => NODE_PUBLISHED, 'promote' => NODE_PROMOTED)), + ), + 'demote' => array( + 'label' => t('Demote selected content from front page'), + 'callback' => 'node_mass_update', + 'callback arguments' => array('updates' => array('promote' => NODE_NOT_PROMOTED)), + ), + 'sticky' => array( + 'label' => t('Make selected content sticky'), + 'callback' => 'node_mass_update', + 'callback arguments' => array('updates' => array('status' => NODE_PUBLISHED, 'sticky' => NODE_STICKY)), + ), + 'unsticky' => array( + 'label' => t('Make selected content not sticky'), + 'callback' => 'node_mass_update', + 'callback arguments' => array('updates' => array('sticky' => NODE_NOT_STICKY)), + ), + 'delete' => array( + 'label' => t('Delete selected content'), + 'callback' => NULL, + ), + ); + return $operations; +} + +/** + * List node administration filters that can be applied. + */ +function node_filters() { + // Regular filters + $filters['status'] = array( + 'title' => t('status'), + 'options' => array( + '[any]' => t('any'), + 'status-1' => t('published'), + 'status-0' => t('not published'), + 'promote-1' => t('promoted'), + 'promote-0' => t('not promoted'), + 'sticky-1' => t('sticky'), + 'sticky-0' => t('not sticky'), + ), + ); + // Include translation states if we have this module enabled + if (module_exists('translation')) { + $filters['status']['options'] += array( + 'translate-0' => t('Up to date translation'), + 'translate-1' => t('Outdated translation'), + ); + } + + $filters['type'] = array( + 'title' => t('type'), + 'options' => array( + '[any]' => t('any'), + ) + node_type_get_names(), + ); + + // Language filter if there is a list of languages + if ($languages = module_invoke('locale', 'language_list')) { + $languages = array(LANGUAGE_NONE => t('Language neutral')) + $languages; + $filters['language'] = array( + 'title' => t('language'), + 'options' => array( + '[any]' => t('any'), + ) + $languages, + ); + } + return $filters; +} + +/** + * Apply filters for node administration filters based on session. + * + * @param $query + * A SelectQuery to which the filters should be applied. + */ +function node_build_filter_query(SelectQueryInterface $query) { + // Build query + $filter_data = isset($_SESSION['node_overview_filter']) ? $_SESSION['node_overview_filter'] : array(); + foreach ($filter_data as $index => $filter) { + list($key, $value) = $filter; + switch ($key) { + case 'term': + $alias = $query->join('taxonomy_index', 'ti', "n.nid = %alias.nid"); + $query->condition($alias . '.tid', $value); + break; + case 'status': + // Note: no exploitable hole as $key/$value have already been checked when submitted + list($key, $value) = explode('-', $value, 2); + case 'type': + case 'language': + $query->condition('n.' . $key, $value); + break; + } + } +} + +/** + * Return form for node administration filters. + */ +function node_filter_form() { + $session = isset($_SESSION['node_overview_filter']) ? $_SESSION['node_overview_filter'] : array(); + $filters = node_filters(); + + $i = 0; + $form['filters'] = array( + '#type' => 'fieldset', + '#title' => t('Show only items where'), + '#theme' => 'exposed_filters__node', + ); + foreach ($session as $filter) { + list($type, $value) = $filter; + if ($type == 'term') { + // Load term name from DB rather than search and parse options array. + $value = module_invoke('taxonomy', 'term_load', $value); + $value = $value->name; + } + elseif ($type == 'language') { + $value = $value == LANGUAGE_NONE ? t('Language neutral') : module_invoke('locale', 'language_name', $value); + } + else { + $value = $filters[$type]['options'][$value]; + } + $t_args = array('%property' => $filters[$type]['title'], '%value' => $value); + if ($i++) { + $form['filters']['current'][] = array('#markup' => t('and where %property is %value', $t_args)); + } + else { + $form['filters']['current'][] = array('#markup' => t('where %property is %value', $t_args)); + } + if (in_array($type, array('type', 'language'))) { + // Remove the option if it is already being filtered on. + unset($filters[$type]); + } + } + + $form['filters']['status'] = array( + '#type' => 'container', + '#attributes' => array('class' => array('clearfix')), + '#prefix' => ($i ? '
' . t('and where') . '
' : ''), + ); + $form['filters']['status']['filters'] = array( + '#type' => 'container', + '#attributes' => array('class' => array('filters')), + ); + foreach ($filters as $key => $filter) { + $form['filters']['status']['filters'][$key] = array( + '#type' => 'select', + '#options' => $filter['options'], + '#title' => $filter['title'], + '#default_value' => '[any]', + ); + } + + $form['filters']['status']['actions'] = array( + '#type' => 'actions', + '#attributes' => array('class' => array('container-inline')), + ); + $form['filters']['status']['actions']['submit'] = array( + '#type' => 'submit', + '#value' => count($session) ? t('Refine') : t('Filter'), + ); + if (count($session)) { + $form['filters']['status']['actions']['undo'] = array('#type' => 'submit', '#value' => t('Undo')); + $form['filters']['status']['actions']['reset'] = array('#type' => 'submit', '#value' => t('Reset')); + } + + drupal_add_js('core/misc/form.js'); + + return $form; +} + +/** + * Process result from node administration filter form. + */ +function node_filter_form_submit($form, &$form_state) { + $filters = node_filters(); + switch ($form_state['values']['op']) { + case t('Filter'): + case t('Refine'): + // Apply every filter that has a choice selected other than 'any'. + foreach ($filters as $filter => $options) { + if (isset($form_state['values'][$filter]) && $form_state['values'][$filter] != '[any]') { + // Flatten the options array to accommodate hierarchical/nested options. + $flat_options = form_options_flatten($filters[$filter]['options']); + // Only accept valid selections offered on the dropdown, block bad input. + if (isset($flat_options[$form_state['values'][$filter]])) { + $_SESSION['node_overview_filter'][] = array($filter, $form_state['values'][$filter]); + } + } + } + break; + case t('Undo'): + array_pop($_SESSION['node_overview_filter']); + break; + case t('Reset'): + $_SESSION['node_overview_filter'] = array(); + break; + } +} + +/** + * Make mass update of nodes, changing all nodes in the $nodes array + * to update them with the field values in $updates. + * + * IMPORTANT NOTE: This function is intended to work when called + * from a form submit handler. Calling it outside of the form submission + * process may not work correctly. + * + * @param array $nodes + * Array of node nids to update. + * @param array $updates + * Array of key/value pairs with node field names and the + * value to update that field to. + */ +function node_mass_update($nodes, $updates) { + // We use batch processing to prevent timeout when updating a large number + // of nodes. + if (count($nodes) > 10) { + $batch = array( + 'operations' => array( + array('_node_mass_update_batch_process', array($nodes, $updates)) + ), + 'finished' => '_node_mass_update_batch_finished', + 'title' => t('Processing'), + // We use a single multi-pass operation, so the default + // 'Remaining x of y operations' message will be confusing here. + 'progress_message' => '', + 'error_message' => t('The update has encountered an error.'), + // The operations do not live in the .module file, so we need to + // tell the batch engine which file to load before calling them. + 'file' => drupal_get_path('module', 'node') . '/node.admin.inc', + ); + batch_set($batch); + } + else { + foreach ($nodes as $nid) { + _node_mass_update_helper($nid, $updates); + } + drupal_set_message(t('The update has been performed.')); + } +} + +/** + * Node Mass Update - helper function. + */ +function _node_mass_update_helper($nid, $updates) { + $node = node_load($nid, NULL, TRUE); + // For efficiency manually save the original node before applying any changes. + $node->original = clone $node; + foreach ($updates as $name => $value) { + $node->$name = $value; + } + node_save($node); + return $node; +} + +/** + * Node Mass Update Batch operation + */ +function _node_mass_update_batch_process($nodes, $updates, &$context) { + if (!isset($context['sandbox']['progress'])) { + $context['sandbox']['progress'] = 0; + $context['sandbox']['max'] = count($nodes); + $context['sandbox']['nodes'] = $nodes; + } + + // Process nodes by groups of 5. + $count = min(5, count($context['sandbox']['nodes'])); + for ($i = 1; $i <= $count; $i++) { + // For each nid, load the node, reset the values, and save it. + $nid = array_shift($context['sandbox']['nodes']); + $node = _node_mass_update_helper($nid, $updates); + + // Store result for post-processing in the finished callback. + $context['results'][] = l($node->title, 'node/' . $node->nid); + + // Update our progress information. + $context['sandbox']['progress']++; + } + + // Inform the batch engine that we are not finished, + // and provide an estimation of the completion level we reached. + if ($context['sandbox']['progress'] != $context['sandbox']['max']) { + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; + } +} + +/** + * Node Mass Update Batch 'finished' callback. + */ +function _node_mass_update_batch_finished($success, $results, $operations) { + if ($success) { + drupal_set_message(t('The update has been performed.')); + } + else { + drupal_set_message(t('An error occurred and processing did not complete.'), 'error'); + $message = format_plural(count($results), '1 item successfully processed:', '@count items successfully processed:'); + $message .= theme('item_list', array('items' => $results)); + drupal_set_message($message); + } +} + +/** + * Menu callback: content administration. + */ +function node_admin_content($form, $form_state) { + if (isset($form_state['values']['operation']) && $form_state['values']['operation'] == 'delete') { + return node_multiple_delete_confirm($form, $form_state, array_filter($form_state['values']['nodes'])); + } + $form['filter'] = node_filter_form(); + $form['#submit'][] = 'node_filter_form_submit'; + $form['admin'] = node_admin_nodes(); + + return $form; +} + +/** + * Form builder: Builds the node administration overview. + */ +function node_admin_nodes() { + $admin_access = user_access('administer nodes'); + + // Build the 'Update options' form. + $form['options'] = array( + '#type' => 'fieldset', + '#title' => t('Update options'), + '#attributes' => array('class' => array('container-inline')), + '#access' => $admin_access, + ); + $options = array(); + foreach (module_invoke_all('node_operations') as $operation => $array) { + $options[$operation] = $array['label']; + } + $form['options']['operation'] = array( + '#type' => 'select', + '#title' => t('Operation'), + '#title_display' => 'invisible', + '#options' => $options, + '#default_value' => 'approve', + ); + $form['options']['submit'] = array( + '#type' => 'submit', + '#value' => t('Update'), + '#validate' => array('node_admin_nodes_validate'), + '#submit' => array('node_admin_nodes_submit'), + ); + + // Enable language column if translation module is enabled or if we have any + // node with language. + $multilanguage = (module_exists('translation') || db_query_range("SELECT 1 FROM {node} WHERE language <> :language", 0, 1, array(':language' => LANGUAGE_NONE))->fetchField()); + + // Build the sortable table header. + $header = array( + 'title' => array('data' => t('Title'), 'field' => 'n.title'), + 'type' => array('data' => t('Type'), 'field' => 'n.type'), + 'author' => t('Author'), + 'status' => array('data' => t('Status'), 'field' => 'n.status'), + 'changed' => array('data' => t('Updated'), 'field' => 'n.changed', 'sort' => 'desc') + ); + if ($multilanguage) { + $header['language'] = array('data' => t('Language'), 'field' => 'n.language'); + } + $header['operations'] = array('data' => t('Operations')); + + $query = db_select('node', 'n')->extend('PagerDefault')->extend('TableSort'); + node_build_filter_query($query); + + if (!user_access('bypass node access')) { + // If the user is able to view their own unpublished nodes, allow them + // to see these in addition to published nodes. Check that they actually + // have some unpublished nodes to view before adding the condition. + if (user_access('view own unpublished content') && $own_unpublished = db_query('SELECT nid FROM {node} WHERE uid = :uid AND status = :status', array(':uid' => $GLOBALS['user']->uid, ':status' => 0))->fetchCol()) { + $query->condition(db_or() + ->condition('n.status', 1) + ->condition('n.nid', $own_unpublished, 'IN') + ); + } + else { + // If not, restrict the query to published nodes. + $query->condition('n.status', 1); + } + } + $nids = $query + ->fields('n',array('nid')) + ->limit(50) + ->orderByHeader($header) + ->execute() + ->fetchCol(); + $nodes = node_load_multiple($nids); + + // Prepare the list of nodes. + $languages = language_list(); + $destination = drupal_get_destination(); + $options = array(); + foreach ($nodes as $node) { + $l_options = $node->language != LANGUAGE_NONE && isset($languages[$node->language]) ? array('language' => $languages[$node->language]) : array(); + $options[$node->nid] = array( + 'title' => array( + 'data' => array( + '#type' => 'link', + '#title' => $node->title, + '#href' => 'node/' . $node->nid, + '#options' => $l_options, + '#suffix' => ' ' . theme('mark', array('type' => node_mark($node->nid, $node->changed))), + ), + ), + 'type' => check_plain(node_type_get_name($node)), + 'author' => theme('username', array('account' => $node)), + 'status' => $node->status ? t('published') : t('not published'), + 'changed' => format_date($node->changed, 'short'), + ); + if ($multilanguage) { + if ($node->language == LANGUAGE_NONE || isset($languages[$node->language])) { + $options[$node->nid]['language'] = $node->language == LANGUAGE_NONE ? t('Language neutral') : t($languages[$node->language]->name); + } + else { + $options[$node->nid]['language'] = t('Undefined language (@langcode)', array('@langcode' => $node->language)); + } + } + // Build a list of all the accessible operations for the current node. + $operations = array(); + if (node_access('update', $node)) { + $operations['edit'] = array( + 'title' => t('edit'), + 'href' => 'node/' . $node->nid . '/edit', + 'query' => $destination, + ); + } + if (node_access('delete', $node)) { + $operations['delete'] = array( + 'title' => t('delete'), + 'href' => 'node/' . $node->nid . '/delete', + 'query' => $destination, + ); + } + $options[$node->nid]['operations'] = array(); + if (count($operations) > 1) { + // Render an unordered list of operations links. + $options[$node->nid]['operations'] = array( + 'data' => array( + '#theme' => 'links__node_operations', + '#links' => $operations, + '#attributes' => array('class' => array('links', 'inline')), + ), + ); + } + elseif (!empty($operations)) { + // Render the first and only operation as a link. + $link = reset($operations); + $options[$node->nid]['operations'] = array( + 'data' => array( + '#type' => 'link', + '#title' => $link['title'], + '#href' => $link['href'], + '#options' => array('query' => $link['query']), + ), + ); + } + } + + // Only use a tableselect when the current user is able to perform any + // operations. + if ($admin_access) { + $form['nodes'] = array( + '#type' => 'tableselect', + '#header' => $header, + '#options' => $options, + '#empty' => t('No content available.'), + ); + } + // Otherwise, use a simple table. + else { + $form['nodes'] = array( + '#theme' => 'table', + '#header' => $header, + '#rows' => $options, + '#empty' => t('No content available.'), + ); + } + + $form['pager'] = array('#markup' => theme('pager')); + return $form; +} + +/** + * Validate node_admin_nodes form submissions. + * + * Check if any nodes have been selected to perform the chosen + * 'Update option' on. + */ +function node_admin_nodes_validate($form, &$form_state) { + // Error if there are no items to select. + if (!is_array($form_state['values']['nodes']) || !count(array_filter($form_state['values']['nodes']))) { + form_set_error('', t('No items selected.')); + } +} + +/** + * Process node_admin_nodes form submissions. + * + * Execute the chosen 'Update option' on the selected nodes. + */ +function node_admin_nodes_submit($form, &$form_state) { + $operations = module_invoke_all('node_operations'); + $operation = $operations[$form_state['values']['operation']]; + // Filter out unchecked nodes + $nodes = array_filter($form_state['values']['nodes']); + if ($function = $operation['callback']) { + // Add in callback arguments if present. + if (isset($operation['callback arguments'])) { + $args = array_merge(array($nodes), $operation['callback arguments']); + } + else { + $args = array($nodes); + } + call_user_func_array($function, $args); + + cache_clear_all(); + } + else { + // We need to rebuild the form to go to a second step. For example, to + // show the confirmation form for the deletion of nodes. + $form_state['rebuild'] = TRUE; + } +} + +function node_multiple_delete_confirm($form, &$form_state, $nodes) { + $form['nodes'] = array('#prefix' => '
    ', '#suffix' => '
', '#tree' => TRUE); + // array_filter returns only elements with TRUE values + foreach ($nodes as $nid => $value) { + $title = db_query('SELECT title FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchField(); + $form['nodes'][$nid] = array( + '#type' => 'hidden', + '#value' => $nid, + '#prefix' => '
  • ', + '#suffix' => check_plain($title) . "
  • \n", + ); + } + $form['operation'] = array('#type' => 'hidden', '#value' => 'delete'); + $form['#submit'][] = 'node_multiple_delete_confirm_submit'; + $confirm_question = format_plural(count($nodes), + 'Are you sure you want to delete this item?', + 'Are you sure you want to delete these items?'); + return confirm_form($form, + $confirm_question, + 'admin/content', t('This action cannot be undone.'), + t('Delete'), t('Cancel')); +} + +function node_multiple_delete_confirm_submit($form, &$form_state) { + if ($form_state['values']['confirm']) { + node_delete_multiple(array_keys($form_state['values']['nodes'])); + $count = count($form_state['values']['nodes']); + watchdog('content', 'Deleted @count posts.', array('@count' => $count)); + drupal_set_message(format_plural($count, 'Deleted 1 post.', 'Deleted @count posts.')); + } + $form_state['redirect'] = 'admin/content'; +} diff --git a/modules/node/node.api.php b/core/modules/node/node.api.php similarity index 100% rename from modules/node/node.api.php rename to core/modules/node/node.api.php diff --git a/modules/node/node.css b/core/modules/node/node.css similarity index 100% rename from modules/node/node.css rename to core/modules/node/node.css diff --git a/modules/node/node.info b/core/modules/node/node.info similarity index 100% rename from modules/node/node.info rename to core/modules/node/node.info diff --git a/modules/node/node.install b/core/modules/node/node.install similarity index 100% rename from modules/node/node.install rename to core/modules/node/node.install diff --git a/modules/node/node.js b/core/modules/node/node.js similarity index 100% rename from modules/node/node.js rename to core/modules/node/node.js diff --git a/modules/node/node.module b/core/modules/node/node.module similarity index 100% rename from modules/node/node.module rename to core/modules/node/node.module diff --git a/modules/node/node.pages.inc b/core/modules/node/node.pages.inc similarity index 100% rename from modules/node/node.pages.inc rename to core/modules/node/node.pages.inc diff --git a/modules/node/node.test b/core/modules/node/node.test similarity index 100% rename from modules/node/node.test rename to core/modules/node/node.test diff --git a/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc similarity index 100% rename from modules/node/node.tokens.inc rename to core/modules/node/node.tokens.inc diff --git a/modules/node/node.tpl.php b/core/modules/node/node.tpl.php similarity index 100% rename from modules/node/node.tpl.php rename to core/modules/node/node.tpl.php diff --git a/modules/node/tests/node_access_test.info b/core/modules/node/tests/node_access_test.info similarity index 100% rename from modules/node/tests/node_access_test.info rename to core/modules/node/tests/node_access_test.info diff --git a/modules/node/tests/node_access_test.module b/core/modules/node/tests/node_access_test.module similarity index 100% rename from modules/node/tests/node_access_test.module rename to core/modules/node/tests/node_access_test.module diff --git a/modules/node/tests/node_test.info b/core/modules/node/tests/node_test.info similarity index 100% rename from modules/node/tests/node_test.info rename to core/modules/node/tests/node_test.info diff --git a/modules/node/tests/node_test.module b/core/modules/node/tests/node_test.module similarity index 100% rename from modules/node/tests/node_test.module rename to core/modules/node/tests/node_test.module diff --git a/modules/node/tests/node_test_exception.info b/core/modules/node/tests/node_test_exception.info similarity index 100% rename from modules/node/tests/node_test_exception.info rename to core/modules/node/tests/node_test_exception.info diff --git a/modules/node/tests/node_test_exception.module b/core/modules/node/tests/node_test_exception.module similarity index 100% rename from modules/node/tests/node_test_exception.module rename to core/modules/node/tests/node_test_exception.module diff --git a/modules/openid/login-bg.png b/core/modules/openid/login-bg.png similarity index 100% rename from modules/openid/login-bg.png rename to core/modules/openid/login-bg.png diff --git a/modules/openid/openid-rtl.css b/core/modules/openid/openid-rtl.css similarity index 100% rename from modules/openid/openid-rtl.css rename to core/modules/openid/openid-rtl.css diff --git a/modules/openid/openid.api.php b/core/modules/openid/openid.api.php similarity index 100% rename from modules/openid/openid.api.php rename to core/modules/openid/openid.api.php diff --git a/modules/openid/openid.css b/core/modules/openid/openid.css similarity index 100% rename from modules/openid/openid.css rename to core/modules/openid/openid.css diff --git a/modules/openid/openid.inc b/core/modules/openid/openid.inc similarity index 100% rename from modules/openid/openid.inc rename to core/modules/openid/openid.inc diff --git a/modules/openid/openid.info b/core/modules/openid/openid.info similarity index 100% rename from modules/openid/openid.info rename to core/modules/openid/openid.info diff --git a/modules/openid/openid.install b/core/modules/openid/openid.install similarity index 100% rename from modules/openid/openid.install rename to core/modules/openid/openid.install diff --git a/modules/openid/openid.js b/core/modules/openid/openid.js similarity index 100% rename from modules/openid/openid.js rename to core/modules/openid/openid.js diff --git a/modules/openid/openid.module b/core/modules/openid/openid.module similarity index 100% rename from modules/openid/openid.module rename to core/modules/openid/openid.module diff --git a/modules/openid/openid.pages.inc b/core/modules/openid/openid.pages.inc similarity index 100% rename from modules/openid/openid.pages.inc rename to core/modules/openid/openid.pages.inc diff --git a/modules/openid/openid.test b/core/modules/openid/openid.test similarity index 100% rename from modules/openid/openid.test rename to core/modules/openid/openid.test diff --git a/modules/openid/tests/openid_test.info b/core/modules/openid/tests/openid_test.info similarity index 100% rename from modules/openid/tests/openid_test.info rename to core/modules/openid/tests/openid_test.info diff --git a/modules/openid/tests/openid_test.install b/core/modules/openid/tests/openid_test.install similarity index 100% rename from modules/openid/tests/openid_test.install rename to core/modules/openid/tests/openid_test.install diff --git a/modules/openid/tests/openid_test.module b/core/modules/openid/tests/openid_test.module similarity index 100% rename from modules/openid/tests/openid_test.module rename to core/modules/openid/tests/openid_test.module diff --git a/modules/overlay/images/background.png b/core/modules/overlay/images/background.png similarity index 100% rename from modules/overlay/images/background.png rename to core/modules/overlay/images/background.png diff --git a/modules/overlay/images/close.png b/core/modules/overlay/images/close.png similarity index 100% rename from modules/overlay/images/close.png rename to core/modules/overlay/images/close.png diff --git a/modules/overlay/overlay-child.css b/core/modules/overlay/overlay-child.css similarity index 100% rename from modules/overlay/overlay-child.css rename to core/modules/overlay/overlay-child.css diff --git a/modules/overlay/overlay-child.js b/core/modules/overlay/overlay-child.js similarity index 100% rename from modules/overlay/overlay-child.js rename to core/modules/overlay/overlay-child.js diff --git a/modules/overlay/overlay-parent.css b/core/modules/overlay/overlay-parent.css similarity index 100% rename from modules/overlay/overlay-parent.css rename to core/modules/overlay/overlay-parent.css diff --git a/modules/overlay/overlay-parent.js b/core/modules/overlay/overlay-parent.js similarity index 100% rename from modules/overlay/overlay-parent.js rename to core/modules/overlay/overlay-parent.js diff --git a/modules/overlay/overlay.api.php b/core/modules/overlay/overlay.api.php similarity index 100% rename from modules/overlay/overlay.api.php rename to core/modules/overlay/overlay.api.php diff --git a/modules/overlay/overlay.info b/core/modules/overlay/overlay.info similarity index 100% rename from modules/overlay/overlay.info rename to core/modules/overlay/overlay.info diff --git a/modules/overlay/overlay.install b/core/modules/overlay/overlay.install similarity index 100% rename from modules/overlay/overlay.install rename to core/modules/overlay/overlay.install diff --git a/modules/overlay/overlay.module b/core/modules/overlay/overlay.module similarity index 100% rename from modules/overlay/overlay.module rename to core/modules/overlay/overlay.module diff --git a/modules/overlay/overlay.tpl.php b/core/modules/overlay/overlay.tpl.php similarity index 100% rename from modules/overlay/overlay.tpl.php rename to core/modules/overlay/overlay.tpl.php diff --git a/modules/path/path.admin.inc b/core/modules/path/path.admin.inc similarity index 100% rename from modules/path/path.admin.inc rename to core/modules/path/path.admin.inc diff --git a/modules/path/path.api.php b/core/modules/path/path.api.php similarity index 100% rename from modules/path/path.api.php rename to core/modules/path/path.api.php diff --git a/modules/path/path.info b/core/modules/path/path.info similarity index 100% rename from modules/path/path.info rename to core/modules/path/path.info diff --git a/modules/path/path.js b/core/modules/path/path.js similarity index 100% rename from modules/path/path.js rename to core/modules/path/path.js diff --git a/modules/path/path.module b/core/modules/path/path.module similarity index 100% rename from modules/path/path.module rename to core/modules/path/path.module diff --git a/modules/path/path.test b/core/modules/path/path.test similarity index 100% rename from modules/path/path.test rename to core/modules/path/path.test diff --git a/modules/php/php.info b/core/modules/php/php.info similarity index 100% rename from modules/php/php.info rename to core/modules/php/php.info diff --git a/modules/php/php.install b/core/modules/php/php.install similarity index 100% rename from modules/php/php.install rename to core/modules/php/php.install diff --git a/modules/php/php.module b/core/modules/php/php.module similarity index 100% rename from modules/php/php.module rename to core/modules/php/php.module diff --git a/modules/php/php.test b/core/modules/php/php.test similarity index 100% rename from modules/php/php.test rename to core/modules/php/php.test diff --git a/modules/poll/poll-bar--block.tpl.php b/core/modules/poll/poll-bar--block.tpl.php similarity index 100% rename from modules/poll/poll-bar--block.tpl.php rename to core/modules/poll/poll-bar--block.tpl.php diff --git a/modules/poll/poll-bar.tpl.php b/core/modules/poll/poll-bar.tpl.php similarity index 100% rename from modules/poll/poll-bar.tpl.php rename to core/modules/poll/poll-bar.tpl.php diff --git a/modules/poll/poll-results--block.tpl.php b/core/modules/poll/poll-results--block.tpl.php similarity index 100% rename from modules/poll/poll-results--block.tpl.php rename to core/modules/poll/poll-results--block.tpl.php diff --git a/modules/poll/poll-results.tpl.php b/core/modules/poll/poll-results.tpl.php similarity index 100% rename from modules/poll/poll-results.tpl.php rename to core/modules/poll/poll-results.tpl.php diff --git a/modules/poll/poll-rtl.css b/core/modules/poll/poll-rtl.css similarity index 100% rename from modules/poll/poll-rtl.css rename to core/modules/poll/poll-rtl.css diff --git a/modules/poll/poll-vote.tpl.php b/core/modules/poll/poll-vote.tpl.php similarity index 100% rename from modules/poll/poll-vote.tpl.php rename to core/modules/poll/poll-vote.tpl.php diff --git a/modules/poll/poll.css b/core/modules/poll/poll.css similarity index 100% rename from modules/poll/poll.css rename to core/modules/poll/poll.css diff --git a/modules/poll/poll.info b/core/modules/poll/poll.info similarity index 100% rename from modules/poll/poll.info rename to core/modules/poll/poll.info diff --git a/modules/poll/poll.install b/core/modules/poll/poll.install similarity index 100% rename from modules/poll/poll.install rename to core/modules/poll/poll.install diff --git a/modules/poll/poll.module b/core/modules/poll/poll.module similarity index 100% rename from modules/poll/poll.module rename to core/modules/poll/poll.module diff --git a/modules/poll/poll.pages.inc b/core/modules/poll/poll.pages.inc similarity index 100% rename from modules/poll/poll.pages.inc rename to core/modules/poll/poll.pages.inc diff --git a/modules/poll/poll.test b/core/modules/poll/poll.test similarity index 100% rename from modules/poll/poll.test rename to core/modules/poll/poll.test diff --git a/modules/poll/poll.tokens.inc b/core/modules/poll/poll.tokens.inc similarity index 100% rename from modules/poll/poll.tokens.inc rename to core/modules/poll/poll.tokens.inc diff --git a/modules/profile/profile-block.tpl.php b/core/modules/profile/profile-block.tpl.php similarity index 100% rename from modules/profile/profile-block.tpl.php rename to core/modules/profile/profile-block.tpl.php diff --git a/modules/profile/profile-listing.tpl.php b/core/modules/profile/profile-listing.tpl.php similarity index 100% rename from modules/profile/profile-listing.tpl.php rename to core/modules/profile/profile-listing.tpl.php diff --git a/modules/profile/profile-wrapper.tpl.php b/core/modules/profile/profile-wrapper.tpl.php similarity index 100% rename from modules/profile/profile-wrapper.tpl.php rename to core/modules/profile/profile-wrapper.tpl.php diff --git a/modules/profile/profile.admin.inc b/core/modules/profile/profile.admin.inc similarity index 100% rename from modules/profile/profile.admin.inc rename to core/modules/profile/profile.admin.inc diff --git a/modules/profile/profile.css b/core/modules/profile/profile.css similarity index 100% rename from modules/profile/profile.css rename to core/modules/profile/profile.css diff --git a/modules/profile/profile.info b/core/modules/profile/profile.info similarity index 100% rename from modules/profile/profile.info rename to core/modules/profile/profile.info diff --git a/modules/profile/profile.install b/core/modules/profile/profile.install similarity index 100% rename from modules/profile/profile.install rename to core/modules/profile/profile.install diff --git a/modules/profile/profile.js b/core/modules/profile/profile.js similarity index 100% rename from modules/profile/profile.js rename to core/modules/profile/profile.js diff --git a/modules/profile/profile.module b/core/modules/profile/profile.module similarity index 100% rename from modules/profile/profile.module rename to core/modules/profile/profile.module diff --git a/modules/profile/profile.pages.inc b/core/modules/profile/profile.pages.inc similarity index 100% rename from modules/profile/profile.pages.inc rename to core/modules/profile/profile.pages.inc diff --git a/core/modules/profile/profile.test b/core/modules/profile/profile.test new file mode 100644 index 0000000..9c91110 --- /dev/null +++ b/core/modules/profile/profile.test @@ -0,0 +1,490 @@ +admin_user = $this->drupalCreateUser(array('administer users', 'access user profiles', 'administer blocks')); + + // This is the user whose profile will be edited. + $this->normal_user = $this->drupalCreateUser(); + } + + /** + * Create a profile field. + * + * @param $type + * The field type to be created. + * @param $category + * The category in which the field should be created. + * @param $edit + * Additional parameters to be submitted. + * @return + * The fid of the field that was just created. + */ + function createProfileField($type = 'textfield', $category = 'simpletest', $edit = array()) { + $edit['title'] = $title = $this->randomName(8); + $edit['name'] = $form_name = 'profile_' . $title; + $edit['category'] = $category; + $edit['explanation'] = $this->randomName(50); + + $this->drupalPost('admin/config/people/profile/add/' . $type, $edit, t('Save field')); + $fid = db_query("SELECT fid FROM {profile_field} WHERE title = :title", array(':title' => $title))->fetchField(); + $this->assertTrue($fid, t('New Profile field has been entered in the database')); + + // Check that the new field is appearing on the user edit form. + $this->drupalGet('user/' . $this->admin_user->uid . '/edit/' . $category); + + // Checking field. + if ($type == 'date') { + $this->assertField($form_name . '[month]', t('Found month selection field')); + $this->assertField($form_name . '[day]', t('Found day selection field')); + $this->assertField($form_name . '[year]', t('Found day selection field')); + } + else { + $this->assertField($form_name , t('Found form named @name', array('@name' => $form_name))); + } + + // Checking name. + $this->assertText($title, t('Checking title for field %title', array('%title' => $title))); + // Checking explanation. + $this->assertText($edit['explanation'], t('Checking explanation for field %title', array('%title' => $title))); + + return array( + 'fid' => $fid, + 'type' => $type, + 'form_name' => $form_name, + 'title' => $title, + 'category' => $category, + ); + } + + /** + * Update a profile field. + * + * @param $fid + * The fid of the field to be updated. + * @param $type + * The type of field to be updated. + * @param $edit + * Field parameters to be submitted. + * @return + * Array representation of the updated field. + */ + function updateProfileField($fid, $type = 'textfield', $edit = array()) { + + $form_name = $edit['name']; + $title = $edit['title']; + $category = $edit['category']; + + $this->drupalPost('admin/config/people/profile/edit/' . $fid, $edit, t('Save field')); + + // Check that the updated field is appearing on the user edit form. + $this->drupalGet('user/' . $this->admin_user->uid . '/edit/' . $category); + + // Checking field. + if ($type == 'date') { + $this->assertField($form_name . '[month]', t('Found month selection field')); + $this->assertField($form_name . '[day]', t('Found day selection field')); + $this->assertField($form_name . '[year]', t('Found day selection field')); + } + else { + $this->assertField($form_name , t('Found form named @name', array('@name' => $form_name))); + } + + // Checking name. + $this->assertText($title, t('Checking title for field %title', array('%title' => $title))); + // Checking explanation. + $this->assertText($edit['explanation'], t('Checking explanation for field %title', array('%title' => $title))); + + return array( + 'fid' => $fid, + 'type' => $type, + 'form_name' => $form_name, + 'title' => $title, + 'category' => $category, + ); + } + + /** + * Set the profile field to a random value + * + * @param $field + * The field that should be set. + * @param $value + * The value for the field, defaults to a random string. + * @return + * The value that has been assigned to + */ + function setProfileField($field, $value = NULL) { + + if (!isset($value)) { + $value = $this->randomName(); + } + + $edit = array( + $field['form_name'] => $value, + ); + $this->drupalPost('user/' . $this->normal_user->uid . '/edit/' . $field['category'], $edit, t('Save')); + + // Check profile page. + $content = $this->drupalGet('user/' . $this->normal_user->uid); + $this->assertText($field['title'], t('Found profile field with title %title', array('%title' => $field['title']))); + + if ($field['type'] != 'checkbox') { + // $value must be cast to a string in order to be found by assertText. + $this->assertText("$value", t('Found profile field with value %value', array('%value' => $value))); + } + + return $value; + } + + /** + * Delete a profile field. + * + * @param $field + * The field to be deleted. + */ + function deleteProfileField($field) { + $this->drupalPost('admin/config/people/profile/delete/' . $field['fid'], array(), t('Delete')); + $this->drupalGet('admin/config/people/profile'); + $this->assertNoText($field['title'], t('Checking deleted field %title', array('%title' => $field['title']))); + } +} + +class ProfileTestFields extends ProfileTestCase { + public static function getInfo() { + return array( + 'name' => 'Test single fields', + 'description' => 'Testing profile module with add/edit/delete textfield, textarea, list, checkbox, and url fields into profile page', + 'group' => 'Profile' + ); + } + + /** + * Test each of the field types. List selection and date fields are tested + * separately because they need some special handling. + */ + function testProfileFields() { + $this->drupalLogin($this->admin_user); + + // Set test values for every field type. + $field_types = array( + 'textfield' => $this->randomName(), + 'textarea' => $this->randomName(), + 'list' => $this->randomName(), + 'checkbox' => 1, + // An underscore is an invalid character in a domain name. The method randomName can + // return an underscore. + 'url' => 'http://www.' . str_replace('_', '', $this->randomName(10)) . '.org', + ); + + // For each field type, create a field, give it a value, update the field, + // and delete the field. + foreach ($field_types as $type => $value) { + $field = $this->createProfileField($type); + $this->setProfileField($field, $value); + $edit = array( + 'name' => $field['form_name'], + 'title' => $this->randomName(), + 'category' => $field['category'], + 'explanation' => $this->randomName(), + ); + $field = $this->updateProfileField($field['fid'], $field['type'], $edit); + $this->deleteProfileField($field); + } + } +} + +class ProfileTestSelect extends ProfileTestCase { + public static function getInfo() { + return array( + 'name' => 'Test select field', + 'description' => 'Testing profile module with add/edit/delete a select field', + 'group' => 'Profile' + ); + } + + /** + * Create a list selection field, give it a value, update and delete the field. + */ + function testProfileSelectionField() { + $this->drupalLogin($this->admin_user); + + $edit = array( + 'options' => implode("\n", range(1, 10)), + ); + $field = $this->createProfileField('selection', 'simpletest', $edit); + + $this->setProfileField($field, rand(1, 10)); + + $edit = array( + 'name' => $field['form_name'], + 'title' => $this->randomName(), + 'category' => $field['category'], + 'explanation' => $this->randomName(), + ); + $field = $this->updateProfileField($field['fid'], $field['type'], $edit); + $this->deleteProfileField($field); + } +} + +class ProfileTestDate extends ProfileTestCase { + public static function getInfo() { + return array( + 'name' => 'Test date field', + 'description' => 'Testing profile module with add/edit/delete a date field', + 'group' => 'Profile' + ); + } + + /** + * Create a date field, give it a value, update and delete the field. + */ + function testProfileDateField() { + $this->drupalLogin($this->admin_user); + + variable_set('date_format_short', 'm/d/Y - H:i'); + $field = $this->createProfileField('date'); + + // Set date to January 09, 1983 + $edit = array( + $field['form_name'] . '[month]' => 1, + $field['form_name'] . '[day]' => 9, + $field['form_name'] . '[year]' => 1983, + ); + + $this->drupalPost('user/' . $this->normal_user->uid . '/edit/' . $field['category'], $edit, t('Save')); + + // Check profile page. + $this->drupalGet('user/' . $this->normal_user->uid); + $this->assertText($field['title'], t('Found profile field with title %title', array('%title' => $field['title']))); + + $this->assertText('01/09/1983', t('Found date profile field.')); + + $edit = array( + 'name' => $field['form_name'], + 'title' => $this->randomName(), + 'category' => $field['category'], + 'explanation' => $this->randomName(), + ); + $field = $this->updateProfileField($field['fid'], $field['type'], $edit); + $this->deleteProfileField($field); + } +} + +class ProfileTestWeights extends ProfileTestCase { + public static function getInfo() { + return array( + 'name' => 'Test field weights', + 'description' => 'Testing profile modules weigting of fields', + 'group' => 'Profile' + ); + } + + function testProfileFieldWeights() { + $this->drupalLogin($this->admin_user); + + $category = $this->randomName(); + $field1 = $this->createProfileField('textfield', $category, array('weight' => 1)); + $field2 = $this->createProfileField('textfield', $category, array('weight' => -1)); + + $this->setProfileField($field1, $this->randomName(8)); + $this->setProfileField($field2, $this->randomName(8)); + + $profile_edit = $this->drupalGet('user/' . $this->normal_user->uid . '/edit/' . $category); + $this->assertTrue(strpos($profile_edit, $field1['title']) > strpos($profile_edit, $field2['title']), t('Profile field weights are respected on the user edit form.')); + + $profile_page = $this->drupalGet('user/' . $this->normal_user->uid); + $this->assertTrue(strpos($profile_page, $field1['title']) > strpos($profile_page, $field2['title']), t('Profile field weights are respected on the user profile page.')); + } +} + +/** + * Test profile field autocompletion and access. + */ +class ProfileTestAutocomplete extends ProfileTestCase { + public static function getInfo() { + return array( + 'name' => 'Autocompletion', + 'description' => 'Test profile fields with autocompletion.', + 'group' => 'Profile' + ); + } + + /** + * Tests profile field autocompletion and access. + */ + function testAutocomplete() { + $this->drupalLogin($this->admin_user); + + // Create a new profile field with autocompletion enabled. + $category = $this->randomName(); + $field = $this->createProfileField('textfield', $category, array('weight' => 1, 'autocomplete' => 1)); + + // Enter profile field value. + $field['value'] = $this->randomName(); + $this->setProfileField($field, $field['value']); + + // Set some html for what we want to see in the page output later. + $autocomplete_html = ''; + $field_html = ''; + + // Check that autocompletion html is found on the user's profile edit page. + $this->drupalGet('user/' . $this->admin_user->uid . '/edit/' . $category); + $this->assertRaw($autocomplete_html, t('Autocomplete found.')); + $this->assertRaw('core/misc/autocomplete.js', t('Autocomplete JavaScript found.')); + $this->assertRaw('class="form-text form-autocomplete"', t('Autocomplete form element class found.')); + + // Check the autocompletion path using the first letter of our user's profile + // field value to make sure access is allowed and a valid result if found. + $this->drupalGet('profile/autocomplete/' . $field['fid'] . '/' . $field['value'][0]); + $this->assertResponse(200, t('Autocomplete path allowed to user with permission.')); + $this->assertRaw($field['value'], t('Autocomplete value found.')); + + // Logout and login with a user without the 'access user profiles' permission. + $this->drupalLogout(); + $this->drupalLogin($this->normal_user); + + // Check that autocompletion html is not found on the user's profile edit page. + $this->drupalGet('user/' . $this->normal_user->uid . '/edit/' . $category); + $this->assertNoRaw($autocomplete_html, t('Autocomplete not found.')); + + // User should be denied access to the profile autocomplete path. + $this->drupalGet('profile/autocomplete/' . $field['fid'] . '/' . $field['value'][0]); + $this->assertResponse(403, t('Autocomplete path denied to user without permission.')); + } +} + +class ProfileBlockTestCase extends ProfileTestCase { + public static function getInfo() { + return array( + 'name' => 'Block availability', + 'description' => 'Check if the Author Information block is available.', + 'group' => 'Profile', + ); + } + + function setUp() { + parent::setUp(); + + // Login the admin user. + $this->drupalLogin($this->admin_user); + + // Create two fields. + $category = $this->randomName(); + $this->field1 = $this->createProfileField('textfield', $category, array('weight' => 0)); + $this->field2 = $this->createProfileField('textfield', $category, array('weight' => 1)); + + // Assign values to those fields. + $this->value1 = $this->setProfileField($this->field1); + $this->value2 = $this->setProfileField($this->field2); + + // Create a node authored by the normal user. + $this->node = $this->drupalCreateNode(array( + 'uid' => $this->normal_user->uid, + )); + } + + function testAuthorInformationBlock() { + // Set the block to a region to confirm the block is availble. + $edit = array(); + $edit['blocks[profile_author-information][region]'] = 'footer'; + $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); + $this->assertText(t('The block settings have been updated.'), t('Block successfully move to footer region.')); + + // Enable field 1. + $this->drupalPost('admin/structure/block/manage/profile/author-information/configure', array( + 'profile_block_author_fields[' . $this->field1['form_name'] . ']' => TRUE, + ), t('Save block')); + $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.')); + + // Visit the node and confirm that the field is displayed. + $this->drupalGet('node/' . $this->node->nid); + $this->assertRaw($this->value1, t('Field 1 is displayed')); + $this->assertNoRaw($this->value2, t('Field 2 is not displayed')); + + // Enable only field 2. + $this->drupalPost('admin/structure/block/manage/profile/author-information/configure', array( + 'profile_block_author_fields[' . $this->field1['form_name'] . ']' => FALSE, + 'profile_block_author_fields[' . $this->field2['form_name'] . ']' => TRUE, + ), t('Save block')); + $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.')); + + // Visit the node and confirm that the field is displayed. + $this->drupalGet('node/' . $this->node->nid); + $this->assertNoRaw($this->value1, t('Field 1 is not displayed')); + $this->assertRaw($this->value2, t('Field 2 is displayed')); + + // Enable both fields. + $this->drupalPost('admin/structure/block/manage/profile/author-information/configure', array( + 'profile_block_author_fields[' . $this->field1['form_name'] . ']' => TRUE, + 'profile_block_author_fields[' . $this->field2['form_name'] . ']' => TRUE, + ), t('Save block')); + $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.')); + + // Visit the node and confirm that the field is displayed. + $this->drupalGet('node/' . $this->node->nid); + $this->assertRaw($this->value1, t('Field 1 is displayed')); + $this->assertRaw($this->value2, t('Field 2 is displayed')); + + // Enable the link to the user profile. + $this->drupalPost('admin/structure/block/manage/profile/author-information/configure', array( + 'profile_block_author_fields[user_profile]' => TRUE, + ), t('Save block')); + $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.')); + + // Visit the node and confirm that the user profile link is displayed. + $this->drupalGet('node/' . $this->node->nid); + $this->clickLink(t('View full user profile')); + $this->assertEqual($this->getUrl(), url('user/' . $this->normal_user->uid, array('absolute' => TRUE))); + } +} + +/** + * Test profile browsing. + */ +class ProfileTestBrowsing extends ProfileTestCase { + public static function getInfo() { + return array( + 'name' => 'Profile browsing', + 'description' => 'Test profile browsing.', + 'group' => 'Profile', + ); + } + + /** + * Test profile browsing. + */ + function testProfileBrowsing() { + $this->drupalLogin($this->admin_user); + $field = $this->createProfileField('list', 'simpletest', array('page' => '%value')); + + // Set a random value for the profile field. + $value = $this->setProfileField($field); + + // Check that user is found on the profile browse page. + $this->drupalGet("profile/{$field['form_name']}/$value"); + $this->assertText($this->normal_user->name); + } +} + + /** + * TODO: + * - Test field visibility + * - Test required fields + * - Test fields on registration form + * - Test updating fields + */ diff --git a/modules/rdf/rdf.api.php b/core/modules/rdf/rdf.api.php similarity index 100% rename from modules/rdf/rdf.api.php rename to core/modules/rdf/rdf.api.php diff --git a/modules/rdf/rdf.info b/core/modules/rdf/rdf.info similarity index 100% rename from modules/rdf/rdf.info rename to core/modules/rdf/rdf.info diff --git a/modules/rdf/rdf.install b/core/modules/rdf/rdf.install similarity index 100% rename from modules/rdf/rdf.install rename to core/modules/rdf/rdf.install diff --git a/modules/rdf/rdf.module b/core/modules/rdf/rdf.module similarity index 100% rename from modules/rdf/rdf.module rename to core/modules/rdf/rdf.module diff --git a/modules/rdf/rdf.test b/core/modules/rdf/rdf.test similarity index 100% rename from modules/rdf/rdf.test rename to core/modules/rdf/rdf.test diff --git a/modules/rdf/tests/rdf_test.info b/core/modules/rdf/tests/rdf_test.info similarity index 100% rename from modules/rdf/tests/rdf_test.info rename to core/modules/rdf/tests/rdf_test.info diff --git a/modules/rdf/tests/rdf_test.install b/core/modules/rdf/tests/rdf_test.install similarity index 100% rename from modules/rdf/tests/rdf_test.install rename to core/modules/rdf/tests/rdf_test.install diff --git a/modules/rdf/tests/rdf_test.module b/core/modules/rdf/tests/rdf_test.module similarity index 100% rename from modules/rdf/tests/rdf_test.module rename to core/modules/rdf/tests/rdf_test.module diff --git a/modules/search/search-block-form.tpl.php b/core/modules/search/search-block-form.tpl.php similarity index 100% rename from modules/search/search-block-form.tpl.php rename to core/modules/search/search-block-form.tpl.php diff --git a/modules/search/search-result.tpl.php b/core/modules/search/search-result.tpl.php similarity index 100% rename from modules/search/search-result.tpl.php rename to core/modules/search/search-result.tpl.php diff --git a/modules/search/search-results.tpl.php b/core/modules/search/search-results.tpl.php similarity index 100% rename from modules/search/search-results.tpl.php rename to core/modules/search/search-results.tpl.php diff --git a/modules/search/search-rtl.css b/core/modules/search/search-rtl.css similarity index 100% rename from modules/search/search-rtl.css rename to core/modules/search/search-rtl.css diff --git a/modules/search/search.admin.inc b/core/modules/search/search.admin.inc similarity index 100% rename from modules/search/search.admin.inc rename to core/modules/search/search.admin.inc diff --git a/modules/search/search.api.php b/core/modules/search/search.api.php similarity index 100% rename from modules/search/search.api.php rename to core/modules/search/search.api.php diff --git a/modules/search/search.css b/core/modules/search/search.css similarity index 100% rename from modules/search/search.css rename to core/modules/search/search.css diff --git a/modules/search/search.extender.inc b/core/modules/search/search.extender.inc similarity index 100% rename from modules/search/search.extender.inc rename to core/modules/search/search.extender.inc diff --git a/modules/search/search.info b/core/modules/search/search.info similarity index 100% rename from modules/search/search.info rename to core/modules/search/search.info diff --git a/modules/search/search.install b/core/modules/search/search.install similarity index 100% rename from modules/search/search.install rename to core/modules/search/search.install diff --git a/modules/search/search.module b/core/modules/search/search.module similarity index 100% rename from modules/search/search.module rename to core/modules/search/search.module diff --git a/modules/search/search.pages.inc b/core/modules/search/search.pages.inc similarity index 100% rename from modules/search/search.pages.inc rename to core/modules/search/search.pages.inc diff --git a/core/modules/search/search.test b/core/modules/search/search.test new file mode 100644 index 0000000..498e282 --- /dev/null +++ b/core/modules/search/search.test @@ -0,0 +1,1949 @@ + 'Search engine queries', + 'description' => 'Indexes content and queries it.', + 'group' => 'Search', + ); + } + + /** + * Implementation setUp(). + */ + function setUp() { + parent::setUp('search'); + } + + /** + * Test search indexing. + */ + function testMatching() { + $this->_setup(); + $this->_testQueries(); + } + + /** + * Set up a small index of items to test against. + */ + function _setup() { + variable_set('minimum_word_size', 3); + + for ($i = 1; $i <= 7; ++$i) { + search_index($i, SEARCH_TYPE, $this->getText($i)); + } + for ($i = 1; $i <= 5; ++$i) { + search_index($i + 7, SEARCH_TYPE_2, $this->getText2($i)); + } + // No getText builder function for Japanese text; just a simple array. + foreach (array( + 13 => '以呂波耳・ほへとち。リヌルヲ。', + 14 => 'ドルーパルが大好きよ!', + 15 => 'コーヒーとケーキ', + ) as $i => $jpn) { + search_index($i, SEARCH_TYPE_JPN, $jpn); + } + search_update_totals(); + } + + /** + * _test_: Helper method for generating snippets of content. + * + * Generated items to test against: + * 1 ipsum + * 2 dolore sit + * 3 sit am ut + * 4 am ut enim am + * 5 ut enim am minim veniam + * 6 enim am minim veniam es cillum + * 7 am minim veniam es cillum dolore eu + */ + function getText($n) { + $words = explode(' ', "Ipsum dolore sit am. Ut enim am minim veniam. Es cillum dolore eu."); + return implode(' ', array_slice($words, $n - 1, $n)); + } + + /** + * _test2_: Helper method for generating snippets of content. + * + * Generated items to test against: + * 8 dear + * 9 king philip + * 10 philip came over + * 11 came over from germany + * 12 over from germany swimming + */ + function getText2($n) { + $words = explode(' ', "Dear King Philip came over from Germany swimming."); + return implode(' ', array_slice($words, $n - 1, $n)); + } + + /** + * Run predefine queries looking for indexed terms. + */ + function _testQueries() { + /* + Note: OR queries that include short words in OR groups are only accepted + if the ORed terms are ANDed with at least one long word in the rest of the query. + + e.g. enim dolore OR ut = enim (dolore OR ut) = (enim dolor) OR (enim ut) -> good + e.g. dolore OR ut = (dolore) OR (ut) -> bad + + This is a design limitation to avoid full table scans. + */ + $queries = array( + // Simple AND queries. + 'ipsum' => array(1), + 'enim' => array(4, 5, 6), + 'xxxxx' => array(), + 'enim minim' => array(5, 6), + 'enim xxxxx' => array(), + 'dolore eu' => array(7), + 'dolore xx' => array(), + 'ut minim' => array(5), + 'xx minim' => array(), + 'enim veniam am minim ut' => array(5), + // Simple OR queries. + 'dolore OR ipsum' => array(1, 2, 7), + 'dolore OR xxxxx' => array(2, 7), + 'dolore OR ipsum OR enim' => array(1, 2, 4, 5, 6, 7), + 'ipsum OR dolore sit OR cillum' => array(2, 7), + 'minim dolore OR ipsum' => array(7), + 'dolore OR ipsum veniam' => array(7), + 'minim dolore OR ipsum OR enim' => array(5, 6, 7), + 'dolore xx OR yy' => array(), + 'xxxxx dolore OR ipsum' => array(), + // Negative queries. + 'dolore -sit' => array(7), + 'dolore -eu' => array(2), + 'dolore -xxxxx' => array(2, 7), + 'dolore -xx' => array(2, 7), + // Phrase queries. + '"dolore sit"' => array(2), + '"sit dolore"' => array(), + '"am minim veniam es"' => array(6, 7), + '"minim am veniam es"' => array(), + // Mixed queries. + '"am minim veniam es" OR dolore' => array(2, 6, 7), + '"minim am veniam es" OR "dolore sit"' => array(2), + '"minim am veniam es" OR "sit dolore"' => array(), + '"am minim veniam es" -eu' => array(6), + '"am minim veniam" -"cillum dolore"' => array(5, 6), + '"am minim veniam" -"dolore cillum"' => array(5, 6, 7), + 'xxxxx "minim am veniam es" OR dolore' => array(), + 'xx "minim am veniam es" OR dolore' => array() + ); + foreach ($queries as $query => $results) { + $result = db_select('search_index', 'i') + ->extend('SearchQuery') + ->searchExpression($query, SEARCH_TYPE) + ->execute(); + + $set = $result ? $result->fetchAll() : array(); + $this->_testQueryMatching($query, $set, $results); + $this->_testQueryScores($query, $set, $results); + } + + // These queries are run against the second index type, SEARCH_TYPE_2. + $queries = array( + // Simple AND queries. + 'ipsum' => array(), + 'enim' => array(), + 'enim minim' => array(), + 'dear' => array(8), + 'germany' => array(11, 12), + ); + foreach ($queries as $query => $results) { + $result = db_select('search_index', 'i') + ->extend('SearchQuery') + ->searchExpression($query, SEARCH_TYPE_2) + ->execute(); + + $set = $result ? $result->fetchAll() : array(); + $this->_testQueryMatching($query, $set, $results); + $this->_testQueryScores($query, $set, $results); + } + + // These queries are run against the third index type, SEARCH_TYPE_JPN. + $queries = array( + // Simple AND queries. + '呂波耳' => array(13), + '以呂波耳' => array(13), + 'ほへと ヌルヲ' => array(13), + 'とちリ' => array(), + 'ドルーパル' => array(14), + 'パルが大' => array(14), + 'コーヒー' => array(15), + 'ヒーキ' => array(), + ); + foreach ($queries as $query => $results) { + $result = db_select('search_index', 'i') + ->extend('SearchQuery') + ->searchExpression($query, SEARCH_TYPE_JPN) + ->execute(); + + $set = $result ? $result->fetchAll() : array(); + $this->_testQueryMatching($query, $set, $results); + $this->_testQueryScores($query, $set, $results); + } + } + + /** + * Test the matching abilities of the engine. + * + * Verify if a query produces the correct results. + */ + function _testQueryMatching($query, $set, $results) { + // Get result IDs. + $found = array(); + foreach ($set as $item) { + $found[] = $item->sid; + } + + // Compare $results and $found. + sort($found); + sort($results); + $this->assertEqual($found, $results, "Query matching '$query'"); + } + + /** + * Test the scoring abilities of the engine. + * + * Verify if a query produces normalized, monotonous scores. + */ + function _testQueryScores($query, $set, $results) { + // Get result scores. + $scores = array(); + foreach ($set as $item) { + $scores[] = $item->calculated_score; + } + + // Check order. + $sorted = $scores; + sort($sorted); + $this->assertEqual($scores, array_reverse($sorted), "Query order '$query'"); + + // Check range. + $this->assertEqual(!count($scores) || (min($scores) > 0.0 && max($scores) <= 1.0001), TRUE, "Query scoring '$query'"); + } +} + +/** + * Tests the bike shed text on no results page, and text on the search page. + */ +class SearchPageText extends DrupalWebTestCase { + protected $searching_user; + + public static function getInfo() { + return array( + 'name' => 'Search page text', + 'description' => 'Tests the bike shed text on the no results page, and various other text on search pages.', + 'group' => 'Search' + ); + } + + function setUp() { + parent::setUp('search'); + + // Create user. + $this->searching_user = $this->drupalCreateUser(array('search content', 'access user profiles')); + } + + /** + * Tests the failed search text, and various other text on the search page. + */ + function testSearchText() { + $this->drupalLogin($this->searching_user); + $this->drupalGet('search/node'); + $this->assertText(t('Enter your keywords')); + $this->assertText(t('Search')); + $title = t('Search') . ' | Drupal'; + $this->assertTitle($title, 'Search page title is correct'); + + $edit = array(); + $edit['keys'] = 'bike shed ' . $this->randomName(); + $this->drupalPost('search/node', $edit, t('Search')); + $this->assertText(t('Consider loosening your query with OR. bike OR shed will often show more results than bike shed.'), t('Help text is displayed when search returns no results.')); + $this->assertText(t('Search')); + $this->assertTitle($title, 'Search page title is correct'); + + $edit['keys'] = $this->searching_user->name; + $this->drupalPost('search/user', $edit, t('Search')); + $this->assertText(t('Search')); + $this->assertTitle($title, 'Search page title is correct'); + + // Test that search keywords containing slashes are correctly loaded + // from the path and displayed in the search form. + $arg = $this->randomName() . '/' . $this->randomName(); + $this->drupalGet('search/node/' . $arg); + $input = $this->xpath("//input[@id='edit-keys' and @value='{$arg}']"); + $this->assertFalse(empty($input), 'Search keys with a / are correctly set as the default value in the search box.'); + } +} + +class SearchAdvancedSearchForm extends DrupalWebTestCase { + protected $node; + + public static function getInfo() { + return array( + 'name' => 'Advanced search form', + 'description' => 'Indexes content and tests the advanced search form.', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search'); + // Create and login user. + $test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'administer nodes')); + $this->drupalLogin($test_user); + + // Create initial node. + $node = $this->drupalCreateNode(); + $this->node = $this->drupalCreateNode(); + + // First update the index. This does the initial processing. + node_update_index(); + + // Then, run the shutdown function. Testing is a unique case where indexing + // and searching has to happen in the same request, so running the shutdown + // function manually is needed to finish the indexing process. + search_update_totals(); + } + + /** + * Test using the search form with GET and POST queries. + * Test using the advanced search form to limit search to nodes of type "Basic page". + */ + function testNodeType() { + $this->assertTrue($this->node->type == 'page', t('Node type is Basic page.')); + + // Assert that the dummy title doesn't equal the real title. + $dummy_title = 'Lorem ipsum'; + $this->assertNotEqual($dummy_title, $this->node->title, t("Dummy title doens't equal node title")); + + // Search for the dummy title with a GET query. + $this->drupalGet('search/node/' . $dummy_title); + $this->assertNoText($this->node->title, t('Basic page node is not found with dummy title.')); + + // Search for the title of the node with a GET query. + $this->drupalGet('search/node/' . $this->node->title); + $this->assertText($this->node->title, t('Basic page node is found with GET query.')); + + // Search for the title of the node with a POST query. + $edit = array('or' => $this->node->title); + $this->drupalPost('search/node', $edit, t('Advanced search')); + $this->assertText($this->node->title, t('Basic page node is found with POST query.')); + + // Advanced search type option. + $this->drupalPost('search/node', array_merge($edit, array('type[page]' => 'page')), t('Advanced search')); + $this->assertText($this->node->title, t('Basic page node is found with POST query and type:page.')); + + $this->drupalPost('search/node', array_merge($edit, array('type[article]' => 'article')), t('Advanced search')); + $this->assertText('bike shed', t('Article node is not found with POST query and type:article.')); + } +} + +class SearchRankingTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Search engine ranking', + 'description' => 'Indexes content and tests ranking factors.', + 'group' => 'Search', + ); + } + + /** + * Implementation setUp(). + */ + function setUp() { + parent::setUp('search', 'statistics', 'comment'); + } + + function testRankings() { + // Login with sufficient privileges. + $this->drupalLogin($this->drupalCreateUser(array('skip comment approval', 'create page content'))); + + // Build a list of the rankings to test. + $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views'); + + // Create nodes for testing. + foreach ($node_ranks as $node_rank) { + $settings = array( + 'type' => 'page', + 'title' => 'Drupal rocks', + 'body' => array(LANGUAGE_NONE => array(array('value' => "Drupal's search rocks"))), + ); + foreach (array(0, 1) as $num) { + if ($num == 1) { + switch ($node_rank) { + case 'sticky': + case 'promote': + $settings[$node_rank] = 1; + break; + case 'relevance': + $settings['body'][LANGUAGE_NONE][0]['value'] .= " really rocks"; + break; + case 'recent': + $settings['created'] = REQUEST_TIME + 3600; + break; + case 'comments': + $settings['comment'] = 2; + break; + } + } + $nodes[$node_rank][$num] = $this->drupalCreateNode($settings); + } + } + + // Update the search index. + module_invoke_all('update_index'); + search_update_totals(); + + // Refresh variables after the treatment. + $this->refreshVariables(); + + // Add a comment to one of the nodes. + $edit = array(); + $edit['subject'] = 'my comment title'; + $edit['comment_body[' . LANGUAGE_NONE . '][0][value]'] = 'some random comment'; + $this->drupalGet('comment/reply/' . $nodes['comments'][1]->nid); + $this->drupalPost(NULL, $edit, t('Preview')); + $this->drupalPost(NULL, $edit, t('Save')); + + // Enable counting of statistics. + variable_set('statistics_count_content_views', 1); + + // Then View one of the nodes a bunch of times. + for ($i = 0; $i < 5; $i ++) { + $this->drupalGet('node/' . $nodes['views'][1]->nid); + } + + // Test each of the possible rankings. + foreach ($node_ranks as $node_rank) { + // Disable all relevancy rankings except the one we are testing. + foreach ($node_ranks as $var) { + variable_set('node_rank_' . $var, $var == $node_rank ? 10 : 0); + } + + // Do the search and assert the results. + $set = node_search_execute('rocks'); + $this->assertEqual($set[0]['node']->nid, $nodes[$node_rank][1]->nid, 'Search ranking "' . $node_rank . '" order.'); + } + } + + /** + * Test rankings of HTML tags. + */ + function testHTMLRankings() { + // Login with sufficient privileges. + $this->drupalLogin($this->drupalCreateUser(array('create page content'))); + + // Test HTML tags with different weights. + $sorted_tags = array('h1', 'h2', 'h3', 'h4', 'a', 'h5', 'h6', 'notag'); + $shuffled_tags = $sorted_tags; + + // Shuffle tags to ensure HTML tags are ranked properly. + shuffle($shuffled_tags); + $settings = array( + 'type' => 'page', + 'title' => 'Simple node', + ); + foreach ($shuffled_tags as $tag) { + switch ($tag) { + case 'a': + $settings['body'] = array(LANGUAGE_NONE => array(array('value' => l('Drupal Rocks', 'node'), 'format' => 'full_html'))); + break; + case 'notag': + $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'Drupal Rocks'))); + break; + default: + $settings['body'] = array(LANGUAGE_NONE => array(array('value' => "<$tag>Drupal Rocks", 'format' => 'full_html'))); + break; + } + $nodes[$tag] = $this->drupalCreateNode($settings); + } + + // Update the search index. + module_invoke_all('update_index'); + search_update_totals(); + + // Refresh variables after the treatment. + $this->refreshVariables(); + + // Disable all other rankings. + $node_ranks = array('sticky', 'promote', 'recent', 'comments', 'views'); + foreach ($node_ranks as $node_rank) { + variable_set('node_rank_' . $node_rank, 0); + } + $set = node_search_execute('rocks'); + + // Test the ranking of each tag. + foreach ($sorted_tags as $tag_rank => $tag) { + // Assert the results. + if ($tag == 'notag') { + $this->assertEqual($set[$tag_rank]['node']->nid, $nodes[$tag]->nid, 'Search tag ranking for plain text order.'); + } else { + $this->assertEqual($set[$tag_rank]['node']->nid, $nodes[$tag]->nid, 'Search tag ranking for "<' . $sorted_tags[$tag_rank] . '>" order.'); + } + } + + // Test tags with the same weight against the sorted tags. + $unsorted_tags = array('u', 'b', 'i', 'strong', 'em'); + foreach ($unsorted_tags as $tag) { + $settings['body'] = array(LANGUAGE_NONE => array(array('value' => "<$tag>Drupal Rocks", 'format' => 'full_html'))); + $node = $this->drupalCreateNode($settings); + + // Update the search index. + module_invoke_all('update_index'); + search_update_totals(); + + // Refresh variables after the treatment. + $this->refreshVariables(); + + $set = node_search_execute('rocks'); + + // Ranking should always be second to last. + $set = array_slice($set, -2, 1); + + // Assert the results. + $this->assertEqual($set[0]['node']->nid, $node->nid, 'Search tag ranking for "<' . $tag . '>" order.'); + + // Delete node so it doesn't show up in subsequent search results. + node_delete($node->nid); + } + } + + /** + * Verifies that if we combine two rankings, search still works. + * + * See issue http://drupal.org/node/771596 + */ + function testDoubleRankings() { + // Login with sufficient privileges. + $this->drupalLogin($this->drupalCreateUser(array('skip comment approval', 'create page content'))); + + // See testRankings() above - build a node that will rank high for sticky. + $settings = array( + 'type' => 'page', + 'title' => 'Drupal rocks', + 'body' => array(LANGUAGE_NONE => array(array('value' => "Drupal's search rocks"))), + 'sticky' => 1, + ); + + $node = $this->drupalCreateNode($settings); + + // Update the search index. + module_invoke_all('update_index'); + search_update_totals(); + + // Refresh variables after the treatment. + $this->refreshVariables(); + + // Set up for ranking sticky and lots of comments; make sure others are + // disabled. + $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views'); + foreach ($node_ranks as $var) { + $value = ($var == 'sticky' || $var == 'comments') ? 10 : 0; + variable_set('node_rank_' . $var, $value); + } + + // Do the search and assert the results. + $set = node_search_execute('rocks'); + $this->assertEqual($set[0]['node']->nid, $node->nid, 'Search double ranking order.'); + } +} + +class SearchBlockTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Block availability', + 'description' => 'Check if the search form block is available.', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search'); + + // Create and login user + $admin_user = $this->drupalCreateUser(array('administer blocks', 'search content')); + $this->drupalLogin($admin_user); + } + + function testSearchFormBlock() { + // Set block title to confirm that the interface is availble. + $this->drupalPost('admin/structure/block/manage/search/form/configure', array('title' => $this->randomName(8)), t('Save block')); + $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.')); + + // Set the block to a region to confirm block is availble. + $edit = array(); + $edit['blocks[search_form][region]'] = 'footer'; + $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); + $this->assertText(t('The block settings have been updated.'), t('Block successfully move to footer region.')); + } + + /** + * Test that the search block form works correctly. + */ + function testBlock() { + // Enable the block, and place it in the 'content' region so that it isn't + // hidden on 404 pages. + $edit = array('blocks[search_form][region]' => 'content'); + $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); + + // Test a normal search via the block form, from the front page. + $terms = array('search_block_form' => 'test'); + $this->drupalPost('node', $terms, t('Search')); + $this->assertText('Your search yielded no results'); + + // Test a search from the block on a 404 page. + $this->drupalGet('foo'); + $this->assertResponse(404); + $this->drupalPost(NULL, $terms, t('Search')); + $this->assertResponse(200); + $this->assertText('Your search yielded no results'); + + // Test a search from the block when it doesn't appear on the search page. + $edit = array('pages' => 'search'); + $this->drupalPost('admin/structure/block/manage/search/form/configure', $edit, t('Save block')); + $this->drupalPost('node', $terms, t('Search')); + $this->assertText('Your search yielded no results'); + + // Confirm that the user is redirected to the search page. + $this->assertEqual( + $this->getUrl(), + url('search/node/' . $terms['search_block_form'], array('absolute' => TRUE)), + t('Redirected to correct url.') + ); + + // Test an empty search via the block form, from the front page. + $terms = array('search_block_form' => ''); + $this->drupalPost('node', $terms, t('Search')); + $this->assertText('Please enter some keywords'); + + // Confirm that the user is redirected to the search page, when form is submitted empty. + $this->assertEqual( + $this->getUrl(), + url('search/node/', array('absolute' => TRUE)), + t('Redirected to correct url.') + ); + } +} + +/** + * Tests that searching for a phrase gets the correct page count. + */ +class SearchExactTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Search engine phrase queries', + 'description' => 'Tests that searching for a phrase gets the correct page count.', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search'); + } + + /** + * Tests that the correct number of pager links are found for both keywords and phrases. + */ + function testExactQuery() { + // Login with sufficient privileges. + $this->drupalLogin($this->drupalCreateUser(array('create page content', 'search content'))); + + $settings = array( + 'type' => 'page', + 'title' => 'Simple Node', + ); + // Create nodes with exact phrase. + for ($i = 0; $i <= 17; $i++) { + $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love pizza'))); + $this->drupalCreateNode($settings); + } + // Create nodes containing keywords. + for ($i = 0; $i <= 17; $i++) { + $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love cheesy pizza'))); + $this->drupalCreateNode($settings); + } + + // Update the search index. + module_invoke_all('update_index'); + search_update_totals(); + + // Refresh variables after the treatment. + $this->refreshVariables(); + + // Test that the correct number of pager links are found for keyword search. + $edit = array('keys' => 'love pizza'); + $this->drupalPost('search/node', $edit, t('Search')); + $this->assertLinkByHref('page=1', 0, '2nd page link is found for keyword search.'); + $this->assertLinkByHref('page=2', 0, '3rd page link is found for keyword search.'); + $this->assertLinkByHref('page=3', 0, '4th page link is found for keyword search.'); + $this->assertNoLinkByHref('page=4', '5th page link is not found for keyword search.'); + + // Test that the correct number of pager links are found for exact phrase search. + $edit = array('keys' => '"love pizza"'); + $this->drupalPost('search/node', $edit, t('Search')); + $this->assertLinkByHref('page=1', 0, '2nd page link is found for exact phrase search.'); + $this->assertNoLinkByHref('page=2', '3rd page link is not found for exact phrase search.'); + } +} + +/** + * Test integration searching comments. + */ +class SearchCommentTestCase extends DrupalWebTestCase { + protected $admin_user; + + public static function getInfo() { + return array( + 'name' => 'Comment Search tests', + 'description' => 'Verify text formats and filters used elsewhere.', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('comment', 'search'); + + // Create and log in an administrative user having access to the Full HTML + // text format. + $full_html_format = filter_format_load('full_html'); + $permissions = array( + 'administer filters', + filter_permission_name($full_html_format), + 'administer permissions', + 'create page content', + 'skip comment approval', + 'access comments', + ); + $this->admin_user = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->admin_user); + } + + /** + * Verify that comments are rendered using proper format in search results. + */ + function testSearchResultsComment() { + $comment_body = 'Test comment body'; + + variable_set('comment_preview_article', DRUPAL_OPTIONAL); + // Enable check_plain() for 'Filtered HTML' text format. + $filtered_html_format_id = 'filtered_html'; + $edit = array( + 'filters[filter_html_escape][status]' => TRUE, + ); + $this->drupalPost('admin/config/content/formats/' . $filtered_html_format_id, $edit, t('Save configuration')); + // Allow anonymous users to search content. + $edit = array( + DRUPAL_ANONYMOUS_RID . '[search content]' => 1, + DRUPAL_ANONYMOUS_RID . '[access comments]' => 1, + DRUPAL_ANONYMOUS_RID . '[post comments]' => 1, + ); + $this->drupalPost('admin/people/permissions', $edit, t('Save permissions')); + + // Create a node. + $node = $this->drupalCreateNode(array('type' => 'article')); + // Post a comment using 'Full HTML' text format. + $edit_comment = array(); + $edit_comment['subject'] = 'Test comment subject'; + $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '

    ' . $comment_body . '

    '; + $full_html_format_id = 'full_html'; + $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $full_html_format_id; + $this->drupalPost('comment/reply/' . $node->nid, $edit_comment, t('Save')); + + // Invoke search index update. + $this->drupalLogout(); + $this->cronRun(); + + // Search for the comment subject. + $edit = array( + 'search_block_form' => "'" . $edit_comment['subject'] . "'", + ); + $this->drupalPost('', $edit, t('Search')); + $this->assertText($node->title, t('Node found in search results.')); + $this->assertText($edit_comment['subject'], t('Comment subject found in search results.')); + + // Search for the comment body. + $edit = array( + 'search_block_form' => "'" . $comment_body . "'", + ); + $this->drupalPost('', $edit, t('Search')); + $this->assertText($node->title, t('Node found in search results.')); + + // Verify that comment is rendered using proper format. + $this->assertText($comment_body, t('Comment body text found in search results.')); + $this->assertNoRaw(t('n/a'), t('HTML in comment body is not hidden.')); + $this->assertNoRaw(check_plain($edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]']), t('HTML in comment body is not escaped.')); + + // Hide comments. + $this->drupalLogin($this->admin_user); + $node->comment = 0; + node_save($node); + + // Invoke search index update. + $this->drupalLogout(); + $this->cronRun(); + + // Search for $title. + $this->drupalPost('', $edit, t('Search')); + $this->assertNoText($comment_body, t('Comment body text not found in search results.')); + } + + /** + * Verify access rules for comment indexing with different permissions. + */ + function testSearchResultsCommentAccess() { + $comment_body = 'Test comment body'; + $this->comment_subject = 'Test comment subject'; + $this->admin_role = $this->admin_user->roles; + unset($this->admin_role[DRUPAL_AUTHENTICATED_RID]); + $this->admin_role = key($this->admin_role); + + // Create a node. + variable_set('comment_preview_article', DRUPAL_OPTIONAL); + $this->node = $this->drupalCreateNode(array('type' => 'article')); + + // Post a comment using 'Full HTML' text format. + $edit_comment = array(); + $edit_comment['subject'] = $this->comment_subject; + $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '

    ' . $comment_body . '

    '; + $this->drupalPost('comment/reply/' . $this->node->nid, $edit_comment, t('Save')); + + $this->drupalLogout(); + $this->setRolePermissions(DRUPAL_ANONYMOUS_RID); + $this->checkCommentAccess('Anon user has search permission but no access comments permission, comments should not be indexed'); + + $this->setRolePermissions(DRUPAL_ANONYMOUS_RID, TRUE); + $this->checkCommentAccess('Anon user has search permission and access comments permission, comments should be indexed', TRUE); + + $this->drupalLogin($this->admin_user); + $this->drupalGet('admin/people/permissions'); + + // Disable search access for authenticated user to test admin user. + $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, FALSE, FALSE); + + $this->setRolePermissions($this->admin_role); + $this->checkCommentAccess('Admin user has search permission but no access comments permission, comments should not be indexed'); + + $this->setRolePermissions($this->admin_role, TRUE); + $this->checkCommentAccess('Admin user has search permission and access comments permission, comments should be indexed', TRUE); + + $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID); + $this->checkCommentAccess('Authenticated user has search permission but no access comments permission, comments should not be indexed'); + + $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE); + $this->checkCommentAccess('Authenticated user has search permission and access comments permission, comments should be indexed', TRUE); + + // Verify that access comments permission is inherited from the + // authenticated role. + $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, FALSE); + $this->setRolePermissions($this->admin_role); + $this->checkCommentAccess('Admin user has search permission and no access comments permission, but comments should be indexed because admin user inherits authenticated user\'s permission to access comments', TRUE); + + // Verify that search content permission is inherited from the authenticated + // role. + $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, TRUE); + $this->setRolePermissions($this->admin_role, TRUE, FALSE); + $this->checkCommentAccess('Admin user has access comments permission and no search permission, but comments should be indexed because admin user inherits authenticated user\'s permission to search', TRUE); + + } + + /** + * Set permissions for role. + */ + function setRolePermissions($rid, $access_comments = FALSE, $search_content = TRUE) { + $permissions = array( + 'access comments' => $access_comments, + 'search content' => $search_content, + ); + user_role_change_permissions($rid, $permissions); + } + + /** + * Update search index and search for comment. + */ + function checkCommentAccess($message, $assume_access = FALSE) { + // Invoke search index update. + search_touch_node($this->node->nid); + $this->cronRun(); + + // Search for the comment subject. + $edit = array( + 'search_block_form' => "'" . $this->comment_subject . "'", + ); + $this->drupalPost('', $edit, t('Search')); + $method = $assume_access ? 'assertText' : 'assertNoText'; + $verb = $assume_access ? 'found' : 'not found'; + $this->{$method}($this->node->title, "Node $verb in search results: " . $message); + $this->{$method}($this->comment_subject, "Comment subject $verb in search results: " . $message); + } + + /** + * Verify that 'add new comment' does not appear in search results or index. + */ + function testAddNewComment() { + // Create a node with a short body. + $settings = array( + 'type' => 'article', + 'title' => 'short title', + 'body' => array(LANGUAGE_NONE => array(array('value' => 'short body text'))), + ); + + $user = $this->drupalCreateUser(array('search content', 'create article content', 'access content')); + $this->drupalLogin($user); + + $node = $this->drupalCreateNode($settings); + // Verify that if you view the node on its own page, 'add new comment' + // is there. + $this->drupalGet('node/' . $node->nid); + $this->assertText(t('Add new comment'), t('Add new comment appears on node page')); + + // Run cron to index this page. + $this->drupalLogout(); + $this->cronRun(); + + // Search for 'comment'. Should be no results. + $this->drupalLogin($user); + $this->drupalPost('search/node', array('keys' => 'comment'), t('Search')); + $this->assertText(t('Your search yielded no results'), t('No results searching for the word comment')); + + // Search for the node title. Should be found, and 'Add new comment' should + // not be part of the search snippet. + $this->drupalPost('search/node', array('keys' => 'short'), t('Search')); + $this->assertText($node->title, t('Search for keyword worked')); + $this->assertNoText(t('Add new comment'), t('Add new comment does not appear on search results page')); + } + +} + +/** + * Tests search_expression_insert() and search_expression_extract(). + * + * @see http://drupal.org/node/419388 (issue) + */ +class SearchExpressionInsertExtractTestCase extends DrupalUnitTestCase { + public static function getInfo() { + return array( + 'name' => 'Search expression insert/extract', + 'description' => 'Tests the functions search_expression_insert() and search_expression_extract()', + 'group' => 'Search', + ); + } + + function setUp() { + drupal_load('module', 'search'); + parent::setUp(); + } + + /** + * Tests search_expression_insert() and search_expression_extract(). + */ + function testInsertExtract() { + $base_expression = "mykeyword"; + // Build an array of option, value, what should be in the expression, what + // should be retrieved from expression. + $cases = array( + array('foo', 'bar', 'foo:bar', 'bar'), // Normal case. + array('foo', NULL, '', NULL), // Empty value: shouldn't insert. + array('foo', ' ', 'foo:', ''), // Space as value: should insert but retrieve empty string. + array('foo', '', 'foo:', ''), // Empty string as value: should insert but retrieve empty string. + array('foo', '0', 'foo:0', '0'), // String zero as value: should insert. + array('foo', 0, 'foo:0', '0'), // Numeric zero as value: should insert. + ); + + foreach ($cases as $index => $case) { + $after_insert = search_expression_insert($base_expression, $case[0], $case[1]); + if (empty($case[2])) { + $this->assertEqual($after_insert, $base_expression, "Empty insert does not change expression in case $index"); + } + else { + $this->assertEqual($after_insert, $base_expression . ' ' . $case[2], "Insert added correct expression for case $index"); + } + + $retrieved = search_expression_extract($after_insert, $case[0]); + if (!isset($case[3])) { + $this->assertFalse(isset($retrieved), "Empty retrieval results in unset value in case $index"); + } + else { + $this->assertEqual($retrieved, $case[3], "Value is retrieved for case $index"); + } + + $after_clear = search_expression_insert($after_insert, $case[0]); + $this->assertEqual(trim($after_clear), $base_expression, "After clearing, base expression is restored for case $index"); + + $cleared = search_expression_extract($after_clear, $case[0]); + $this->assertFalse(isset($cleared), "After clearing, value could not be retrieved for case $index"); + } + } +} + +/** + * Tests that comment count display toggles properly on comment status of node + * + * Issue 537278 + * + * - Nodes with comment status set to Open should always how comment counts + * - Nodes with comment status set to Closed should show comment counts + * only when there are comments + * - Nodes with comment status set to Hidden should never show comment counts + */ +class SearchCommentCountToggleTestCase extends DrupalWebTestCase { + protected $searching_user; + protected $searchable_nodes; + + public static function getInfo() { + return array( + 'name' => 'Comment count toggle', + 'description' => 'Verify that comment count display toggles properly on comment status of node.', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search'); + + // Create searching user. + $this->searching_user = $this->drupalCreateUser(array('search content', 'access content', 'access comments', 'skip comment approval')); + + // Create initial nodes. + $node_params = array('type' => 'article', 'body' => array(LANGUAGE_NONE => array(array('value' => 'SearchCommentToggleTestCase')))); + + $this->searchable_nodes['1 comment'] = $this->drupalCreateNode($node_params); + $this->searchable_nodes['0 comments'] = $this->drupalCreateNode($node_params); + + // Login with sufficient privileges. + $this->drupalLogin($this->searching_user); + + // Create a comment array + $edit_comment = array(); + $edit_comment['subject'] = $this->randomName(); + $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = $this->randomName(); + $filtered_html_format_id = 'filtered_html'; + $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $filtered_html_format_id; + + // Post comment to the test node with comment + $this->drupalPost('comment/reply/' . $this->searchable_nodes['1 comment']->nid, $edit_comment, t('Save')); + + // First update the index. This does the initial processing. + node_update_index(); + + // Then, run the shutdown function. Testing is a unique case where indexing + // and searching has to happen in the same request, so running the shutdown + // function manually is needed to finish the indexing process. + search_update_totals(); + } + + /** + * Verify that comment count display toggles properly on comment status of node + */ + function testSearchCommentCountToggle() { + // Search for the nodes by string in the node body. + $edit = array( + 'search_block_form' => "'SearchCommentToggleTestCase'", + ); + + // Test comment count display for nodes with comment status set to Open + $this->drupalPost('', $edit, t('Search')); + $this->assertText(t('0 comments'), t('Empty comment count displays for nodes with comment status set to Open')); + $this->assertText(t('1 comment'), t('Non-empty comment count displays for nodes with comment status set to Open')); + + // Test comment count display for nodes with comment status set to Closed + $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_CLOSED; + node_save($this->searchable_nodes['0 comments']); + $this->searchable_nodes['1 comment']->comment = COMMENT_NODE_CLOSED; + node_save($this->searchable_nodes['1 comment']); + + $this->drupalPost('', $edit, t('Search')); + $this->assertNoText(t('0 comments'), t('Empty comment count does not display for nodes with comment status set to Closed')); + $this->assertText(t('1 comment'), t('Non-empty comment count displays for nodes with comment status set to Closed')); + + // Test comment count display for nodes with comment status set to Hidden + $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_HIDDEN; + node_save($this->searchable_nodes['0 comments']); + $this->searchable_nodes['1 comment']->comment = COMMENT_NODE_HIDDEN; + node_save($this->searchable_nodes['1 comment']); + + $this->drupalPost('', $edit, t('Search')); + $this->assertNoText(t('0 comments'), t('Empty comment count does not display for nodes with comment status set to Hidden')); + $this->assertNoText(t('1 comment'), t('Non-empty comment count does not display for nodes with comment status set to Hidden')); + } +} + +/** + * Test search_simplify() on every Unicode character, and some other cases. + */ +class SearchSimplifyTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Search simplify', + 'description' => 'Check that the search_simply() function works as intended.', + 'group' => 'Search', + ); + } + + /** + * Tests that all Unicode characters simplify correctly. + */ + function testSearchSimplifyUnicode() { + // This test uses a file that was constructed so that the even lines are + // boundary characters, and the odd lines are valid word characters. (It + // was generated as a sequence of all the Unicode characters, and then the + // boundary chararacters (punctuation, spaces, etc.) were split off into + // their own lines). So the even-numbered lines should simplify to nothing, + // and the odd-numbered lines we need to split into shorter chunks and + // verify that simplification doesn't lose any characters. + $input = file_get_contents(DRUPAL_ROOT . '/core/modules/search/tests/UnicodeTest.txt'); + $basestrings = explode(chr(10), $input); + $strings = array(); + foreach ($basestrings as $key => $string) { + if ($key %2) { + // Even line - should simplify down to a space. + $simplified = search_simplify($string); + $this->assertIdentical($simplified, ' ', "Line $key is excluded from the index"); + } + else { + // Odd line, should be word characters. + // Split this into 30-character chunks, so we don't run into limits + // of truncation in search_simplify(). + $start = 0; + while ($start < drupal_strlen($string)) { + $newstr = drupal_substr($string, $start, 30); + // Special case: leading zeros are removed from numeric strings, + // and there's one string in this file that is numbers starting with + // zero, so prepend a 1 on that string. + if (preg_match('/^[0-9]+$/', $newstr)) { + $newstr = '1' . $newstr; + } + $strings[] = $newstr; + $start += 30; + } + } + } + foreach ($strings as $key => $string) { + $simplified = search_simplify($string); + $this->assertTrue(drupal_strlen($simplified) >= drupal_strlen($string), "Nothing is removed from string $key."); + } + + // Test the low-numbered ASCII control characters separately. They are not + // in the text file because they are problematic for diff, especially \0. + $string = ''; + for ($i = 0; $i < 32; $i++) { + $string .= chr($i); + } + $this->assertIdentical(' ', search_simplify($string), t('Search simplify works for ASCII control characters.')); + } + + /** + * Tests that search_simplify() does the right thing with punctuation. + */ + function testSearchSimplifyPunctuation() { + $cases = array( + array('20.03/94-28,876', '20039428876', 'Punctuation removed from numbers'), + array('great...drupal--module', 'great drupal module', 'Multiple dot and dashes are word boundaries'), + array('very_great-drupal.module', 'verygreatdrupalmodule', 'Single dot, dash, underscore are removed'), + array('regular,punctuation;word', 'regular punctuation word', 'Punctuation is a word boundary'), + ); + + foreach ($cases as $case) { + $out = trim(search_simplify($case[0])); + $this->assertEqual($out, $case[1], $case[2]); + } + } +} + + +/** + * Tests keywords and conditions. + */ +class SearchKeywordsConditions extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Keywords and conditions', + 'description' => 'Verify the search pulls in keywords and extra conditions.', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search', 'search_extra_type'); + // Create searching user. + $this->searching_user = $this->drupalCreateUser(array('search content', 'access content', 'access comments', 'skip comment approval')); + // Login with sufficient privileges. + $this->drupalLogin($this->searching_user); + // Test with all search modules enabled. + variable_set('search_active_modules', array('node' => 'node', 'user' => 'user', 'search_extra_type' => 'search_extra_type')); + menu_rebuild(); + } + + /** + * Verify the kewords are captured and conditions respected. + */ + function testSearchKeyswordsConditions() { + // No keys, not conditions - no results. + $this->drupalGet('search/dummy_path'); + $this->assertNoText('Dummy search snippet to display'); + // With keys - get results. + $keys = 'bike shed ' . $this->randomName(); + $this->drupalGet("search/dummy_path/{$keys}"); + $this->assertText("Dummy search snippet to display. Keywords: {$keys}"); + $keys = 'blue drop ' . $this->randomName(); + $this->drupalGet("search/dummy_path", array('query' => array('keys' => $keys))); + $this->assertText("Dummy search snippet to display. Keywords: {$keys}"); + // Add some conditions and keys. + $keys = 'moving drop ' . $this->randomName(); + $this->drupalGet("search/dummy_path/bike", array('query' => array('search_conditions' => $keys))); + $this->assertText("Dummy search snippet to display."); + $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE)); + // Add some conditions and no keys. + $keys = 'drop kick ' . $this->randomName(); + $this->drupalGet("search/dummy_path", array('query' => array('search_conditions' => $keys))); + $this->assertText("Dummy search snippet to display."); + $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE)); + } +} + +/** + * Tests that numbers can be searched. + */ +class SearchNumbersTestCase extends DrupalWebTestCase { + protected $test_user; + protected $numbers; + protected $nodes; + + public static function getInfo() { + return array( + 'name' => 'Search numbers', + 'description' => 'Check that numbers can be searched', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search'); + + $this->test_user = $this->drupalCreateUser(array('search content', 'access content', 'administer nodes', 'access site reports')); + $this->drupalLogin($this->test_user); + + // Create content with various numbers in it. + // Note: 50 characters is the current limit of the search index's word + // field. + $this->numbers = array( + 'ISBN' => '978-0446365383', + 'UPC' => '036000 291452', + 'EAN bar code' => '5901234123457', + 'negative' => '-123456.7890', + 'quoted negative' => '"-123456.7890"', + 'leading zero' => '0777777777', + 'tiny' => '111', + 'small' => '22222222222222', + 'medium' => '333333333333333333333333333', + 'large' => '444444444444444444444444444444444444444', + 'gigantic' => '5555555555555555555555555555555555555555555555555', + 'over fifty characters' => '666666666666666666666666666666666666666666666666666666666666', + 'date', '01/02/2009', + 'commas', '987,654,321', + ); + + foreach ($this->numbers as $doc => $num) { + $info = array( + 'body' => array(LANGUAGE_NONE => array(array('value' => $num))), + 'type' => 'page', + 'language' => LANGUAGE_NONE, + 'title' => $doc . ' number', + ); + $this->nodes[$doc] = $this->drupalCreateNode($info); + } + + // Run cron to ensure the content is indexed. + $this->cronRun(); + $this->drupalGet('admin/reports/dblog'); + $this->assertText(t('Cron run completed'), 'Log shows cron run completed'); + } + + /** + * Tests that all the numbers can be searched. + */ + function testNumberSearching() { + $types = array_keys($this->numbers); + + foreach ($types as $type) { + $number = $this->numbers[$type]; + // If the number is negative, remove the - sign, because - indicates + // "not keyword" when searching. + $number = ltrim($number, '-'); + $node = $this->nodes[$type]; + + // Verify that the node title does not appear on the search page + // with a dummy search. + $this->drupalPost('search/node', + array('keys' => 'foo'), + t('Search')); + $this->assertNoText($node->title, $type . ': node title not shown in dummy search'); + + // Verify that the node title does appear as a link on the search page + // when searching for the number. + $this->drupalPost('search/node', + array('keys' => $number), + t('Search')); + $this->assertText($node->title, $type . ': node title shown (search found the node) in search for number ' . $number); + } + } +} + +/** + * Tests that numbers can be searched, with more complex matching. + */ +class SearchNumberMatchingTestCase extends DrupalWebTestCase { + protected $test_user; + protected $numbers; + protected $nodes; + + public static function getInfo() { + return array( + 'name' => 'Search number matching', + 'description' => 'Check that numbers can be searched with more complex matching', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search'); + + $this->test_user = $this->drupalCreateUser(array('search content', 'access content', 'administer nodes', 'access site reports')); + $this->drupalLogin($this->test_user); + + // Define a group of numbers that should all match each other -- + // numbers with internal punctuation should match each other, as well + // as numbers with and without leading zeros and leading/trailing + // . and -. + $this->numbers = array( + '123456789', + '12/34/56789', + '12.3456789', + '12-34-56789', + '123,456,789', + '-123456789', + '0123456789', + ); + + foreach ($this->numbers as $num) { + $info = array( + 'body' => array(LANGUAGE_NONE => array(array('value' => $num))), + 'type' => 'page', + 'language' => LANGUAGE_NONE, + ); + $this->nodes[] = $this->drupalCreateNode($info); + } + + // Run cron to ensure the content is indexed. + $this->cronRun(); + $this->drupalGet('admin/reports/dblog'); + $this->assertText(t('Cron run completed'), 'Log shows cron run completed'); + } + + /** + * Tests that all the numbers can be searched. + */ + function testNumberSearching() { + for ($i = 0; $i < count($this->numbers); $i++) { + $node = $this->nodes[$i]; + + // Verify that the node title does not appear on the search page + // with a dummy search. + $this->drupalPost('search/node', + array('keys' => 'foo'), + t('Search')); + $this->assertNoText($node->title, $i . ': node title not shown in dummy search'); + + // Now verify that we can find node i by searching for any of the + // numbers. + for ($j = 0; $j < count($this->numbers); $j++) { + $number = $this->numbers[$j]; + // If the number is negative, remove the - sign, because - indicates + // "not keyword" when searching. + $number = ltrim($number, '-'); + + $this->drupalPost('search/node', + array('keys' => $number), + t('Search')); + $this->assertText($node->title, $i . ': node title shown (search found the node) in search for number ' . $number); + } + } + + } +} + +/** + * Test config page. + */ +class SearchConfigSettingsForm extends DrupalWebTestCase { + public $search_user; + public $search_node; + + public static function getInfo() { + return array( + 'name' => 'Config settings form', + 'description' => 'Verify the search config settings form.', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search', 'search_extra_type'); + + // Login as a user that can create and search content. + $this->search_user = $this->drupalCreateUser(array('search content', 'administer search', 'administer nodes', 'bypass node access', 'access user profiles', 'administer users', 'administer blocks')); + $this->drupalLogin($this->search_user); + + // Add a single piece of content and index it. + $node = $this->drupalCreateNode(); + $this->search_node = $node; + // Link the node to itself to test that it's only indexed once. The content + // also needs the word "pizza" so we can use it as the search keyword. + $langcode = LANGUAGE_NONE; + $body_key = "body[$langcode][0][value]"; + $edit[$body_key] = l($node->title, 'node/' . $node->nid) . ' pizza sandwich'; + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + + node_update_index(); + search_update_totals(); + + // Enable the search block. + $edit = array(); + $edit['blocks[search_form][region]'] = 'content'; + $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); + } + + /** + * Verify the search settings form. + */ + function testSearchSettingsPage() { + + // Test that the settings form displays the correct count of items left to index. + $this->drupalGet('admin/config/search/settings'); + $this->assertText(t('There are @count items left to index.', array('@count' => 0))); + + // Test the re-index button. + $this->drupalPost('admin/config/search/settings', array(), t('Re-index site')); + $this->assertText(t('Are you sure you want to re-index the site')); + $this->drupalPost('admin/config/search/settings/reindex', array(), t('Re-index site')); + $this->assertText(t('The index will be rebuilt')); + $this->drupalGet('admin/config/search/settings'); + $this->assertText(t('There is 1 item left to index.')); + } + + /** + * Verify that you can disable individual search modules. + */ + function testSearchModuleDisabling() { + // Array of search modules to test: 'path' is the search path, 'title' is + // the tab title, 'keys' are the keywords to search for, and 'text' is + // the text to assert is on the results page. + $module_info = array( + 'node' => array( + 'path' => 'node', + 'title' => 'Content', + 'keys' => 'pizza', + 'text' => $this->search_node->title, + ), + 'user' => array( + 'path' => 'user', + 'title' => 'User', + 'keys' => $this->search_user->name, + 'text' => $this->search_user->mail, + ), + 'search_extra_type' => array( + 'path' => 'dummy_path', + 'title' => 'Dummy search type', + 'keys' => 'foo', + 'text' => 'Dummy search snippet to display', + ), + ); + $modules = array_keys($module_info); + + // Test each module if it's enabled as the only search module. + foreach ($modules as $module) { + // Enable the one module and disable other ones. + $info = $module_info[$module]; + $edit = array(); + foreach ($modules as $other) { + $edit['search_active_modules[' . $other . ']'] = (($other == $module) ? $module : FALSE); + } + $edit['search_default_module'] = $module; + $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration')); + + // Run a search from the correct search URL. + $this->drupalGet('search/' . $info['path'] . '/' . $info['keys']); + $this->assertNoText('no results', $info['title'] . ' search found results'); + $this->assertText($info['text'], 'Correct search text found'); + + // Verify that other module search tab titles are not visible. + foreach ($modules as $other) { + if ($other != $module) { + $title = $module_info[$other]['title']; + $this->assertNoText($title, $title . ' search tab is not shown'); + } + } + + // Run a search from the search block on the node page. Verify you get + // to this module's search results page. + $terms = array('search_block_form' => $info['keys']); + $this->drupalPost('node', $terms, t('Search')); + $this->assertEqual( + $this->getURL(), + url('search/' . $info['path'] . '/' . $info['keys'], array('absolute' => TRUE)), + 'Block redirected to right search page'); + + // Try an invalid search path. Should redirect to our active module. + $this->drupalGet('search/not_a_module_path'); + $this->assertEqual( + $this->getURL(), + url('search/' . $info['path'], array('absolute' => TRUE)), + 'Invalid search path redirected to default search page'); + } + + // Test with all search modules enabled. When you go to the search + // page or run search, all modules should be shown. + $edit = array(); + foreach ($modules as $module) { + $edit['search_active_modules[' . $module . ']'] = $module; + } + $edit['search_default_module'] = 'node'; + + $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration')); + + foreach (array('search/node/pizza', 'search/node') as $path) { + $this->drupalGet($path); + foreach ($modules as $module) { + $title = $module_info[$module]['title']; + $this->assertText($title, $title . ' search tab is shown'); + } + } + } +} + +/** + * Tests the search_excerpt() function. + */ +class SearchExcerptTestCase extends DrupalUnitTestCase { + public static function getInfo() { + return array( + 'name' => 'Search excerpt extraction', + 'description' => 'Tests that the search_excerpt() function works.', + 'group' => 'Search', + ); + } + + function setUp() { + drupal_load('module', 'search'); + parent::setUp(); + } + + /** + * Tests search_excerpt() with several simulated search keywords. + * + * Passes keywords and a sample marked up string, "The quick + * brown fox jumps over the lazy dog", and compares it to the + * correctly marked up string. The correctly marked up string + * contains either highlighted keywords or the original marked + * up string if no keywords matched the string. + */ + function testSearchExcerpt() { + // Make some text with entities and tags. + $text = 'The quick brown fox & jumps

    over

    the lazy dog'; + // Note: The search_excerpt() function adds some extra spaces -- not + // important for HTML formatting. Remove these for comparison. + $expected = 'The quick brown fox & jumps over the lazy dog'; + $result = preg_replace('| +|', ' ', search_excerpt('nothing', $text)); + $this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Entire string is returned when keyword is not found in short string'); + + $result = preg_replace('| +|', ' ', search_excerpt('fox', $text)); + $this->assertEqual($result, 'The quick brown fox & jumps over the lazy dog ...', 'Found keyword is highlighted'); + + $longtext = str_repeat($text . ' ', 10); + $result = preg_replace('| +|', ' ', search_excerpt('nothing', $text)); + $this->assertTrue(strpos($result, $expected) === 0, 'When keyword is not found in long string, return value starts as expected'); + + $entities = str_repeat('készítése ', 20); + $result = preg_replace('| +|', ' ', search_excerpt('nothing', $entities)); + $this->assertFalse(strpos($result, '&'), 'Entities are not present in excerpt'); + $this->assertTrue(strpos($result, 'í') > 0, 'Entities are converted in excerpt'); + } + + /** + * Tests search_excerpt() with search keywords matching simplified words. + * + * Excerpting should handle keywords that are matched only after going through + * search_simplify(). This test passes keywords that match simplified words + * and compares them with strings that contain the original unsimplified word. + */ + function testSearchExcerptSimplified() { + $lorem1 = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae arcu at leo cursus laoreet. Curabitur dui tortor, adipiscing malesuada tempor in, bibendum ac diam. Cras non tellus a libero pellentesque condimentum. What is a Drupalism? Suspendisse ac lacus libero. Ut non est vel nisl faucibus interdum nec sed leo. Pellentesque sem risus, vulputate eu semper eget, auctor in libero.'; + $lorem2 = 'Ut fermentum est vitae metus convallis scelerisque. Phasellus pellentesque rhoncus tellus, eu dignissim purus posuere id. Quisque eu fringilla ligula. Morbi ullamcorper, lorem et mattis egestas, tortor neque pretium velit, eget eleifend odio turpis eu purus. Donec vitae metus quis leo pretium tincidunt a pulvinar sem. Morbi adipiscing laoreet mauris vel placerat. Nullam elementum, nisl sit amet scelerisque malesuada, dolor nunc hendrerit quam, eu ultrices erat est in orci.'; + + // Make some text with some keywords that will get simplified. + $text = $lorem1 . ' Number: 123456.7890 Hyphenated: one-two abc,def ' . $lorem2; + // Note: The search_excerpt() function adds some extra spaces -- not + // important for HTML formatting. Remove these for comparison. + $result = preg_replace('| +|', ' ', search_excerpt('123456.7890', $text)); + $this->assertTrue(strpos($result, 'Number: 123456.7890') !== FALSE, 'Numeric keyword is highlighted with exact match'); + + $result = preg_replace('| +|', ' ', search_excerpt('1234567890', $text)); + $this->assertTrue(strpos($result, 'Number: 123456.7890') !== FALSE, 'Numeric keyword is highlighted with simplified match'); + + $result = preg_replace('| +|', ' ', search_excerpt('Number 1234567890', $text)); + $this->assertTrue(strpos($result, 'Number: 123456.7890') !== FALSE, 'Punctuated and numeric keyword is highlighted with simplified match'); + + $result = preg_replace('| +|', ' ', search_excerpt('"Number 1234567890"', $text)); + $this->assertTrue(strpos($result, 'Number: 123456.7890') !== FALSE, 'Phrase with punctuated and numeric keyword is highlighted with simplified match'); + + $result = preg_replace('| +|', ' ', search_excerpt('"Hyphenated onetwo"', $text)); + $this->assertTrue(strpos($result, 'Hyphenated: one-two') !== FALSE, 'Phrase with punctuated and hyphenated keyword is highlighted with simplified match'); + + $result = preg_replace('| +|', ' ', search_excerpt('"abc def"', $text)); + $this->assertTrue(strpos($result, 'abc,def') !== FALSE, 'Phrase with keyword simplified into two separate words is highlighted with simplified match'); + } +} + +/** + * Test the CJK tokenizer. + */ +class SearchTokenizerTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'CJK tokenizer', + 'description' => 'Check that CJK tokenizer works as intended.', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search'); + } + + /** + * Verifies that strings of CJK characters are tokenized. + * + * The search_simplify() function does special things with numbers, symbols, + * and punctuation. So we only test that CJK characters that are not in these + * character classes are tokenized properly. See PREG_CLASS_CKJ for more + * information. + */ + function testTokenizer() { + // Set the minimum word size to 1 (to split all CJK characters) and make + // sure CJK tokenizing is turned on. + variable_set('minimum_word_size', 1); + variable_set('overlap_cjk', TRUE); + $this->refreshVariables(); + + // Create a string of CJK characters from various character ranges in + // the Unicode tables. + + // Beginnings of the character ranges. + $starts = array( + 'CJK unified' => 0x4e00, + 'CJK Ext A' => 0x3400, + 'CJK Compat' => 0xf900, + 'Hangul Jamo' => 0x1100, + 'Hangul Ext A' => 0xa960, + 'Hangul Ext B' => 0xd7b0, + 'Hangul Compat' => 0x3131, + 'Half non-punct 1' => 0xff21, + 'Half non-punct 2' => 0xff41, + 'Half non-punct 3' => 0xff66, + 'Hangul Syllables' => 0xac00, + 'Hiragana' => 0x3040, + 'Katakana' => 0x30a1, + 'Katakana Ext' => 0x31f0, + 'CJK Reserve 1' => 0x20000, + 'CJK Reserve 2' => 0x30000, + 'Bomofo' => 0x3100, + 'Bomofo Ext' => 0x31a0, + 'Lisu' => 0xa4d0, + 'Yi' => 0xa000, + ); + + // Ends of the character ranges. + $ends = array( + 'CJK unified' => 0x9fcf, + 'CJK Ext A' => 0x4dbf, + 'CJK Compat' => 0xfaff, + 'Hangul Jamo' => 0x11ff, + 'Hangul Ext A' => 0xa97f, + 'Hangul Ext B' => 0xd7ff, + 'Hangul Compat' => 0x318e, + 'Half non-punct 1' => 0xff3a, + 'Half non-punct 2' => 0xff5a, + 'Half non-punct 3' => 0xffdc, + 'Hangul Syllables' => 0xd7af, + 'Hiragana' => 0x309f, + 'Katakana' => 0x30ff, + 'Katakana Ext' => 0x31ff, + 'CJK Reserve 1' => 0x2fffd, + 'CJK Reserve 2' => 0x3fffd, + 'Bomofo' => 0x312f, + 'Bomofo Ext' => 0x31b7, + 'Lisu' => 0xa4fd, + 'Yi' => 0xa48f, + ); + + // Generate characters consisting of starts, midpoints, and ends. + $chars = array(); + $charcodes = array(); + foreach ($starts as $key => $value) { + $charcodes[] = $starts[$key]; + $chars[] = $this->code2utf($starts[$key]); + $mid = round(0.5 * ($starts[$key] + $ends[$key])); + $charcodes[] = $mid; + $chars[] = $this->code2utf($mid); + $charcodes[] = $ends[$key]; + $chars[] = $this->code2utf($ends[$key]); + } + + // Merge into a string and tokenize. + $string = implode('', $chars); + $out = trim(search_simplify($string)); + $expected = drupal_strtolower(implode(' ', $chars)); + + // Verify that the output matches what we expect. + $this->assertEqual($out, $expected, 'CJK tokenizer worked on all supplied CJK characters'); + } + + /** + * Verifies that strings of non-CJK characters are not tokenized. + * + * This is just a sanity check - it verifies that strings of letters are + * not tokenized. + */ + function testNoTokenizer() { + // Set the minimum word size to 1 (to split all CJK characters) and make + // sure CJK tokenizing is turned on. + variable_set('minimum_word_size', 1); + variable_set('overlap_cjk', TRUE); + $this->refreshVariables(); + + $letters = 'abcdefghijklmnopqrstuvwxyz'; + $out = trim(search_simplify($letters)); + + $this->assertEqual($letters, $out, 'Letters are not CJK tokenized'); + } + + /** + * Like PHP chr() function, but for unicode characters. + * + * chr() only works for ASCII characters up to character 255. This function + * converts a number to the corresponding unicode character. Adapted from + * functions supplied in comments on several functions on php.net. + */ + function code2utf($num) { + if ($num < 128) { + return chr($num); + } + + if ($num < 2048) { + return chr(($num >> 6) + 192) . chr(($num & 63) + 128); + } + + if ($num < 65536) { + return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128); + } + + if ($num < 2097152) { + return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128); + } + + return ''; + } +} + +/** + * Tests that we can embed a form in search results and submit it. + */ +class SearchEmbedForm extends DrupalWebTestCase { + /** + * Node used for testing. + */ + public $node; + + /** + * Count of how many times the form has been submitted. + */ + public $submit_count = 0; + + public static function getInfo() { + return array( + 'name' => 'Embedded forms', + 'description' => 'Verifies that a form embedded in search results works', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search', 'search_embedded_form'); + + // Create a user and a node, and update the search index. + $test_user = $this->drupalCreateUser(array('access content', 'search content', 'administer nodes')); + $this->drupalLogin($test_user); + + $this->node = $this->drupalCreateNode(); + + node_update_index(); + search_update_totals(); + + // Set up a dummy initial count of times the form has been submitted. + $this->submit_count = 12; + variable_set('search_embedded_form_submitted', $this->submit_count); + $this->refreshVariables(); + } + + /** + * Tests that the embedded form appears and can be submitted. + */ + function testEmbeddedForm() { + // First verify we can submit the form from the module's page. + $this->drupalPost('search_embedded_form', + array('name' => 'John'), + t('Send away')); + $this->assertText(t('Test form was submitted'), 'Form message appears'); + $count = variable_get('search_embedded_form_submitted', 0); + $this->assertEqual($this->submit_count + 1, $count, 'Form submission count is correct'); + $this->submit_count = $count; + + // Now verify that we can see and submit the form from the search results. + $this->drupalGet('search/node/' . $this->node->title); + $this->assertText(t('Your name'), 'Form is visible'); + $this->drupalPost('search/node/' . $this->node->title, + array('name' => 'John'), + t('Send away')); + $this->assertText(t('Test form was submitted'), 'Form message appears'); + $count = variable_get('search_embedded_form_submitted', 0); + $this->assertEqual($this->submit_count + 1, $count, 'Form submission count is correct'); + $this->submit_count = $count; + + // Now verify that if we submit the search form, it doesn't count as + // our form being submitted. + $this->drupalPost('search', + array('keys' => 'foo'), + t('Search')); + $this->assertNoText(t('Test form was submitted'), 'Form message does not appear'); + $count = variable_get('search_embedded_form_submitted', 0); + $this->assertEqual($this->submit_count, $count, 'Form submission count is correct'); + $this->submit_count = $count; + } +} + +/** + * Tests that hook_search_page runs. + */ +class SearchPageOverride extends DrupalWebTestCase { + public $search_user; + + public static function getInfo() { + return array( + 'name' => 'Search page override', + 'description' => 'Verify that hook_search_page can override search page display.', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search', 'search_extra_type'); + + // Login as a user that can create and search content. + $this->search_user = $this->drupalCreateUser(array('search content', 'administer search')); + $this->drupalLogin($this->search_user); + + // Enable the extra type module for searching. + variable_set('search_active_modules', array('node' => 'node', 'user' => 'user', 'search_extra_type' => 'search_extra_type')); + menu_rebuild(); + } + + function testSearchPageHook() { + $keys = 'bike shed ' . $this->randomName(); + $this->drupalGet("search/dummy_path/{$keys}"); + $this->assertText('Dummy search snippet', 'Dummy search snippet is shown'); + $this->assertText('Test page text is here', 'Page override is working'); + } +} + +/** + * Test node search with multiple languages. + */ +class SearchLanguageTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Search language selection', + 'description' => 'Tests advanced search with different languages enabled.', + 'group' => 'Search', + ); + } + + /** + * Implementation setUp(). + */ + function setUp() { + parent::setUp('search', 'locale'); + + // Create and login user. + $test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'administer nodes', 'administer languages', 'access administration pages')); + $this->drupalLogin($test_user); + } + + function testLanguages() { + // Check that there are initially no languages displayed. + $this->drupalGet('search/node'); + $this->assertNoText(t('Languages'), t('No languages to choose from.')); + + // Add predefined language. + $edit = array('langcode' => 'fr'); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + $this->assertText('fr', t('Language added successfully.')); + + // Now we should have languages displayed. + $this->drupalGet('search/node'); + $this->assertText(t('Languages'), t('Languages displayed to choose from.')); + $this->assertText(t('English'), t('English is a possible choice.')); + $this->assertText(t('French'), t('French is a possible choice.')); + + // Ensure selecting no language does not make the query different. + $this->drupalPost('search/node', array(), t('Advanced search')); + $this->assertEqual($this->getUrl(), url('search/node/', array('absolute' => TRUE)), t('Correct page redirection, no language filtering.')); + + // Pick French and ensure it is selected. + $edit = array('language[fr]' => TRUE); + $this->drupalPost('search/node', $edit, t('Advanced search')); + $this->assertFieldByXPath('//input[@name="keys"]', 'language:fr', t('Language filter added to query.')); + + // Change the default language and disable English. + $path = 'admin/config/regional/language'; + $this->drupalGet($path); + $this->assertFieldChecked('edit-site-default-en', t('English is the default language.')); + $edit = array('site_default' => 'fr'); + $this->drupalPost(NULL, $edit, t('Save configuration')); + $this->assertNoFieldChecked('edit-site-default-en', t('Default language updated.')); + $edit = array('enabled[en]' => FALSE); + $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration')); + $this->assertNoFieldChecked('edit-enabled-en', t('Language disabled.')); + + // Check that there are again no languages displayed. + $this->drupalGet('search/node'); + $this->assertNoText(t('Languages'), t('No languages to choose from.')); + } +} diff --git a/modules/search/tests/UnicodeTest.txt b/core/modules/search/tests/UnicodeTest.txt similarity index 100% rename from modules/search/tests/UnicodeTest.txt rename to core/modules/search/tests/UnicodeTest.txt diff --git a/modules/search/tests/search_embedded_form.info b/core/modules/search/tests/search_embedded_form.info similarity index 100% rename from modules/search/tests/search_embedded_form.info rename to core/modules/search/tests/search_embedded_form.info diff --git a/modules/search/tests/search_embedded_form.module b/core/modules/search/tests/search_embedded_form.module similarity index 100% rename from modules/search/tests/search_embedded_form.module rename to core/modules/search/tests/search_embedded_form.module diff --git a/modules/search/tests/search_extra_type.info b/core/modules/search/tests/search_extra_type.info similarity index 100% rename from modules/search/tests/search_extra_type.info rename to core/modules/search/tests/search_extra_type.info diff --git a/modules/search/tests/search_extra_type.module b/core/modules/search/tests/search_extra_type.module similarity index 100% rename from modules/search/tests/search_extra_type.module rename to core/modules/search/tests/search_extra_type.module diff --git a/modules/shortcut/shortcut-rtl.css b/core/modules/shortcut/shortcut-rtl.css similarity index 100% rename from modules/shortcut/shortcut-rtl.css rename to core/modules/shortcut/shortcut-rtl.css diff --git a/modules/shortcut/shortcut.admin.css b/core/modules/shortcut/shortcut.admin.css similarity index 100% rename from modules/shortcut/shortcut.admin.css rename to core/modules/shortcut/shortcut.admin.css diff --git a/modules/shortcut/shortcut.admin.inc b/core/modules/shortcut/shortcut.admin.inc similarity index 100% rename from modules/shortcut/shortcut.admin.inc rename to core/modules/shortcut/shortcut.admin.inc diff --git a/modules/shortcut/shortcut.admin.js b/core/modules/shortcut/shortcut.admin.js similarity index 100% rename from modules/shortcut/shortcut.admin.js rename to core/modules/shortcut/shortcut.admin.js diff --git a/modules/shortcut/shortcut.api.php b/core/modules/shortcut/shortcut.api.php similarity index 100% rename from modules/shortcut/shortcut.api.php rename to core/modules/shortcut/shortcut.api.php diff --git a/modules/shortcut/shortcut.css b/core/modules/shortcut/shortcut.css similarity index 100% rename from modules/shortcut/shortcut.css rename to core/modules/shortcut/shortcut.css diff --git a/modules/shortcut/shortcut.info b/core/modules/shortcut/shortcut.info similarity index 100% rename from modules/shortcut/shortcut.info rename to core/modules/shortcut/shortcut.info diff --git a/modules/shortcut/shortcut.install b/core/modules/shortcut/shortcut.install similarity index 100% rename from modules/shortcut/shortcut.install rename to core/modules/shortcut/shortcut.install diff --git a/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module similarity index 100% rename from modules/shortcut/shortcut.module rename to core/modules/shortcut/shortcut.module diff --git a/modules/shortcut/shortcut.png b/core/modules/shortcut/shortcut.png similarity index 100% rename from modules/shortcut/shortcut.png rename to core/modules/shortcut/shortcut.png diff --git a/modules/shortcut/shortcut.test b/core/modules/shortcut/shortcut.test similarity index 100% rename from modules/shortcut/shortcut.test rename to core/modules/shortcut/shortcut.test diff --git a/core/modules/simpletest/drupal_web_test_case.php b/core/modules/simpletest/drupal_web_test_case.php new file mode 100644 index 0000000..e61e59c --- /dev/null +++ b/core/modules/simpletest/drupal_web_test_case.php @@ -0,0 +1,3422 @@ + 0, + '#fail' => 0, + '#exception' => 0, + '#debug' => 0, + ); + + /** + * Assertions thrown in that test case. + * + * @var Array + */ + protected $assertions = array(); + + /** + * This class is skipped when looking for the source of an assertion. + * + * When displaying which function an assert comes from, it's not too useful + * to see "drupalWebTestCase->drupalLogin()', we would like to see the test + * that called it. So we need to skip the classes defining these helper + * methods. + */ + protected $skipClasses = array(__CLASS__ => TRUE); + + /** + * Constructor for DrupalTestCase. + * + * @param $test_id + * Tests with the same id are reported together. + */ + public function __construct($test_id = NULL) { + $this->testId = $test_id; + } + + /** + * Internal helper: stores the assert. + * + * @param $status + * Can be 'pass', 'fail', 'exception'. + * TRUE is a synonym for 'pass', FALSE for 'fail'. + * @param $message + * The message string. + * @param $group + * Which group this assert belongs to. + * @param $caller + * By default, the assert comes from a function whose name starts with + * 'test'. Instead, you can specify where this assert originates from + * by passing in an associative array as $caller. Key 'file' is + * the name of the source file, 'line' is the line number and 'function' + * is the caller function itself. + */ + protected function assert($status, $message = '', $group = 'Other', array $caller = NULL) { + // Convert boolean status to string status. + if (is_bool($status)) { + $status = $status ? 'pass' : 'fail'; + } + + // Increment summary result counter. + $this->results['#' . $status]++; + + // Get the function information about the call to the assertion method. + if (!$caller) { + $caller = $this->getAssertionCall(); + } + + // Creation assertion array that can be displayed while tests are running. + $this->assertions[] = $assertion = array( + 'test_id' => $this->testId, + 'test_class' => get_class($this), + 'status' => $status, + 'message' => $message, + 'message_group' => $group, + 'function' => $caller['function'], + 'line' => $caller['line'], + 'file' => $caller['file'], + ); + + // Store assertion for display after the test has completed. + Database::getConnection('default', 'simpletest_original_default') + ->insert('simpletest') + ->fields($assertion) + ->execute(); + + // We do not use a ternary operator here to allow a breakpoint on + // test failure. + if ($status == 'pass') { + return TRUE; + } + else { + return FALSE; + } + } + + /** + * Store an assertion from outside the testing context. + * + * This is useful for inserting assertions that can only be recorded after + * the test case has been destroyed, such as PHP fatal errors. The caller + * information is not automatically gathered since the caller is most likely + * inserting the assertion on behalf of other code. In all other respects + * the method behaves just like DrupalTestCase::assert() in terms of storing + * the assertion. + * + * @return + * Message ID of the stored assertion. + * + * @see DrupalTestCase::assert() + * @see DrupalTestCase::deleteAssert() + */ + public static function insertAssert($test_id, $test_class, $status, $message = '', $group = 'Other', array $caller = array()) { + // Convert boolean status to string status. + if (is_bool($status)) { + $status = $status ? 'pass' : 'fail'; + } + + $caller += array( + 'function' => t('Unknown'), + 'line' => 0, + 'file' => t('Unknown'), + ); + + $assertion = array( + 'test_id' => $test_id, + 'test_class' => $test_class, + 'status' => $status, + 'message' => $message, + 'message_group' => $group, + 'function' => $caller['function'], + 'line' => $caller['line'], + 'file' => $caller['file'], + ); + + return db_insert('simpletest') + ->fields($assertion) + ->execute(); + } + + /** + * Delete an assertion record by message ID. + * + * @param $message_id + * Message ID of the assertion to delete. + * @return + * TRUE if the assertion was deleted, FALSE otherwise. + * + * @see DrupalTestCase::insertAssert() + */ + public static function deleteAssert($message_id) { + return (bool) db_delete('simpletest') + ->condition('message_id', $message_id) + ->execute(); + } + + /** + * Cycles through backtrace until the first non-assertion method is found. + * + * @return + * Array representing the true caller. + */ + protected function getAssertionCall() { + $backtrace = debug_backtrace(); + + // The first element is the call. The second element is the caller. + // We skip calls that occurred in one of the methods of our base classes + // or in an assertion function. + while (($caller = $backtrace[1]) && + ((isset($caller['class']) && isset($this->skipClasses[$caller['class']])) || + substr($caller['function'], 0, 6) == 'assert')) { + // We remove that call. + array_shift($backtrace); + } + + return _drupal_get_last_caller($backtrace); + } + + /** + * Check to see if a value is not false (not an empty string, 0, NULL, or FALSE). + * + * @param $value + * The value on which the assertion is to be done. + * @param $message + * The message to display along with the assertion. + * @param $group + * The type of assertion - examples are "Browser", "PHP". + * @return + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertTrue($value, $message = '', $group = 'Other') { + return $this->assert((bool) $value, $message ? $message : t('Value @value is TRUE.', array('@value' => var_export($value, TRUE))), $group); + } + + /** + * Check to see if a value is false (an empty string, 0, NULL, or FALSE). + * + * @param $value + * The value on which the assertion is to be done. + * @param $message + * The message to display along with the assertion. + * @param $group + * The type of assertion - examples are "Browser", "PHP". + * @return + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertFalse($value, $message = '', $group = 'Other') { + return $this->assert(!$value, $message ? $message : t('Value @value is FALSE.', array('@value' => var_export($value, TRUE))), $group); + } + + /** + * Check to see if a value is NULL. + * + * @param $value + * The value on which the assertion is to be done. + * @param $message + * The message to display along with the assertion. + * @param $group + * The type of assertion - examples are "Browser", "PHP". + * @return + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertNull($value, $message = '', $group = 'Other') { + return $this->assert(!isset($value), $message ? $message : t('Value @value is NULL.', array('@value' => var_export($value, TRUE))), $group); + } + + /** + * Check to see if a value is not NULL. + * + * @param $value + * The value on which the assertion is to be done. + * @param $message + * The message to display along with the assertion. + * @param $group + * The type of assertion - examples are "Browser", "PHP". + * @return + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertNotNull($value, $message = '', $group = 'Other') { + return $this->assert(isset($value), $message ? $message : t('Value @value is not NULL.', array('@value' => var_export($value, TRUE))), $group); + } + + /** + * Check to see if two values are equal. + * + * @param $first + * The first value to check. + * @param $second + * The second value to check. + * @param $message + * The message to display along with the assertion. + * @param $group + * The type of assertion - examples are "Browser", "PHP". + * @return + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertEqual($first, $second, $message = '', $group = 'Other') { + return $this->assert($first == $second, $message ? $message : t('Value @first is equal to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group); + } + + /** + * Check to see if two values are not equal. + * + * @param $first + * The first value to check. + * @param $second + * The second value to check. + * @param $message + * The message to display along with the assertion. + * @param $group + * The type of assertion - examples are "Browser", "PHP". + * @return + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertNotEqual($first, $second, $message = '', $group = 'Other') { + return $this->assert($first != $second, $message ? $message : t('Value @first is not equal to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group); + } + + /** + * Check to see if two values are identical. + * + * @param $first + * The first value to check. + * @param $second + * The second value to check. + * @param $message + * The message to display along with the assertion. + * @param $group + * The type of assertion - examples are "Browser", "PHP". + * @return + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertIdentical($first, $second, $message = '', $group = 'Other') { + return $this->assert($first === $second, $message ? $message : t('Value @first is identical to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group); + } + + /** + * Check to see if two values are not identical. + * + * @param $first + * The first value to check. + * @param $second + * The second value to check. + * @param $message + * The message to display along with the assertion. + * @param $group + * The type of assertion - examples are "Browser", "PHP". + * @return + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertNotIdentical($first, $second, $message = '', $group = 'Other') { + return $this->assert($first !== $second, $message ? $message : t('Value @first is not identical to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group); + } + + /** + * Fire an assertion that is always positive. + * + * @param $message + * The message to display along with the assertion. + * @param $group + * The type of assertion - examples are "Browser", "PHP". + * @return + * TRUE. + */ + protected function pass($message = NULL, $group = 'Other') { + return $this->assert(TRUE, $message, $group); + } + + /** + * Fire an assertion that is always negative. + * + * @param $message + * The message to display along with the assertion. + * @param $group + * The type of assertion - examples are "Browser", "PHP". + * @return + * FALSE. + */ + protected function fail($message = NULL, $group = 'Other') { + return $this->assert(FALSE, $message, $group); + } + + /** + * Fire an error assertion. + * + * @param $message + * The message to display along with the assertion. + * @param $group + * The type of assertion - examples are "Browser", "PHP". + * @param $caller + * The caller of the error. + * @return + * FALSE. + */ + protected function error($message = '', $group = 'Other', array $caller = NULL) { + if ($group == 'User notice') { + // Since 'User notice' is set by trigger_error() which is used for debug + // set the message to a status of 'debug'. + return $this->assert('debug', $message, 'Debug', $caller); + } + + return $this->assert('exception', $message, $group, $caller); + } + + /** + * Logs verbose message in a text file. + * + * The a link to the vebose message will be placed in the test results via + * as a passing assertion with the text '[verbose message]'. + * + * @param $message + * The verbose message to be stored. + * + * @see simpletest_verbose() + */ + protected function verbose($message) { + if ($id = simpletest_verbose($message)) { + $url = file_create_url($this->originalFileDirectory . '/simpletest/verbose/' . get_class($this) . '-' . $id . '.html'); + $this->error(l(t('Verbose message'), $url, array('attributes' => array('target' => '_blank'))), 'User notice'); + } + } + + /** + * Run all tests in this class. + * + * Regardless of whether $methods are passed or not, only method names + * starting with "test" are executed. + * + * @param $methods + * (optional) A list of method names in the test case class to run; e.g., + * array('testFoo', 'testBar'). By default, all methods of the class are + * taken into account, but it can be useful to only run a few selected test + * methods during debugging. + */ + public function run(array $methods = array()) { + // Initialize verbose debugging. + simpletest_verbose(NULL, variable_get('file_public_path', conf_path() . '/files'), get_class($this)); + + // HTTP auth settings (:) for the simpletest browser + // when sending requests to the test site. + $this->httpauth_method = variable_get('simpletest_httpauth_method', CURLAUTH_BASIC); + $username = variable_get('simpletest_httpauth_username', NULL); + $password = variable_get('simpletest_httpauth_password', NULL); + if ($username && $password) { + $this->httpauth_credentials = $username . ':' . $password; + } + + set_error_handler(array($this, 'errorHandler')); + $class = get_class($this); + // Iterate through all the methods in this class, unless a specific list of + // methods to run was passed. + $class_methods = get_class_methods($class); + if ($methods) { + $class_methods = array_intersect($class_methods, $methods); + } + foreach ($class_methods as $method) { + // If the current method starts with "test", run it - it's a test. + if (strtolower(substr($method, 0, 4)) == 'test') { + // Insert a fail record. This will be deleted on completion to ensure + // that testing completed. + $method_info = new ReflectionMethod($class, $method); + $caller = array( + 'file' => $method_info->getFileName(), + 'line' => $method_info->getStartLine(), + 'function' => $class . '->' . $method . '()', + ); + $completion_check_id = DrupalTestCase::insertAssert($this->testId, $class, FALSE, t('The test did not complete due to a fatal error.'), 'Completion check', $caller); + $this->setUp(); + try { + $this->$method(); + // Finish up. + } + catch (Exception $e) { + $this->exceptionHandler($e); + } + $this->tearDown(); + // Remove the completion check record. + DrupalTestCase::deleteAssert($completion_check_id); + } + } + // Clear out the error messages and restore error handler. + drupal_get_messages(); + restore_error_handler(); + } + + /** + * Handle errors during test runs. + * + * Because this is registered in set_error_handler(), it has to be public. + * @see set_error_handler + */ + public function errorHandler($severity, $message, $file = NULL, $line = NULL) { + if ($severity & error_reporting()) { + $error_map = array( + E_STRICT => 'Run-time notice', + E_WARNING => 'Warning', + E_NOTICE => 'Notice', + E_CORE_ERROR => 'Core error', + E_CORE_WARNING => 'Core warning', + E_USER_ERROR => 'User error', + E_USER_WARNING => 'User warning', + E_USER_NOTICE => 'User notice', + E_RECOVERABLE_ERROR => 'Recoverable error', + ); + + $backtrace = debug_backtrace(); + $this->error($message, $error_map[$severity], _drupal_get_last_caller($backtrace)); + } + return TRUE; + } + + /** + * Handle exceptions. + * + * @see set_exception_handler + */ + protected function exceptionHandler($exception) { + $backtrace = $exception->getTrace(); + // Push on top of the backtrace the call that generated the exception. + array_unshift($backtrace, array( + 'line' => $exception->getLine(), + 'file' => $exception->getFile(), + )); + require_once DRUPAL_ROOT . '/core/includes/errors.inc'; + // The exception message is run through check_plain() by _drupal_decode_exception(). + $this->error(t('%type: !message in %function (line %line of %file).', _drupal_decode_exception($exception)), 'Uncaught exception', _drupal_get_last_caller($backtrace)); + } + + /** + * Generates a random string of ASCII characters of codes 32 to 126. + * + * The generated string includes alpha-numeric characters and common misc + * characters. Use this method when testing general input where the content + * is not restricted. + * + * @param $length + * Length of random string to generate. + * @return + * Randomly generated string. + */ + public static function randomString($length = 8) { + $str = ''; + for ($i = 0; $i < $length; $i++) { + $str .= chr(mt_rand(32, 126)); + } + return $str; + } + + /** + * Generates a random string containing letters and numbers. + * + * The string will always start with a letter. The letters may be upper or + * lower case. This method is better for restricted inputs that do not + * accept certain characters. For example, when testing input fields that + * require machine readable values (i.e. without spaces and non-standard + * characters) this method is best. + * + * @param $length + * Length of random string to generate. + * @return + * Randomly generated string. + */ + public static function randomName($length = 8) { + $values = array_merge(range(65, 90), range(97, 122), range(48, 57)); + $max = count($values) - 1; + $str = chr(mt_rand(97, 122)); + for ($i = 1; $i < $length; $i++) { + $str .= chr($values[mt_rand(0, $max)]); + } + return $str; + } + + /** + * Converts a list of possible parameters into a stack of permutations. + * + * Takes a list of parameters containing possible values, and converts all of + * them into a list of items containing every possible permutation. + * + * Example: + * @code + * $parameters = array( + * 'one' => array(0, 1), + * 'two' => array(2, 3), + * ); + * $permutations = $this->permute($parameters); + * // Result: + * $permutations == array( + * array('one' => 0, 'two' => 2), + * array('one' => 1, 'two' => 2), + * array('one' => 0, 'two' => 3), + * array('one' => 1, 'two' => 3), + * ) + * @endcode + * + * @param $parameters + * An associative array of parameters, keyed by parameter name, and whose + * values are arrays of parameter values. + * + * @return + * A list of permutations, which is an array of arrays. Each inner array + * contains the full list of parameters that have been passed, but with a + * single value only. + */ + public static function generatePermutations($parameters) { + $all_permutations = array(array()); + foreach ($parameters as $parameter => $values) { + $new_permutations = array(); + // Iterate over all values of the parameter. + foreach ($values as $value) { + // Iterate over all existing permutations. + foreach ($all_permutations as $permutation) { + // Add the new parameter value to existing permutations. + $new_permutations[] = $permutation + array($parameter => $value); + } + } + // Replace the old permutations with the new permutations. + $all_permutations = $new_permutations; + } + return $all_permutations; + } +} + +/** + * Test case for Drupal unit tests. + * + * These tests can not access the database nor files. Calling any Drupal + * function that needs the database will throw exceptions. These include + * watchdog(), module_implements(), module_invoke_all() etc. + */ +class DrupalUnitTestCase extends DrupalTestCase { + + /** + * Constructor for DrupalUnitTestCase. + */ + function __construct($test_id = NULL) { + parent::__construct($test_id); + $this->skipClasses[__CLASS__] = TRUE; + } + + /** + * Sets up unit test environment. + * + * Unlike DrupalWebTestCase::setUp(), DrupalUnitTestCase::setUp() does not + * install modules because tests are performed without accessing the database. + * Any required files must be explicitly included by the child class setUp() + * method. + */ + protected function setUp() { + global $conf; + + // Store necessary current values before switching to the test environment. + $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); + + // Reset all statics so that test is performed with a clean environment. + drupal_static_reset(); + + // Generate temporary prefixed database to ensure that tests have a clean starting point. + $this->databasePrefix = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}'); + + // Create test directory. + $public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); + file_prepare_directory($public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + $conf['file_public_path'] = $public_files_directory; + + // Clone the current connection and replace the current prefix. + $connection_info = Database::getConnectionInfo('default'); + Database::renameConnection('default', 'simpletest_original_default'); + foreach ($connection_info as $target => $value) { + $connection_info[$target]['prefix'] = array( + 'default' => $value['prefix']['default'] . $this->databasePrefix, + ); + } + Database::addConnectionInfo('default', 'default', $connection_info['default']); + + // Set user agent to be consistent with web test case. + $_SERVER['HTTP_USER_AGENT'] = $this->databasePrefix; + + // If locale is enabled then t() will try to access the database and + // subsequently will fail as the database is not accessible. + $module_list = module_list(); + if (isset($module_list['locale'])) { + $this->originalModuleList = $module_list; + unset($module_list['locale']); + module_list(TRUE, FALSE, FALSE, $module_list); + } + } + + protected function tearDown() { + global $conf; + + // Get back to the original connection. + Database::removeConnection('default'); + Database::renameConnection('simpletest_original_default', 'default'); + + $conf['file_public_path'] = $this->originalFileDirectory; + // Restore modules if necessary. + if (isset($this->originalModuleList)) { + module_list(TRUE, FALSE, FALSE, $this->originalModuleList); + } + } +} + +/** + * Test case for typical Drupal tests. + */ +class DrupalWebTestCase extends DrupalTestCase { + /** + * The profile to install as a basis for testing. + * + * @var string + */ + protected $profile = 'standard'; + + /** + * The URL currently loaded in the internal browser. + * + * @var string + */ + protected $url; + + /** + * The handle of the current cURL connection. + * + * @var resource + */ + protected $curlHandle; + + /** + * The headers of the page currently loaded in the internal browser. + * + * @var Array + */ + protected $headers; + + /** + * The content of the page currently loaded in the internal browser. + * + * @var string + */ + protected $content; + + /** + * The content of the page currently loaded in the internal browser (plain text version). + * + * @var string + */ + protected $plainTextContent; + + /** + * The value of the Drupal.settings JavaScript variable for the page currently loaded in the internal browser. + * + * @var Array + */ + protected $drupalSettings; + + /** + * The parsed version of the page. + * + * @var SimpleXMLElement + */ + protected $elements = NULL; + + /** + * The current user logged in using the internal browser. + * + * @var bool + */ + protected $loggedInUser = FALSE; + + /** + * The current cookie file used by cURL. + * + * We do not reuse the cookies in further runs, so we do not need a file + * but we still need cookie handling, so we set the jar to NULL. + */ + protected $cookieFile = NULL; + + /** + * Additional cURL options. + * + * DrupalWebTestCase itself never sets this but always obeys what is set. + */ + protected $additionalCurlOptions = array(); + + /** + * The original user, before it was changed to a clean uid = 1 for testing purposes. + * + * @var object + */ + protected $originalUser = NULL; + + /** + * The original shutdown handlers array, before it was cleaned for testing purposes. + * + * @var array + */ + protected $originalShutdownCallbacks = array(); + + /** + * HTTP authentication method + */ + protected $httpauth_method = CURLAUTH_BASIC; + + /** + * HTTP authentication credentials (:). + */ + protected $httpauth_credentials = NULL; + + /** + * The current session name, if available. + */ + protected $session_name = NULL; + + /** + * The current session ID, if available. + */ + protected $session_id = NULL; + + /** + * Whether the files were copied to the test files directory. + */ + protected $generatedTestFiles = FALSE; + + /** + * The number of redirects followed during the handling of a request. + */ + protected $redirect_count; + + /** + * Constructor for DrupalWebTestCase. + */ + function __construct($test_id = NULL) { + parent::__construct($test_id); + $this->skipClasses[__CLASS__] = TRUE; + } + + /** + * Get a node from the database based on its title. + * + * @param title + * A node title, usually generated by $this->randomName(). + * @param $reset + * (optional) Whether to reset the internal node_load() cache. + * + * @return + * A node object matching $title. + */ + function drupalGetNodeByTitle($title, $reset = FALSE) { + $nodes = node_load_multiple(array(), array('title' => $title), $reset); + // Load the first node returned from the database. + $returned_node = reset($nodes); + return $returned_node; + } + + /** + * Creates a node based on default settings. + * + * @param $settings + * An associative array of settings to change from the defaults, keys are + * node properties, for example 'title' => 'Hello, world!'. + * @return + * Created node object. + */ + protected function drupalCreateNode($settings = array()) { + // Populate defaults array. + $settings += array( + 'body' => array(LANGUAGE_NONE => array(array())), + 'title' => $this->randomName(8), + 'comment' => 2, + 'changed' => REQUEST_TIME, + 'moderate' => 0, + 'promote' => 0, + 'revision' => 1, + 'log' => '', + 'status' => 1, + 'sticky' => 0, + 'type' => 'page', + 'revisions' => NULL, + 'language' => LANGUAGE_NONE, + ); + + // Use the original node's created time for existing nodes. + if (isset($settings['created']) && !isset($settings['date'])) { + $settings['date'] = format_date($settings['created'], 'custom', 'Y-m-d H:i:s O'); + } + + // If the node's user uid is not specified manually, use the currently + // logged in user if available, or else the user running the test. + if (!isset($settings['uid'])) { + if ($this->loggedInUser) { + $settings['uid'] = $this->loggedInUser->uid; + } + else { + global $user; + $settings['uid'] = $user->uid; + } + } + + // Merge body field value and format separately. + $body = array( + 'value' => $this->randomName(32), + 'format' => filter_default_format(), + ); + $settings['body'][$settings['language']][0] += $body; + + $node = (object) $settings; + node_save($node); + + // Small hack to link revisions to our test user. + db_update('node_revision') + ->fields(array('uid' => $node->uid)) + ->condition('vid', $node->vid) + ->execute(); + return $node; + } + + /** + * Creates a custom content type based on default settings. + * + * @param $settings + * An array of settings to change from the defaults. + * Example: 'type' => 'foo'. + * @return + * Created content type. + */ + protected function drupalCreateContentType($settings = array()) { + // Find a non-existent random type name. + do { + $name = strtolower($this->randomName(8)); + } while (node_type_get_type($name)); + + // Populate defaults array. + $defaults = array( + 'type' => $name, + 'name' => $name, + 'base' => 'node_content', + 'description' => '', + 'help' => '', + 'title_label' => 'Title', + 'body_label' => 'Body', + 'has_title' => 1, + 'has_body' => 1, + ); + // Imposed values for a custom type. + $forced = array( + 'orig_type' => '', + 'old_type' => '', + 'module' => 'node', + 'custom' => 1, + 'modified' => 1, + 'locked' => 0, + ); + $type = $forced + $settings + $defaults; + $type = (object) $type; + + $saved_type = node_type_save($type); + node_types_rebuild(); + menu_rebuild(); + node_add_body_field($type); + + $this->assertEqual($saved_type, SAVED_NEW, t('Created content type %type.', array('%type' => $type->type))); + + // Reset permissions so that permissions for this content type are available. + $this->checkPermissions(array(), TRUE); + + return $type; + } + + /** + * Get a list files that can be used in tests. + * + * @param $type + * File type, possible values: 'binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'. + * @param $size + * File size in bytes to match. Please check the tests/files folder. + * @return + * List of files that match filter. + */ + protected function drupalGetTestFiles($type, $size = NULL) { + if (empty($this->generatedTestFiles)) { + // Generate binary test files. + $lines = array(64, 1024); + $count = 0; + foreach ($lines as $line) { + simpletest_generate_file('binary-' . $count++, 64, $line, 'binary'); + } + + // Generate text test files. + $lines = array(16, 256, 1024, 2048, 20480); + $count = 0; + foreach ($lines as $line) { + simpletest_generate_file('text-' . $count++, 64, $line); + } + + // Copy other test files from simpletest. + $original = drupal_get_path('module', 'simpletest') . '/files'; + $files = file_scan_directory($original, '/(html|image|javascript|php|sql)-.*/'); + foreach ($files as $file) { + file_unmanaged_copy($file->uri, variable_get('file_public_path', conf_path() . '/files')); + } + + $this->generatedTestFiles = TRUE; + } + + $files = array(); + // Make sure type is valid. + if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) { + $files = file_scan_directory('public://', '/' . $type . '\-.*/'); + + // If size is set then remove any files that are not of that size. + if ($size !== NULL) { + foreach ($files as $file) { + $stats = stat($file->uri); + if ($stats['size'] != $size) { + unset($files[$file->uri]); + } + } + } + } + usort($files, array($this, 'drupalCompareFiles')); + return $files; + } + + /** + * Compare two files based on size and file name. + */ + protected function drupalCompareFiles($file1, $file2) { + $compare_size = filesize($file1->uri) - filesize($file2->uri); + if ($compare_size) { + // Sort by file size. + return $compare_size; + } + else { + // The files were the same size, so sort alphabetically. + return strnatcmp($file1->name, $file2->name); + } + } + + /** + * Create a user with a given set of permissions. The permissions correspond to the + * names given on the privileges page. + * + * @param $permissions + * Array of permission names to assign to user. + * @return + * A fully loaded user object with pass_raw property, or FALSE if account + * creation fails. + */ + protected function drupalCreateUser($permissions = array('access comments', 'access content', 'post comments', 'skip comment approval')) { + // Create a role with the given permission set. + if (!($rid = $this->drupalCreateRole($permissions))) { + return FALSE; + } + + // Create a user assigned to that role. + $edit = array(); + $edit['name'] = $this->randomName(); + $edit['mail'] = $edit['name'] . '@example.com'; + $edit['roles'] = array($rid => $rid); + $edit['pass'] = user_password(); + $edit['status'] = 1; + + $account = user_save(drupal_anonymous_user(), $edit); + + $this->assertTrue(!empty($account->uid), t('User created with name %name and pass %pass', array('%name' => $edit['name'], '%pass' => $edit['pass'])), t('User login')); + if (empty($account->uid)) { + return FALSE; + } + + // Add the raw password so that we can log in as this user. + $account->pass_raw = $edit['pass']; + return $account; + } + + /** + * Internal helper function; Create a role with specified permissions. + * + * @param $permissions + * Array of permission names to assign to role. + * @param $name + * (optional) String for the name of the role. Defaults to a random string. + * @return + * Role ID of newly created role, or FALSE if role creation failed. + */ + protected function drupalCreateRole(array $permissions, $name = NULL) { + // Generate random name if it was not passed. + if (!$name) { + $name = $this->randomName(); + } + + // Check the all the permissions strings are valid. + if (!$this->checkPermissions($permissions)) { + return FALSE; + } + + // Create new role. + $role = new stdClass(); + $role->name = $name; + user_role_save($role); + user_role_grant_permissions($role->rid, $permissions); + + $this->assertTrue(isset($role->rid), t('Created role of name: @name, id: @rid', array('@name' => $name, '@rid' => (isset($role->rid) ? $role->rid : t('-n/a-')))), t('Role')); + if ($role && !empty($role->rid)) { + $count = db_query('SELECT COUNT(*) FROM {role_permission} WHERE rid = :rid', array(':rid' => $role->rid))->fetchField(); + $this->assertTrue($count == count($permissions), t('Created permissions: @perms', array('@perms' => implode(', ', $permissions))), t('Role')); + return $role->rid; + } + else { + return FALSE; + } + } + + /** + * Check to make sure that the array of permissions are valid. + * + * @param $permissions + * Permissions to check. + * @param $reset + * Reset cached available permissions. + * @return + * TRUE or FALSE depending on whether the permissions are valid. + */ + protected function checkPermissions(array $permissions, $reset = FALSE) { + $available = &drupal_static(__FUNCTION__); + + if (!isset($available) || $reset) { + $available = array_keys(module_invoke_all('permission')); + } + + $valid = TRUE; + foreach ($permissions as $permission) { + if (!in_array($permission, $available)) { + $this->fail(t('Invalid permission %permission.', array('%permission' => $permission)), t('Role')); + $valid = FALSE; + } + } + return $valid; + } + + /** + * Log in a user with the internal browser. + * + * If a user is already logged in, then the current user is logged out before + * logging in the specified user. + * + * Please note that neither the global $user nor the passed-in user object is + * populated with data of the logged in user. If you need full access to the + * user object after logging in, it must be updated manually. If you also need + * access to the plain-text password of the user (set by drupalCreateUser()), + * e.g. to log in the same user again, then it must be re-assigned manually. + * For example: + * @code + * // Create a user. + * $account = $this->drupalCreateUser(array()); + * $this->drupalLogin($account); + * // Load real user object. + * $pass_raw = $account->pass_raw; + * $account = user_load($account->uid); + * $account->pass_raw = $pass_raw; + * @endcode + * + * @param $user + * User object representing the user to log in. + * + * @see drupalCreateUser() + */ + protected function drupalLogin(stdClass $user) { + if ($this->loggedInUser) { + $this->drupalLogout(); + } + + $edit = array( + 'name' => $user->name, + 'pass' => $user->pass_raw + ); + $this->drupalPost('user', $edit, t('Log in')); + + // If a "log out" link appears on the page, it is almost certainly because + // the login was successful. + $pass = $this->assertLink(t('Log out'), 0, t('User %name successfully logged in.', array('%name' => $user->name)), t('User login')); + + if ($pass) { + $this->loggedInUser = $user; + } + } + + /** + * Generate a token for the currently logged in user. + */ + protected function drupalGetToken($value = '') { + $private_key = drupal_get_private_key(); + return drupal_hmac_base64($value, $this->session_id . $private_key); + } + + /* + * Logs a user out of the internal browser, then check the login page to confirm logout. + */ + protected function drupalLogout() { + // Make a request to the logout page, and redirect to the user page, the + // idea being if you were properly logged out you should be seeing a login + // screen. + $this->drupalGet('user/logout'); + $this->drupalGet('user'); + $pass = $this->assertField('name', t('Username field found.'), t('Logout')); + $pass = $pass && $this->assertField('pass', t('Password field found.'), t('Logout')); + + if ($pass) { + $this->loggedInUser = FALSE; + } + } + + /** + * Generates a random database prefix, runs the install scripts on the + * prefixed database and enable the specified modules. After installation + * many caches are flushed and the internal browser is setup so that the + * page requests will run on the new prefix. A temporary files directory + * is created with the same name as the database prefix. + * + * @param ... + * List of modules to enable for the duration of the test. This can be + * either a single array or a variable number of string arguments. + */ + protected function setUp() { + global $user, $language, $conf; + + // Generate a temporary prefixed database to ensure that tests have a clean starting point. + $this->databasePrefix = 'simpletest' . mt_rand(1000, 1000000); + db_update('simpletest_test_id') + ->fields(array('last_prefix' => $this->databasePrefix)) + ->condition('test_id', $this->testId) + ->execute(); + + // Clone the current connection and replace the current prefix. + $connection_info = Database::getConnectionInfo('default'); + Database::renameConnection('default', 'simpletest_original_default'); + foreach ($connection_info as $target => $value) { + $connection_info[$target]['prefix'] = array( + 'default' => $value['prefix']['default'] . $this->databasePrefix, + ); + } + Database::addConnectionInfo('default', 'default', $connection_info['default']); + + // Store necessary current values before switching to prefixed database. + $this->originalLanguage = $language; + $this->originalLanguageDefault = variable_get('language_default'); + $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); + $this->originalProfile = drupal_get_profile(); + $clean_url_original = variable_get('clean_url', 0); + + // Save and clean shutdown callbacks array because it static cached and + // will be changed by the test run. If we don't, then it will contain + // callbacks from both environments. So testing environment will try + // to call handlers from original environment. + $callbacks = &drupal_register_shutdown_function(); + $this->originalShutdownCallbacks = $callbacks; + $callbacks = array(); + + // Create test directory ahead of installation so fatal errors and debug + // information can be logged during installation process. + // Use temporary files directory with the same prefix as the database. + $public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); + $private_files_directory = $public_files_directory . '/private'; + $temp_files_directory = $private_files_directory . '/temp'; + + // Create the directories + file_prepare_directory($public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + file_prepare_directory($private_files_directory, FILE_CREATE_DIRECTORY); + file_prepare_directory($temp_files_directory, FILE_CREATE_DIRECTORY); + $this->generatedTestFiles = FALSE; + + // Log fatal errors. + ini_set('log_errors', 1); + ini_set('error_log', $public_files_directory . '/error.log'); + + // Reset all statics and variables to perform tests in a clean environment. + $conf = array(); + drupal_static_reset(); + + // Set the test information for use in other parts of Drupal. + $test_info = &$GLOBALS['drupal_test_info']; + $test_info['test_run_id'] = $this->databasePrefix; + $test_info['in_child_site'] = FALSE; + + include_once DRUPAL_ROOT . '/core/includes/install.inc'; + drupal_install_system(); + + $this->preloadRegistry(); + + // Set path variables. + variable_set('file_public_path', $public_files_directory); + variable_set('file_private_path', $private_files_directory); + variable_set('file_temporary_path', $temp_files_directory); + + // Include the testing profile. + variable_set('install_profile', $this->profile); + $profile_details = install_profile_info($this->profile, 'en'); + + // Install the modules specified by the testing profile. + module_enable($profile_details['dependencies'], FALSE); + + // Install modules needed for this test. This could have been passed in as + // either a single array argument or a variable number of string arguments. + // @todo Remove this compatibility layer in Drupal 8, and only accept + // $modules as a single array argument. + $modules = func_get_args(); + if (isset($modules[0]) && is_array($modules[0])) { + $modules = $modules[0]; + } + if ($modules) { + module_enable($modules, TRUE); + } + + // Run the profile tasks. + $install_profile_module_exists = db_query("SELECT 1 FROM {system} WHERE type = 'module' AND name = :name", array( + ':name' => $this->profile, + ))->fetchField(); + if ($install_profile_module_exists) { + module_enable(array($this->profile), FALSE); + } + + // Reset/rebuild all data structures after enabling the modules. + $this->resetAll(); + + // Run cron once in that environment, as install.php does at the end of + // the installation process. + drupal_cron_run(); + + // Log in with a clean $user. + $this->originalUser = $user; + drupal_save_session(FALSE); + $user = user_load(1); + + // Restore necessary variables. + variable_set('install_task', 'done'); + variable_set('clean_url', $clean_url_original); + variable_set('site_mail', 'simpletest@example.com'); + variable_set('date_default_timezone', date_default_timezone_get()); + // Set up English language. + unset($GLOBALS['conf']['language_default']); + $language = language_default(); + + // Use the test mail class instead of the default mail handler class. + variable_set('mail_system', array('default-system' => 'TestingMailSystem')); + + drupal_set_time_limit($this->timeLimit); + } + + /** + * Preload the registry from the testing site. + * + * This method is called by DrupalWebTestCase::setUp(), and preloads the + * registry from the testing site to cut down on the time it takes to + * set up a clean environment for the current test run. + */ + protected function preloadRegistry() { + // Use two separate queries, each with their own connections: copy the + // {registry} and {registry_file} tables over from the parent installation + // to the child installation. + $original_connection = Database::getConnection('default', 'simpletest_original_default'); + $test_connection = Database::getConnection(); + + foreach (array('registry', 'registry_file') as $table) { + // Find the records from the parent database. + $source_query = $original_connection + ->select($table, array(), array('fetch' => PDO::FETCH_ASSOC)) + ->fields($table); + + $dest_query = $test_connection->insert($table); + + $first = TRUE; + foreach ($source_query->execute() as $row) { + if ($first) { + $dest_query->fields(array_keys($row)); + $first = FALSE; + } + // Insert the records into the child database. + $dest_query->values($row); + } + + $dest_query->execute(); + } + } + + /** + * Reset all data structures after having enabled new modules. + * + * This method is called by DrupalWebTestCase::setUp() after enabling + * the requested modules. It must be called again when additional modules + * are enabled later. + */ + protected function resetAll() { + // Reset all static variables. + drupal_static_reset(); + // Reset the list of enabled modules. + module_list(TRUE); + + // Reset cached schema for new database prefix. This must be done before + // drupal_flush_all_caches() so rebuilds can make use of the schema of + // modules enabled on the cURL side. + drupal_get_schema(NULL, TRUE); + + // Perform rebuilds and flush remaining caches. + drupal_flush_all_caches(); + + // Reload global $conf array and permissions. + $this->refreshVariables(); + $this->checkPermissions(array(), TRUE); + } + + /** + * Refresh the in-memory set of variables. Useful after a page request is made + * that changes a variable in a different thread. + * + * In other words calling a settings page with $this->drupalPost() with a changed + * value would update a variable to reflect that change, but in the thread that + * made the call (thread running the test) the changed variable would not be + * picked up. + * + * This method clears the variables cache and loads a fresh copy from the database + * to ensure that the most up-to-date set of variables is loaded. + */ + protected function refreshVariables() { + global $conf; + cache_clear_all('variables', 'cache_bootstrap'); + $conf = variable_initialize(); + } + + /** + * Delete created files and temporary files directory, delete the tables created by setUp(), + * and reset the database prefix. + */ + protected function tearDown() { + global $user, $language; + + // In case a fatal error occured that was not in the test process read the + // log to pick up any fatal errors. + simpletest_log_read($this->testId, $this->databasePrefix, get_class($this), TRUE); + + $emailCount = count(variable_get('drupal_test_email_collector', array())); + if ($emailCount) { + $message = format_plural($emailCount, '1 e-mail was sent during this test.', '@count e-mails were sent during this test.'); + $this->pass($message, t('E-mail')); + } + + // Delete temporary files directory. + file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10)); + + // Remove all prefixed tables (all the tables in the schema). + $schema = drupal_get_schema(NULL, TRUE); + foreach ($schema as $name => $table) { + db_drop_table($name); + } + + // Get back to the original connection. + Database::removeConnection('default'); + Database::renameConnection('simpletest_original_default', 'default'); + + // Restore original shutdown callbacks array to prevent original + // environment of calling handlers from test run. + $callbacks = &drupal_register_shutdown_function(); + $callbacks = $this->originalShutdownCallbacks; + + // Return the user to the original one. + $user = $this->originalUser; + drupal_save_session(TRUE); + + // Ensure that internal logged in variable and cURL options are reset. + $this->loggedInUser = FALSE; + $this->additionalCurlOptions = array(); + + // Reload module list and implementations to ensure that test module hooks + // aren't called after tests. + module_list(TRUE); + module_implements('', FALSE, TRUE); + + // Reset the Field API. + field_cache_clear(); + + // Rebuild caches. + $this->refreshVariables(); + + // Reset language. + $language = $this->originalLanguage; + if ($this->originalLanguageDefault) { + $GLOBALS['conf']['language_default'] = $this->originalLanguageDefault; + } + + // Close the CURL handler. + $this->curlClose(); + } + + /** + * Initializes the cURL connection. + * + * If the simpletest_httpauth_credentials variable is set, this function will + * add HTTP authentication headers. This is necessary for testing sites that + * are protected by login credentials from public access. + * See the description of $curl_options for other options. + */ + protected function curlInitialize() { + global $base_url; + + if (!isset($this->curlHandle)) { + $this->curlHandle = curl_init(); + $curl_options = array( + CURLOPT_COOKIEJAR => $this->cookieFile, + CURLOPT_URL => $base_url, + CURLOPT_FOLLOWLOCATION => FALSE, + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_SSL_VERIFYPEER => FALSE, // Required to make the tests run on https. + CURLOPT_SSL_VERIFYHOST => FALSE, // Required to make the tests run on https. + CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'), + CURLOPT_USERAGENT => $this->databasePrefix, + ); + if (isset($this->httpauth_credentials)) { + $curl_options[CURLOPT_HTTPAUTH] = $this->httpauth_method; + $curl_options[CURLOPT_USERPWD] = $this->httpauth_credentials; + } + curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options); + + // By default, the child session name should be the same as the parent. + $this->session_name = session_name(); + } + // We set the user agent header on each request so as to use the current + // time and a new uniqid. + if (preg_match('/simpletest\d+/', $this->databasePrefix, $matches)) { + curl_setopt($this->curlHandle, CURLOPT_USERAGENT, drupal_generate_test_ua($matches[0])); + } + } + + /** + * Initializes and executes a cURL request. + * + * @param $curl_options + * An associative array of cURL options to set, where the keys are constants + * defined by the cURL library. For a list of valid options, see + * http://www.php.net/manual/function.curl-setopt.php + * @param $redirect + * FALSE if this is an initial request, TRUE if this request is the result + * of a redirect. + * + * @return + * The content returned from the call to curl_exec(). + * + * @see curlInitialize() + */ + protected function curlExec($curl_options, $redirect = FALSE) { + $this->curlInitialize(); + + // cURL incorrectly handles URLs with a fragment by including the + // fragment in the request to the server, causing some web servers + // to reject the request citing "400 - Bad Request". To prevent + // this, we strip the fragment from the request. + // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0. + if (!empty($curl_options[CURLOPT_URL]) && strpos($curl_options[CURLOPT_URL], '#')) { + $original_url = $curl_options[CURLOPT_URL]; + $curl_options[CURLOPT_URL] = strtok($curl_options[CURLOPT_URL], '#'); + } + + $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL]; + + if (!empty($curl_options[CURLOPT_POST])) { + // This is a fix for the Curl library to prevent Expect: 100-continue + // headers in POST requests, that may cause unexpected HTTP response + // codes from some webservers (like lighttpd that returns a 417 error + // code). It is done by setting an empty "Expect" header field that is + // not overwritten by Curl. + $curl_options[CURLOPT_HTTPHEADER][] = 'Expect:'; + } + curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options); + + if (!$redirect) { + // Reset headers, the session ID and the redirect counter. + $this->session_id = NULL; + $this->headers = array(); + $this->redirect_count = 0; + } + + $content = curl_exec($this->curlHandle); + $status = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE); + + // cURL incorrectly handles URLs with fragments, so instead of + // letting cURL handle redirects we take of them ourselves to + // to prevent fragments being sent to the web server as part + // of the request. + // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0. + if (in_array($status, array(300, 301, 302, 303, 305, 307)) && $this->redirect_count < variable_get('simpletest_maximum_redirects', 5)) { + if ($this->drupalGetHeader('location')) { + $this->redirect_count++; + $curl_options = array(); + $curl_options[CURLOPT_URL] = $this->drupalGetHeader('location'); + $curl_options[CURLOPT_HTTPGET] = TRUE; + return $this->curlExec($curl_options, TRUE); + } + } + + $this->drupalSetContent($content, isset($original_url) ? $original_url : curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL)); + $message_vars = array( + '!method' => !empty($curl_options[CURLOPT_NOBODY]) ? 'HEAD' : (empty($curl_options[CURLOPT_POSTFIELDS]) ? 'GET' : 'POST'), + '@url' => isset($original_url) ? $original_url : $url, + '@status' => $status, + '!length' => format_size(strlen($this->drupalGetContent())) + ); + $message = t('!method @url returned @status (!length).', $message_vars); + $this->assertTrue($this->drupalGetContent() !== FALSE, $message, t('Browser')); + return $this->drupalGetContent(); + } + + /** + * Reads headers and registers errors received from the tested site. + * + * @see _drupal_log_error(). + * + * @param $curlHandler + * The cURL handler. + * @param $header + * An header. + */ + protected function curlHeaderCallback($curlHandler, $header) { + $this->headers[] = $header; + + // Errors are being sent via X-Drupal-Assertion-* headers, + // generated by _drupal_log_error() in the exact form required + // by DrupalWebTestCase::error(). + if (preg_match('/^X-Drupal-Assertion-[0-9]+: (.*)$/', $header, $matches)) { + // Call DrupalWebTestCase::error() with the parameters from the header. + call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1]))); + } + + // Save cookies. + if (preg_match('/^Set-Cookie: ([^=]+)=(.+)/', $header, $matches)) { + $name = $matches[1]; + $parts = array_map('trim', explode(';', $matches[2])); + $value = array_shift($parts); + $this->cookies[$name] = array('value' => $value, 'secure' => in_array('secure', $parts)); + if ($name == $this->session_name) { + if ($value != 'deleted') { + $this->session_id = $value; + } + else { + $this->session_id = NULL; + } + } + } + + // This is required by cURL. + return strlen($header); + } + + /** + * Close the cURL handler and unset the handler. + */ + protected function curlClose() { + if (isset($this->curlHandle)) { + curl_close($this->curlHandle); + unset($this->curlHandle); + } + } + + /** + * Parse content returned from curlExec using DOM and SimpleXML. + * + * @return + * A SimpleXMLElement or FALSE on failure. + */ + protected function parse() { + if (!$this->elements) { + // DOM can load HTML soup. But, HTML soup can throw warnings, suppress + // them. + $htmlDom = new DOMDocument(); + @$htmlDom->loadHTML($this->drupalGetContent()); + if ($htmlDom) { + $this->pass(t('Valid HTML found on "@path"', array('@path' => $this->getUrl())), t('Browser')); + // It's much easier to work with simplexml than DOM, luckily enough + // we can just simply import our DOM tree. + $this->elements = simplexml_import_dom($htmlDom); + } + } + if (!$this->elements) { + $this->fail(t('Parsed page successfully.'), t('Browser')); + } + + return $this->elements; + } + + /** + * Retrieves a Drupal path or an absolute path. + * + * @param $path + * Drupal path or URL to load into internal browser + * @param $options + * Options to be forwarded to url(). + * @param $headers + * An array containing additional HTTP request headers, each formatted as + * "name: value". + * @return + * The retrieved HTML string, also available as $this->drupalGetContent() + */ + protected function drupalGet($path, array $options = array(), array $headers = array()) { + $options['absolute'] = TRUE; + + // We re-using a CURL connection here. If that connection still has certain + // options set, it might change the GET into a POST. Make sure we clear out + // previous options. + $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers)); + $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up. + + // Replace original page output with new output from redirected page(s). + if ($new = $this->checkForMetaRefresh()) { + $out = $new; + } + $this->verbose('GET request to: ' . $path . + '
    Ending URL: ' . $this->getUrl() . + '
    ' . $out); + return $out; + } + + /** + * Retrieve a Drupal path or an absolute path and JSON decode the result. + */ + protected function drupalGetAJAX($path, array $options = array(), array $headers = array()) { + return drupal_json_decode($this->drupalGet($path, $options, $headers)); + } + + /** + * Execute a POST request on a Drupal page. + * It will be done as usual POST request with SimpleBrowser. + * + * @param $path + * Location of the post form. Either a Drupal path or an absolute path or + * NULL to post to the current page. For multi-stage forms you can set the + * path to NULL and have it post to the last received page. Example: + * + * @code + * // First step in form. + * $edit = array(...); + * $this->drupalPost('some_url', $edit, t('Save')); + * + * // Second step in form. + * $edit = array(...); + * $this->drupalPost(NULL, $edit, t('Save')); + * @endcode + * @param $edit + * Field data in an associative array. Changes the current input fields + * (where possible) to the values indicated. A checkbox can be set to + * TRUE to be checked and FALSE to be unchecked. Note that when a form + * contains file upload fields, other fields cannot start with the '@' + * character. + * + * Multiple select fields can be set using name[] and setting each of the + * possible values. Example: + * @code + * $edit = array(); + * $edit['name[]'] = array('value1', 'value2'); + * @endcode + * @param $submit + * Value of the submit button whose click is to be emulated. For example, + * t('Save'). The processing of the request depends on this value. For + * example, a form may have one button with the value t('Save') and another + * button with the value t('Delete'), and execute different code depending + * on which one is clicked. + * + * This function can also be called to emulate an Ajax submission. In this + * case, this value needs to be an array with the following keys: + * - path: A path to submit the form values to for Ajax-specific processing, + * which is likely different than the $path parameter used for retrieving + * the initial form. Defaults to 'system/ajax'. + * - triggering_element: If the value for the 'path' key is 'system/ajax' or + * another generic Ajax processing path, this needs to be set to the name + * of the element. If the name doesn't identify the element uniquely, then + * this should instead be an array with a single key/value pair, + * corresponding to the element name and value. The callback for the + * generic Ajax processing path uses this to find the #ajax information + * for the element, including which specific callback to use for + * processing the request. + * + * This can also be set to NULL in order to emulate an Internet Explorer + * submission of a form with a single text field, and pressing ENTER in that + * textfield: under these conditions, no button information is added to the + * POST data. + * @param $options + * Options to be forwarded to url(). + * @param $headers + * An array containing additional HTTP request headers, each formatted as + * "name: value". + * @param $form_html_id + * (optional) HTML ID of the form to be submitted. On some pages + * there are many identical forms, so just using the value of the submit + * button is not enough. For example: 'trigger-node-presave-assign-form'. + * Note that this is not the Drupal $form_id, but rather the HTML ID of the + * form, which is typically the same thing but with hyphens replacing the + * underscores. + * @param $extra_post + * (optional) A string of additional data to append to the POST submission. + * This can be used to add POST data for which there are no HTML fields, as + * is done by drupalPostAJAX(). This string is literally appended to the + * POST data, so it must already be urlencoded and contain a leading "&" + * (e.g., "&extra_var1=hello+world&extra_var2=you%26me"). + */ + protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) { + $submit_matches = FALSE; + $ajax = is_array($submit); + if (isset($path)) { + $this->drupalGet($path, $options); + } + if ($this->parse()) { + $edit_save = $edit; + // Let's iterate over all the forms. + $xpath = "//form"; + if (!empty($form_html_id)) { + $xpath .= "[@id='" . $form_html_id . "']"; + } + $forms = $this->xpath($xpath); + foreach ($forms as $form) { + // We try to set the fields of this form as specified in $edit. + $edit = $edit_save; + $post = array(); + $upload = array(); + $submit_matches = $this->handleForm($post, $edit, $upload, $ajax ? NULL : $submit, $form); + $action = isset($form['action']) ? $this->getAbsoluteUrl((string) $form['action']) : $this->getUrl(); + if ($ajax) { + $action = $this->getAbsoluteUrl(!empty($submit['path']) ? $submit['path'] : 'system/ajax'); + // Ajax callbacks verify the triggering element if necessary, so while + // we may eventually want extra code that verifies it in the + // handleForm() function, it's not currently a requirement. + $submit_matches = TRUE; + } + + // We post only if we managed to handle every field in edit and the + // submit button matches. + if (!$edit && ($submit_matches || !isset($submit))) { + $post_array = $post; + if ($upload) { + // TODO: cURL handles file uploads for us, but the implementation + // is broken. This is a less than elegant workaround. Alternatives + // are being explored at #253506. + foreach ($upload as $key => $file) { + $file = drupal_realpath($file); + if ($file && is_file($file)) { + $post[$key] = '@' . $file; + } + } + } + else { + foreach ($post as $key => $value) { + // Encode according to application/x-www-form-urlencoded + // Both names and values needs to be urlencoded, according to + // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1 + $post[$key] = urlencode($key) . '=' . urlencode($value); + } + $post = implode('&', $post) . $extra_post; + } + $out = $this->curlExec(array(CURLOPT_URL => $action, CURLOPT_POST => TRUE, CURLOPT_POSTFIELDS => $post, CURLOPT_HTTPHEADER => $headers)); + // Ensure that any changes to variables in the other thread are picked up. + $this->refreshVariables(); + + // Replace original page output with new output from redirected page(s). + if ($new = $this->checkForMetaRefresh()) { + $out = $new; + } + $this->verbose('POST request to: ' . $path . + '
    Ending URL: ' . $this->getUrl() . + '
    Fields: ' . highlight_string('' . $out); + return $out; + } + } + // We have not found a form which contained all fields of $edit. + foreach ($edit as $name => $value) { + $this->fail(t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value))); + } + if (!$ajax && isset($submit)) { + $this->assertTrue($submit_matches, t('Found the @submit button', array('@submit' => $submit))); + } + $this->fail(t('Found the requested form fields at @path', array('@path' => $path))); + } + } + + /** + * Execute an Ajax submission. + * + * This executes a POST as ajax.js does. It uses the returned JSON data, an + * array of commands, to update $this->content using equivalent DOM + * manipulation as is used by ajax.js. It also returns the array of commands. + * + * @param $path + * Location of the form containing the Ajax enabled element to test. Can be + * either a Drupal path or an absolute path or NULL to use the current page. + * @param $edit + * Field data in an associative array. Changes the current input fields + * (where possible) to the values indicated. + * @param $triggering_element + * The name of the form element that is responsible for triggering the Ajax + * functionality to test. May be a string or, if the triggering element is + * a button, an associative array where the key is the name of the button + * and the value is the button label. i.e.) array('op' => t('Refresh')). + * @param $ajax_path + * (optional) Override the path set by the Ajax settings of the triggering + * element. In the absence of both the triggering element's Ajax path and + * $ajax_path 'system/ajax' will be used. + * @param $options + * (optional) Options to be forwarded to url(). + * @param $headers + * (optional) An array containing additional HTTP request headers, each + * formatted as "name: value". Forwarded to drupalPost(). + * @param $form_html_id + * (optional) HTML ID of the form to be submitted, use when there is more + * than one identical form on the same page and the value of the triggering + * element is not enough to identify the form. Note this is not the Drupal + * ID of the form but rather the HTML ID of the form. + * @param $ajax_settings + * (optional) An array of Ajax settings which if specified will be used in + * place of the Ajax settings of the triggering element. + * + * @return + * An array of Ajax commands. + * + * @see drupalPost() + * @see ajax.js + */ + protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path = NULL, array $options = array(), array $headers = array(), $form_html_id = NULL, $ajax_settings = NULL) { + // Get the content of the initial page prior to calling drupalPost(), since + // drupalPost() replaces $this->content. + if (isset($path)) { + $this->drupalGet($path, $options); + } + $content = $this->content; + $drupal_settings = $this->drupalSettings; + + // Get the Ajax settings bound to the triggering element. + if (!isset($ajax_settings)) { + if (is_array($triggering_element)) { + $xpath = '//*[@name="' . key($triggering_element) . '" and @value="' . current($triggering_element) . '"]'; + } + else { + $xpath = '//*[@name="' . $triggering_element . '"]'; + } + if (isset($form_html_id)) { + $xpath = '//form[@id="' . $form_html_id . '"]' . $xpath; + } + $element = $this->xpath($xpath); + $element_id = (string) $element[0]['id']; + $ajax_settings = $drupal_settings['ajax'][$element_id]; + } + + // Add extra information to the POST data as ajax.js does. + $extra_post = ''; + if (isset($ajax_settings['submit'])) { + foreach ($ajax_settings['submit'] as $key => $value) { + $extra_post .= '&' . urlencode($key) . '=' . urlencode($value); + } + } + foreach ($this->xpath('//*[@id]') as $element) { + $id = (string) $element['id']; + $extra_post .= '&' . urlencode('ajax_html_ids[]') . '=' . urlencode($id); + } + + // Unless a particular path is specified, use the one specified by the + // Ajax settings, or else 'system/ajax'. + if (!isset($ajax_path)) { + $ajax_path = isset($ajax_settings['url']) ? $ajax_settings['url'] : 'system/ajax'; + } + + // Submit the POST request. + $return = drupal_json_decode($this->drupalPost(NULL, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers, $form_html_id, $extra_post)); + + // Change the page content by applying the returned commands. + if (!empty($ajax_settings) && !empty($return)) { + // ajax.js applies some defaults to the settings object, so do the same + // for what's used by this function. + $ajax_settings += array( + 'method' => 'replaceWith', + ); + // DOM can load HTML soup. But, HTML soup can throw warnings, suppress + // them. + $dom = new DOMDocument(); + @$dom->loadHTML($content); + foreach ($return as $command) { + switch ($command['command']) { + case 'settings': + $drupal_settings = array_merge_recursive($drupal_settings, $command['settings']); + break; + + case 'insert': + // @todo ajax.js can process commands that include a 'selector', but + // these are hard to emulate with DOMDocument. For now, we only + // implement 'insert' commands that use $ajax_settings['wrapper']. + if (!isset($command['selector'])) { + // $dom->getElementById() doesn't work when drupalPostAJAX() is + // invoked multiple times for a page, so use XPath instead. This + // also sets us up for adding support for $command['selector'] in + // the future, once we figure out how to transform a jQuery + // selector to XPath. + $xpath = new DOMXPath($dom); + $wrapperNode = $xpath->query('//*[@id="' . $ajax_settings['wrapper'] . '"]')->item(0); + if ($wrapperNode) { + // ajax.js adds an enclosing DIV to work around a Safari bug. + $newDom = new DOMDocument(); + $newDom->loadHTML('
    ' . $command['data'] . '
    '); + $newNode = $dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE); + $method = isset($command['method']) ? $command['method'] : $ajax_settings['method']; + // The "method" is a jQuery DOM manipulation function. Emulate + // each one using PHP's DOMNode API. + switch ($method) { + case 'replaceWith': + $wrapperNode->parentNode->replaceChild($newNode, $wrapperNode); + break; + case 'append': + $wrapperNode->appendChild($newNode); + break; + case 'prepend': + // If no firstChild, insertBefore() falls back to + // appendChild(). + $wrapperNode->insertBefore($newNode, $wrapperNode->firstChild); + break; + case 'before': + $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode); + break; + case 'after': + // If no nextSibling, insertBefore() falls back to + // appendChild(). + $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode->nextSibling); + break; + case 'html': + foreach ($wrapperNode->childNodes as $childNode) { + $wrapperNode->removeChild($childNode); + } + $wrapperNode->appendChild($newNode); + break; + } + } + } + break; + + // @todo Add suitable implementations for these commands in order to + // have full test coverage of what ajax.js can do. + case 'remove': + break; + case 'changed': + break; + case 'css': + break; + case 'data': + break; + case 'restripe': + break; + } + } + $content = $dom->saveHTML(); + } + $this->drupalSetContent($content); + $this->drupalSetSettings($drupal_settings); + return $return; + } + + /** + * Runs cron in the Drupal installed by Simpletest. + */ + protected function cronRun() { + $this->drupalGet($GLOBALS['base_url'] . '/core/cron.php', array('external' => TRUE, 'query' => array('cron_key' => variable_get('cron_key', 'drupal')))); + } + + /** + * Check for meta refresh tag and if found call drupalGet() recursively. This + * function looks for the http-equiv attribute to be set to "Refresh" + * and is case-sensitive. + * + * @return + * Either the new page content or FALSE. + */ + protected function checkForMetaRefresh() { + if (strpos($this->drupalGetContent(), 'parse()) { + $refresh = $this->xpath('//meta[@http-equiv="Refresh"]'); + if (!empty($refresh)) { + // Parse the content attribute of the meta tag for the format: + // "[delay]: URL=[page_to_redirect_to]". + if (preg_match('/\d+;\s*URL=(?P.*)/i', $refresh[0]['content'], $match)) { + return $this->drupalGet($this->getAbsoluteUrl(decode_entities($match['url']))); + } + } + } + return FALSE; + } + + /** + * Retrieves only the headers for a Drupal path or an absolute path. + * + * @param $path + * Drupal path or URL to load into internal browser + * @param $options + * Options to be forwarded to url(). + * @param $headers + * An array containing additional HTTP request headers, each formatted as + * "name: value". + * @return + * The retrieved headers, also available as $this->drupalGetContent() + */ + protected function drupalHead($path, array $options = array(), array $headers = array()) { + $options['absolute'] = TRUE; + $out = $this->curlExec(array(CURLOPT_NOBODY => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_HTTPHEADER => $headers)); + $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up. + return $out; + } + + /** + * Handle form input related to drupalPost(). Ensure that the specified fields + * exist and attempt to create POST data in the correct manner for the particular + * field type. + * + * @param $post + * Reference to array of post values. + * @param $edit + * Reference to array of edit values to be checked against the form. + * @param $submit + * Form submit button value. + * @param $form + * Array of form elements. + * @return + * Submit value matches a valid submit input in the form. + */ + protected function handleForm(&$post, &$edit, &$upload, $submit, $form) { + // Retrieve the form elements. + $elements = $form->xpath('.//input[not(@disabled)]|.//textarea[not(@disabled)]|.//select[not(@disabled)]'); + $submit_matches = FALSE; + foreach ($elements as $element) { + // SimpleXML objects need string casting all the time. + $name = (string) $element['name']; + // This can either be the type of or the name of the tag itself + // for '; - $output .= ''; - return $output; -} - -/** - * Returns HTML for a password form element. - * - * @param $variables - * An associative array containing: - * - element: An associative array containing the properties of the element. - * Properties used: #title, #value, #description, #size, #maxlength, - * #required, #attributes. - * - * @ingroup themeable - */ -function theme_password($variables) { - $element = $variables['element']; - $element['#attributes']['type'] = 'password'; - element_set_attributes($element, array('id', 'name', 'size', 'maxlength')); - _form_set_class($element, array('form-text')); - - return ''; -} - -/** - * Expand weight elements into selects. - */ -function form_process_weight($element) { - for ($n = (-1 * $element['#delta']); $n <= $element['#delta']; $n++) { - $weights[$n] = $n; - } - $element['#options'] = $weights; - $element['#type'] = 'select'; - $element['#is_weight'] = TRUE; - $element += element_info('select'); - return $element; -} - -/** - * Returns HTML for a file upload form element. - * - * For assistance with handling the uploaded file correctly, see the API - * provided by file.inc. - * - * @param $variables - * An associative array containing: - * - element: An associative array containing the properties of the element. - * Properties used: #title, #name, #size, #description, #required, - * #attributes. - * - * @ingroup themeable - */ -function theme_file($variables) { - $element = $variables['element']; - $element['#attributes']['type'] = 'file'; - element_set_attributes($element, array('id', 'name', 'size')); - _form_set_class($element, array('form-file')); - - return ''; -} - -/** - * Returns HTML for a form element. - * - * Each form element is wrapped in a DIV container having the following CSS - * classes: - * - form-item: Generic for all form elements. - * - form-type-#type: The internal element #type. - * - form-item-#name: The internal form element #name (usually derived from the - * $form structure and set via form_builder()). - * - form-disabled: Only set if the form element is #disabled. - * - * In addition to the element itself, the DIV contains a label for the element - * based on the optional #title_display property, and an optional #description. - * - * The optional #title_display property can have these values: - * - before: The label is output before the element. This is the default. - * The label includes the #title and the required marker, if #required. - * - after: The label is output after the element. For example, this is used - * for radio and checkbox #type elements as set in system_element_info(). - * If the #title is empty but the field is #required, the label will - * contain only the required marker. - * - invisible: Labels are critical for screen readers to enable them to - * properly navigate through forms but can be visually distracting. This - * property hides the label for everyone except screen readers. - * - attribute: Set the title attribute on the element to create a tooltip - * but output no label element. This is supported only for checkboxes - * and radios in form_pre_render_conditional_form_element(). It is used - * where a visual label is not needed, such as a table of checkboxes where - * the row and column provide the context. The tooltip will include the - * title and required marker. - * - * If the #title property is not set, then the label and any required marker - * will not be output, regardless of the #title_display or #required values. - * This can be useful in cases such as the password_confirm element, which - * creates children elements that have their own labels and required markers, - * but the parent element should have neither. Use this carefully because a - * field without an associated label can cause accessibility challenges. - * - * @param $variables - * An associative array containing: - * - element: An associative array containing the properties of the element. - * Properties used: #title, #title_display, #description, #id, #required, - * #children, #type, #name. - * - * @ingroup themeable - */ -function theme_form_element($variables) { - $element = &$variables['element']; - // This is also used in the installer, pre-database setup. - $t = get_t(); - - // This function is invoked as theme wrapper, but the rendered form element - // may not necessarily have been processed by form_builder(). - $element += array( - '#title_display' => 'before', - ); - - // Add element #id for #type 'item'. - if (isset($element['#markup']) && !empty($element['#id'])) { - $attributes['id'] = $element['#id']; - } - // Add element's #type and #name as class to aid with JS/CSS selectors. - $attributes['class'] = array('form-item'); - if (!empty($element['#type'])) { - $attributes['class'][] = 'form-type-' . strtr($element['#type'], '_', '-'); - } - if (!empty($element['#name'])) { - $attributes['class'][] = 'form-item-' . strtr($element['#name'], array(' ' => '-', '_' => '-', '[' => '-', ']' => '')); - } - // Add a class for disabled elements to facilitate cross-browser styling. - if (!empty($element['#attributes']['disabled'])) { - $attributes['class'][] = 'form-disabled'; - } - $output = '' . "\n"; - - // If #title is not set, we don't display any label or required marker. - if (!isset($element['#title'])) { - $element['#title_display'] = 'none'; - } - $prefix = isset($element['#field_prefix']) ? '' . $element['#field_prefix'] . ' ' : ''; - $suffix = isset($element['#field_suffix']) ? ' ' . $element['#field_suffix'] . '' : ''; - - switch ($element['#title_display']) { - case 'before': - case 'invisible': - $output .= ' ' . theme('form_element_label', $variables); - $output .= ' ' . $prefix . $element['#children'] . $suffix . "\n"; - break; - - case 'after': - $output .= ' ' . $prefix . $element['#children'] . $suffix; - $output .= ' ' . theme('form_element_label', $variables) . "\n"; - break; - - case 'none': - case 'attribute': - // Output no label and no required marker, only the children. - $output .= ' ' . $prefix . $element['#children'] . $suffix . "\n"; - break; - } - - if (!empty($element['#description'])) { - $output .= '
    ' . $element['#description'] . "
    \n"; - } - - $output .= "\n"; - - return $output; -} - -/** - * Returns HTML for a marker for required form elements. - * - * @param $variables - * An associative array containing: - * - element: An associative array containing the properties of the element. - * - * @ingroup themeable - */ -function theme_form_required_marker($variables) { - // This is also used in the installer, pre-database setup. - $t = get_t(); - $attributes = array( - 'class' => 'form-required', - 'title' => $t('This field is required.'), - ); - return '*'; -} - -/** - * Returns HTML for a form element label and required marker. - * - * Form element labels include the #title and a #required marker. The label is - * associated with the element itself by the element #id. Labels may appear - * before or after elements, depending on theme_form_element() and #title_display. - * - * This function will not be called for elements with no labels, depending on - * #title_display. For elements that have an empty #title and are not required, - * this function will output no label (''). For required elements that have an - * empty #title, this will output the required marker alone within the label. - * The label will use the #id to associate the marker with the field that is - * required. That is especially important for screenreader users to know - * which field is required. - * - * @param $variables - * An associative array containing: - * - element: An associative array containing the properties of the element. - * Properties used: #required, #title, #id, #value, #description. - * - * @ingroup themeable - */ -function theme_form_element_label($variables) { - $element = $variables['element']; - // This is also used in the installer, pre-database setup. - $t = get_t(); - - // If title and required marker are both empty, output no label. - if (empty($element['#title']) && empty($element['#required'])) { - return ''; - } - - // If the element is required, a required marker is appended to the label. - $required = !empty($element['#required']) ? theme('form_required_marker', array('element' => $element)) : ''; - - $title = filter_xss_admin($element['#title']); - - $attributes = array(); - // Style the label as class option to display inline with the element. - if ($element['#title_display'] == 'after') { - $attributes['class'] = 'option'; - } - // Show label only to screen readers to avoid disruption in visual flows. - elseif ($element['#title_display'] == 'invisible') { - $attributes['class'] = 'element-invisible'; - } - - if (!empty($element['#id'])) { - $attributes['for'] = $element['#id']; - } - - // The leading whitespace helps visually separate fields from inline labels. - return ' ' . $t('!title !required', array('!title' => $title, '!required' => $required)) . "\n"; -} - -/** - * Sets a form element's class attribute. - * - * Adds 'required' and 'error' classes as needed. - * - * @param $element - * The form element. - * @param $name - * Array of new class names to be added. - */ -function _form_set_class(&$element, $class = array()) { - if (!empty($class)) { - if (!isset($element['#attributes']['class'])) { - $element['#attributes']['class'] = array(); - } - $element['#attributes']['class'] = array_merge($element['#attributes']['class'], $class); - } - // This function is invoked from form element theme functions, but the - // rendered form element may not necessarily have been processed by - // form_builder(). - if (!empty($element['#required'])) { - $element['#attributes']['class'][] = 'required'; - } - if (isset($element['#parents']) && form_get_error($element)) { - $element['#attributes']['class'][] = 'error'; - } -} - -/** - * @} End of "defgroup form_api". - */ - -/** - * @defgroup batch Batch operations - * @{ - * Create and process batch operations. - * - * Functions allowing forms processing to be spread out over several page - * requests, thus ensuring that the processing does not get interrupted - * because of a PHP timeout, while allowing the user to receive feedback - * on the progress of the ongoing operations. - * - * The API is primarily designed to integrate nicely with the Form API - * workflow, but can also be used by non-Form API scripts (like update.php) - * or even simple page callbacks (which should probably be used sparingly). - * - * Example: - * @code - * $batch = array( - * 'title' => t('Exporting'), - * 'operations' => array( - * array('my_function_1', array($account->uid, 'story')), - * array('my_function_2', array()), - * ), - * 'finished' => 'my_finished_callback', - * 'file' => 'path_to_file_containing_myfunctions', - * ); - * batch_set($batch); - * // only needed if not inside a form _submit handler : - * batch_process(); - * @endcode - * - * Note: if the batch 'title', 'init_message', 'progress_message', or - * 'error_message' could contain any user input, it is the responsibility of - * the code calling batch_set() to sanitize them first with a function like - * check_plain() or filter_xss(). Furthermore, if the batch operation - * returns any user input in the 'results' or 'message' keys of $context, - * it must also sanitize them first. - * - * Sample batch operations: - * @code - * // Simple and artificial: load a node of a given type for a given user - * function my_function_1($uid, $type, &$context) { - * // The $context array gathers batch context information about the execution (read), - * // as well as 'return values' for the current operation (write) - * // The following keys are provided : - * // 'results' (read / write): The array of results gathered so far by - * // the batch processing, for the current operation to append its own. - * // 'message' (write): A text message displayed in the progress page. - * // The following keys allow for multi-step operations : - * // 'sandbox' (read / write): An array that can be freely used to - * // store persistent data between iterations. It is recommended to - * // use this instead of $_SESSION, which is unsafe if the user - * // continues browsing in a separate window while the batch is processing. - * // 'finished' (write): A float number between 0 and 1 informing - * // the processing engine of the completion level for the operation. - * // 1 (or no value explicitly set) means the operation is finished - * // and the batch processing can continue to the next operation. - * - * $node = node_load(array('uid' => $uid, 'type' => $type)); - * $context['results'][] = $node->nid . ' : ' . check_plain($node->title); - * $context['message'] = check_plain($node->title); - * } - * - * // More advanced example: multi-step operation - load all nodes, five by five - * function my_function_2(&$context) { - * if (empty($context['sandbox'])) { - * $context['sandbox']['progress'] = 0; - * $context['sandbox']['current_node'] = 0; - * $context['sandbox']['max'] = db_query('SELECT COUNT(DISTINCT nid) FROM {node}')->fetchField(); - * } - * $limit = 5; - * $result = db_select('node') - * ->fields('node', array('nid')) - * ->condition('nid', $context['sandbox']['current_node'], '>') - * ->orderBy('nid') - * ->range(0, $limit) - * ->execute(); - * foreach ($result as $row) { - * $node = node_load($row->nid, NULL, TRUE); - * $context['results'][] = $node->nid . ' : ' . check_plain($node->title); - * $context['sandbox']['progress']++; - * $context['sandbox']['current_node'] = $node->nid; - * $context['message'] = check_plain($node->title); - * } - * if ($context['sandbox']['progress'] != $context['sandbox']['max']) { - * $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; - * } - * } - * @endcode - * - * Sample 'finished' callback: - * @code - * function batch_test_finished($success, $results, $operations) { - * // The 'success' parameter means no fatal PHP errors were detected. All - * // other error management should be handled using 'results'. - * if ($success) { - * $message = format_plural(count($results), 'One post processed.', '@count posts processed.'); - * } - * else { - * $message = t('Finished with an error.'); - * } - * drupal_set_message($message); - * // Providing data for the redirected page is done through $_SESSION. - * foreach ($results as $result) { - * $items[] = t('Loaded node %title.', array('%title' => $result)); - * } - * $_SESSION['my_batch_results'] = $items; - * } - * @endcode - */ - -/** - * Opens a new batch. - * - * @param $batch - * An array defining the batch. The following keys can be used -- only - * 'operations' is required, and batch_init() provides default values for - * the messages. - * - 'operations': Array of function calls to be performed. - * Example: - * @code - * array( - * array('my_function_1', array($arg1)), - * array('my_function_2', array($arg2_1, $arg2_2)), - * ) - * @endcode - * - 'title': Title for the progress page. Only safe strings should be passed. - * Defaults to t('Processing'). - * - 'init_message': Message displayed while the processing is initialized. - * Defaults to t('Initializing.'). - * - 'progress_message': Message displayed while processing the batch. - * Available placeholders are @current, @remaining, @total, @percentage, - * @estimate and @elapsed. Defaults to t('Completed @current of @total.'). - * - 'error_message': Message displayed if an error occurred while processing - * the batch. Defaults to t('An error has occurred.'). - * - 'finished': Name of a function to be executed after the batch has - * completed. This should be used to perform any result massaging that - * may be needed, and possibly save data in $_SESSION for display after - * final page redirection. - * - 'file': Path to the file containing the definitions of the - * 'operations' and 'finished' functions, for instance if they don't - * reside in the main .module file. The path should be relative to - * base_path(), and thus should be built using drupal_get_path(). - * - 'css': Array of paths to CSS files to be used on the progress page. - * - 'url_options': options passed to url() when constructing redirect - * URLs for the batch. - * - * Operations are added as new batch sets. Batch sets are used to ensure - * clean code independence, ensuring that several batches submitted by - * different parts of the code (core / contrib modules) can be processed - * correctly while not interfering or having to cope with each other. Each - * batch set gets to specify his own UI messages, operates on its own set - * of operations and results, and triggers its own 'finished' callback. - * Batch sets are processed sequentially, with the progress bar starting - * fresh for every new set. - */ -function batch_set($batch_definition) { - if ($batch_definition) { - $batch =& batch_get(); - - // Initialize the batch if needed. - if (empty($batch)) { - $batch = array( - 'sets' => array(), - 'has_form_submits' => FALSE, - ); - } - - // Base and default properties for the batch set. - // Use get_t() to allow batches at install time. - $t = get_t(); - $init = array( - 'sandbox' => array(), - 'results' => array(), - 'success' => FALSE, - 'start' => 0, - 'elapsed' => 0, - ); - $defaults = array( - 'title' => $t('Processing'), - 'init_message' => $t('Initializing.'), - 'progress_message' => $t('Completed @current of @total.'), - 'error_message' => $t('An error has occurred.'), - 'css' => array(), - ); - $batch_set = $init + $batch_definition + $defaults; - - // Tweak init_message to avoid the bottom of the page flickering down after - // init phase. - $batch_set['init_message'] .= '
     '; - - // The non-concurrent workflow of batch execution allows us to save - // numberOfItems() queries by handling our own counter. - $batch_set['total'] = count($batch_set['operations']); - $batch_set['count'] = $batch_set['total']; - - // Add the set to the batch. - if (empty($batch['id'])) { - // The batch is not running yet. Simply add the new set. - $batch['sets'][] = $batch_set; - } - else { - // The set is being added while the batch is running. Insert the new set - // right after the current one to ensure execution order, and store its - // operations in a queue. - $index = $batch['current_set'] + 1; - $slice1 = array_slice($batch['sets'], 0, $index); - $slice2 = array_slice($batch['sets'], $index); - $batch['sets'] = array_merge($slice1, array($batch_set), $slice2); - _batch_populate_queue($batch, $index); - } - } -} - -/** - * Processes the batch. - * - * Unless the batch has been marked with 'progressive' = FALSE, the function - * issues a drupal_goto and thus ends page execution. - * - * This function is generally not needed in form submit handlers; - * Form API takes care of batches that were set during form submission. - * - * @param $redirect - * (optional) Path to redirect to when the batch has finished processing. - * @param $url - * (optional - should only be used for separate scripts like update.php) - * URL of the batch processing page. - * @param $redirect_callback - * (optional) Specify a function to be called to redirect to the progressive - * processing page. By default drupal_goto() will be used to redirect to a - * page which will do the progressive page. Specifying another function will - * allow the progressive processing to be processed differently. - */ -function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = 'drupal_goto') { - $batch =& batch_get(); - - drupal_theme_initialize(); - - if (isset($batch)) { - // Add process information - $process_info = array( - 'current_set' => 0, - 'progressive' => TRUE, - 'url' => $url, - 'url_options' => array(), - 'source_url' => $_GET['q'], - 'redirect' => $redirect, - 'theme' => $GLOBALS['theme_key'], - 'redirect_callback' => $redirect_callback, - ); - $batch += $process_info; - - // The batch is now completely built. Allow other modules to make changes - // to the batch so that it is easier to reuse batch processes in other - // environments. - drupal_alter('batch', $batch); - - // Assign an arbitrary id: don't rely on a serial column in the 'batch' - // table, since non-progressive batches skip database storage completely. - $batch['id'] = db_next_id(); - - // Move operations to a job queue. Non-progressive batches will use a - // memory-based queue. - foreach ($batch['sets'] as $key => $batch_set) { - _batch_populate_queue($batch, $key); - } - - // Initiate processing. - if ($batch['progressive']) { - // Now that we have a batch id, we can generate the redirection link in - // the generic error message. - $t = get_t(); - $batch['error_message'] = $t('Please continue to the error page', array('@error_url' => url($url, array('query' => array('id' => $batch['id'], 'op' => 'finished'))))); - - // Clear the way for the drupal_goto() redirection to the batch processing - // page, by saving and unsetting the 'destination', if there is any. - if (isset($_GET['destination'])) { - $batch['destination'] = $_GET['destination']; - unset($_GET['destination']); - } - - // Store the batch. - db_insert('batch') - ->fields(array( - 'bid' => $batch['id'], - 'timestamp' => REQUEST_TIME, - 'token' => drupal_get_token($batch['id']), - 'batch' => serialize($batch), - )) - ->execute(); - - // Set the batch number in the session to guarantee that it will stay alive. - $_SESSION['batches'][$batch['id']] = TRUE; - - // Redirect for processing. - $function = $batch['redirect_callback']; - if (function_exists($function)) { - $function($batch['url'], array('query' => array('op' => 'start', 'id' => $batch['id']))); - } - } - else { - // Non-progressive execution: bypass the whole progressbar workflow - // and execute the batch in one pass. - require_once DRUPAL_ROOT . '/includes/batch.inc'; - _batch_process(); - } - } -} - -/** - * Retrieves the current batch. - */ -function &batch_get() { - // Not drupal_static(), because Batch API operates at a lower level than most - // use-cases for resetting static variables, and we specifically do not want a - // global drupal_static_reset() resetting the batch information. Functions - // that are part of the Batch API and need to reset the batch information may - // call batch_get() and manipulate the result by reference. Functions that are - // not part of the Batch API can also do this, but shouldn't. - static $batch = array(); - return $batch; -} - -/** - * Populates a job queue with the operations of a batch set. - * - * Depending on whether the batch is progressive or not, the BatchQueue or - * BatchMemoryQueue handler classes will be used. - * - * @param $batch - * The batch array. - * @param $set_id - * The id of the set to process. - * @return - * The name and class of the queue are added by reference to the batch set. - */ -function _batch_populate_queue(&$batch, $set_id) { - $batch_set = &$batch['sets'][$set_id]; - - if (isset($batch_set['operations'])) { - $batch_set += array( - 'queue' => array( - 'name' => 'drupal_batch:' . $batch['id'] . ':' . $set_id, - 'class' => $batch['progressive'] ? 'BatchQueue' : 'BatchMemoryQueue', - ), - ); - - $queue = _batch_queue($batch_set); - $queue->createQueue(); - foreach ($batch_set['operations'] as $operation) { - $queue->createItem($operation); - } - - unset($batch_set['operations']); - } -} - -/** - * Returns a queue object for a batch set. - * - * @param $batch_set - * The batch set. - * @return - * The queue object. - */ -function _batch_queue($batch_set) { - static $queues; - - // The class autoloader is not available when running update.php, so make - // sure the files are manually included. - if (!isset($queues)) { - $queues = array(); - require_once DRUPAL_ROOT . '/modules/system/system.queue.inc'; - require_once DRUPAL_ROOT . '/includes/batch.queue.inc'; - } - - if (isset($batch_set['queue'])) { - $name = $batch_set['queue']['name']; - $class = $batch_set['queue']['class']; - - if (!isset($queues[$class][$name])) { - $queues[$class][$name] = new $class($name); - } - return $queues[$class][$name]; - } -} - -/** - * @} End of "defgroup batch". - */ diff --git a/includes/install.core.inc b/includes/install.core.inc deleted file mode 100644 index a74dfdf..0000000 --- a/includes/install.core.inc +++ /dev/null @@ -1,1831 +0,0 @@ - $interactive) + install_state_defaults(); - try { - // Begin the page request. This adds information about the current state of - // the Drupal installation to the passed-in array. - install_begin_request($install_state); - // Based on the installation state, run the remaining tasks for this page - // request, and collect any output. - $output = install_run_tasks($install_state); - } - catch (Exception $e) { - // When an installation error occurs, either send the error to the web - // browser or pass on the exception so the calling script can use it. - if ($install_state['interactive']) { - install_display_output($e->getMessage(), $install_state); - } - else { - throw $e; - } - } - // All available tasks for this page request are now complete. Interactive - // installations can send output to the browser or redirect the user to the - // next page. - if ($install_state['interactive']) { - if ($install_state['parameters_changed']) { - // Redirect to the correct page if the URL parameters have changed. - install_goto(install_redirect_url($install_state)); - } - elseif (isset($output)) { - // Display a page only if some output is available. Otherwise it is - // possible that we are printing a JSON page and theme output should - // not be shown. - install_display_output($output, $install_state); - } - } -} - -/** - * Returns an array of default settings for the global installation state. - * - * The installation state is initialized with these settings at the beginning - * of each page request. They may evolve during the page request, but they are - * initialized again once the next request begins. - * - * Non-interactive Drupal installations can override some of these default - * settings by passing in an array to the installation script, most notably - * 'parameters' (which contains one-time parameters such as 'profile' and - * 'locale' that are normally passed in via the URL) and 'forms' (which can - * be used to programmatically submit forms during the installation; the keys - * of each element indicate the name of the installation task that the form - * submission is for, and the values are used as the $form_state['values'] - * array that is passed on to the form submission via drupal_form_submit()). - * - * @see drupal_form_submit() - */ -function install_state_defaults() { - $defaults = array( - // The current task being processed. - 'active_task' => NULL, - // The last task that was completed during the previous installation - // request. - 'completed_task' => NULL, - // This becomes TRUE only when Drupal's system module is installed. - 'database_tables_exist' => FALSE, - // An array of forms to be programmatically submitted during the - // installation. The keys of each element indicate the name of the - // installation task that the form submission is for, and the values are - // used as the $form_state['values'] array that is passed on to the form - // submission via drupal_form_submit(). - 'forms' => array(), - // This becomes TRUE only at the end of the installation process, after - // all available tasks have been completed and Drupal is fully installed. - // It is used by the installer to store correct information in the database - // about the completed installation, as well as to inform theme functions - // that all tasks are finished (so that the task list can be displayed - // correctly). - 'installation_finished' => FALSE, - // Whether or not this installation is interactive. By default this will - // be set to FALSE if settings are passed in to install_drupal(). - 'interactive' => TRUE, - // An array of available languages for the installation. - 'locales' => array(), - // An array of parameters for the installation, pre-populated by the URL - // or by the settings passed in to install_drupal(). This is primarily - // used to store 'profile' (the name of the chosen installation profile) - // and 'locale' (the name of the chosen installation language), since - // these settings need to persist from page request to page request before - // the database is available for storage. - 'parameters' => array(), - // Whether or not the parameters have changed during the current page - // request. For interactive installations, this will trigger a page - // redirect. - 'parameters_changed' => FALSE, - // An array of information about the chosen installation profile. This will - // be filled in based on the profile's .info file. - 'profile_info' => array(), - // An array of available installation profiles. - 'profiles' => array(), - // An array of server variables that will be substituted into the global - // $_SERVER array via drupal_override_server_variables(). Used by - // non-interactive installations only. - 'server' => array(), - // This becomes TRUE only when a valid database connection can be - // established. - 'settings_verified' => FALSE, - // Installation tasks can set this to TRUE to force the page request to - // end (even if there is no themable output), in the case of an interactive - // installation. This is needed only rarely; for example, it would be used - // by an installation task that prints JSON output rather than returning a - // themed page. The most common example of this is during batch processing, - // but the Drupal installer automatically takes care of setting this - // parameter properly in that case, so that individual installation tasks - // which implement the batch API do not need to set it themselves. - 'stop_page_request' => FALSE, - // Installation tasks can set this to TRUE to indicate that the task should - // be run again, even if it normally wouldn't be. This can be used, for - // example, if a single task needs to be spread out over multiple page - // requests, or if it needs to perform some validation before allowing - // itself to be marked complete. The most common examples of this are batch - // processing and form submissions, but the Drupal installer automatically - // takes care of setting this parameter properly in those cases, so that - // individual installation tasks which implement the batch API or form API - // do not need to set it themselves. - 'task_not_complete' => FALSE, - // A list of installation tasks which have already been performed during - // the current page request. - 'tasks_performed' => array(), - ); - return $defaults; -} - -/** - * Begin an installation request, modifying the installation state as needed. - * - * This function performs commands that must run at the beginning of every page - * request. It throws an exception if the installation should not proceed. - * - * @param $install_state - * An array of information about the current installation state. This is - * modified with information gleaned from the beginning of the page request. - */ -function install_begin_request(&$install_state) { - // Add any installation parameters passed in via the URL. - $install_state['parameters'] += $_GET; - - // Validate certain core settings that are used throughout the installation. - if (!empty($install_state['parameters']['profile'])) { - $install_state['parameters']['profile'] = preg_replace('/[^a-zA-Z_0-9]/', '', $install_state['parameters']['profile']); - } - if (!empty($install_state['parameters']['locale'])) { - $install_state['parameters']['locale'] = preg_replace('/[^a-zA-Z_0-9\-]/', '', $install_state['parameters']['locale']); - } - - // Allow command line scripts to override server variables used by Drupal. - require_once DRUPAL_ROOT . '/includes/bootstrap.inc'; - if (!$install_state['interactive']) { - drupal_override_server_variables($install_state['server']); - } - - // 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 no - // installation be permitted using such a prefix. - if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], "simpletest") !== FALSE) { - header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); - exit; - } - - drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION); - - // This must go after drupal_bootstrap(), which unsets globals! - global $conf; - - require_once DRUPAL_ROOT . '/modules/system/system.install'; - require_once DRUPAL_ROOT . '/includes/common.inc'; - require_once DRUPAL_ROOT . '/includes/file.inc'; - require_once DRUPAL_ROOT . '/includes/install.inc'; - require_once DRUPAL_ROOT . '/' . variable_get('path_inc', 'includes/path.inc'); - - // Load module basics (needed for hook invokes). - include_once DRUPAL_ROOT . '/includes/module.inc'; - include_once DRUPAL_ROOT . '/includes/session.inc'; - - // Set up $language, so t() caller functions will still work. - drupal_language_initialize(); - - include_once DRUPAL_ROOT . '/includes/entity.inc'; - require_once DRUPAL_ROOT . '/includes/ajax.inc'; - $module_list['system']['filename'] = 'modules/system/system.module'; - $module_list['user']['filename'] = 'modules/user/user.module'; - module_list(TRUE, FALSE, FALSE, $module_list); - drupal_load('module', 'system'); - drupal_load('module', 'user'); - - // Load the cache infrastructure using a "fake" cache implementation that - // does not attempt to write to the database. We need this during the initial - // part of the installer because the database is not available yet. We - // continue to use it even when the database does become available, in order - // to preserve consistency between interactive and command-line installations - // (the latter complete in one page request and therefore are forced to - // continue using the cache implementation they started with) and also - // because any data put in the cache during the installer is inherently - // suspect, due to the fact that Drupal is not fully set up yet. - require_once DRUPAL_ROOT . '/includes/cache.inc'; - require_once DRUPAL_ROOT . '/includes/cache-install.inc'; - $conf['cache_default_class'] = 'DrupalFakeCache'; - - // Prepare for themed output. We need to run this at the beginning of the - // page request to avoid a different theme accidentally getting set. (We also - // need to run it even in the case of command-line installations, to prevent - // any code in the installer that happens to initialize the theme system from - // accessing the database before it is set up yet.) - drupal_maintenance_theme(); - - // Check existing settings.php. - $install_state['settings_verified'] = install_verify_settings(); - - if ($install_state['settings_verified']) { - // 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'; - - // Verify the last completed task in the database, if there is one. - $task = install_verify_completed_task(); - } - else { - $task = NULL; - - // Since previous versions of Drupal stored database connection information - // in the 'db_url' variable, we should never let an installation proceed if - // this variable is defined and the settings file was not verified above - // (otherwise we risk installing over an existing site whose settings file - // has not yet been updated). - if (!empty($GLOBALS['db_url'])) { - throw new Exception(install_already_done_error()); - } - } - - // Modify the installation state as appropriate. - $install_state['completed_task'] = $task; - $install_state['database_tables_exist'] = !empty($task); -} - -/** - * Runs all tasks for the current installation request. - * - * In the case of an interactive installation, all tasks will be attempted - * until one is reached that has output which needs to be displayed to the - * user, or until a page redirect is required. Otherwise, tasks will be - * attempted until the installation is finished. - * - * @param $install_state - * An array of information about the current installation state. This is - * passed along to each task, so it can be modified if necessary. - * - * @return - * HTML output from the last completed task. - */ -function install_run_tasks(&$install_state) { - do { - // Obtain a list of tasks to perform. The list of tasks itself can be - // dynamic (e.g., some might be defined by the installation profile, - // which is not necessarily known until the earlier tasks have run), - // so we regenerate the remaining tasks based on the installation state, - // each time through the loop. - $tasks_to_perform = install_tasks_to_perform($install_state); - // Run the first task on the list. - reset($tasks_to_perform); - $task_name = key($tasks_to_perform); - $task = array_shift($tasks_to_perform); - $install_state['active_task'] = $task_name; - $original_parameters = $install_state['parameters']; - $output = install_run_task($task, $install_state); - $install_state['parameters_changed'] = ($install_state['parameters'] != $original_parameters); - // Store this task as having been performed during the current request, - // and save it to the database as completed, if we need to and if the - // database is in a state that allows us to do so. Also mark the - // installation as 'done' when we have run out of tasks. - if (!$install_state['task_not_complete']) { - $install_state['tasks_performed'][] = $task_name; - $install_state['installation_finished'] = empty($tasks_to_perform); - if ($install_state['database_tables_exist'] && ($task['run'] == INSTALL_TASK_RUN_IF_NOT_COMPLETED || $install_state['installation_finished'])) { - variable_set('install_task', $install_state['installation_finished'] ? 'done' : $task_name); - } - } - // Stop when there are no tasks left. In the case of an interactive - // installation, also stop if we have some output to send to the browser, - // the URL parameters have changed, or an end to the page request was - // specifically called for. - $finished = empty($tasks_to_perform) || ($install_state['interactive'] && (isset($output) || $install_state['parameters_changed'] || $install_state['stop_page_request'])); - } while (!$finished); - return $output; -} - -/** - * Runs an individual installation task. - * - * @param $task - * An array of information about the task to be run. - * @param $install_state - * An array of information about the current installation state. This is - * passed in by reference so that it can be modified by the task. - * - * @return - * The output of the task function, if there is any. - */ -function install_run_task($task, &$install_state) { - $function = $task['function']; - - if ($task['type'] == 'form') { - require_once DRUPAL_ROOT . '/includes/form.inc'; - if ($install_state['interactive']) { - // For interactive forms, build the form and ensure that it will not - // redirect, since the installer handles its own redirection only after - // marking the form submission task complete. - $form_state = array( - // We need to pass $install_state by reference in order for forms to - // modify it, since the form API will use it in call_user_func_array(), - // which requires that referenced variables be passed explicitly. - 'build_info' => array('args' => array(&$install_state)), - 'no_redirect' => TRUE, - ); - $form = drupal_build_form($function, $form_state); - // If a successful form submission did not occur, the form needs to be - // rendered, which means the task is not complete yet. - if (empty($form_state['executed'])) { - $install_state['task_not_complete'] = TRUE; - return drupal_render($form); - } - // Otherwise, return nothing so the next task will run in the same - // request. - return; - } - else { - // For non-interactive forms, submit the form programmatically with the - // values taken from the installation state. Throw an exception if any - // errors were encountered. - $form_state = array( - 'values' => !empty($install_state['forms'][$function]) ? $install_state['forms'][$function] : array(), - // We need to pass $install_state by reference in order for forms to - // modify it, since the form API will use it in call_user_func_array(), - // which requires that referenced variables be passed explicitly. - 'build_info' => array('args' => array(&$install_state)), - ); - drupal_form_submit($function, $form_state); - $errors = form_get_errors(); - if (!empty($errors)) { - throw new Exception(implode("\n", $errors)); - } - } - } - - elseif ($task['type'] == 'batch') { - // Start a new batch based on the task function, if one is not running - // already. - $current_batch = variable_get('install_current_batch'); - if (!$install_state['interactive'] || !$current_batch) { - $batch = $function($install_state); - if (empty($batch)) { - // If the task did some processing and decided no batch was necessary, - // there is nothing more to do here. - return; - } - batch_set($batch); - // For interactive batches, we need to store the fact that this batch - // task is currently running. Otherwise, we need to make sure the batch - // will complete in one page request. - if ($install_state['interactive']) { - variable_set('install_current_batch', $function); - } - else { - $batch =& batch_get(); - $batch['progressive'] = FALSE; - } - // Process the batch. For progressive batches, this will redirect. - // Otherwise, the batch will complete. - batch_process(install_redirect_url($install_state), install_full_redirect_url($install_state)); - } - // If we are in the middle of processing this batch, keep sending back - // any output from the batch process, until the task is complete. - elseif ($current_batch == $function) { - include_once DRUPAL_ROOT . '/includes/batch.inc'; - $output = _batch_page(); - // The task is complete when we try to access the batch page and receive - // FALSE in return, since this means we are at a URL where we are no - // longer requesting a batch ID. - if ($output === FALSE) { - // Return nothing so the next task will run in the same request. - variable_del('install_current_batch'); - return; - } - else { - // We need to force the page request to end if the task is not - // complete, since the batch API sometimes prints JSON output - // rather than returning a themed page. - $install_state['task_not_complete'] = $install_state['stop_page_request'] = TRUE; - return $output; - } - } - } - - else { - // For normal tasks, just return the function result, whatever it is. - return $function($install_state); - } -} - -/** - * Returns a list of tasks to perform during the current installation request. - * - * Note that the list of tasks can change based on the installation state as - * the page request evolves (for example, if an installation profile hasn't - * been selected yet, we don't yet know which profile tasks need to be run). - * - * @param $install_state - * An array of information about the current installation state. - * - * @return - * A list of tasks to be performed, with associated metadata. - */ -function install_tasks_to_perform($install_state) { - // Start with a list of all currently available tasks. - $tasks = install_tasks($install_state); - foreach ($tasks as $name => $task) { - // Remove any tasks that were already performed or that never should run. - // Also, if we started this page request with an indication of the last - // task that was completed, skip that task and all those that come before - // it, unless they are marked as always needing to run. - if ($task['run'] == INSTALL_TASK_SKIP || in_array($name, $install_state['tasks_performed']) || (!empty($install_state['completed_task']) && empty($completed_task_found) && $task['run'] != INSTALL_TASK_RUN_IF_REACHED)) { - unset($tasks[$name]); - } - if (!empty($install_state['completed_task']) && $name == $install_state['completed_task']) { - $completed_task_found = TRUE; - } - } - return $tasks; -} - -/** - * Returns a list of all tasks the installer currently knows about. - * - * This function will return tasks regardless of whether or not they are - * intended to run on the current page request. However, the list can change - * based on the installation state (for example, if an installation profile - * hasn't been selected yet, we don't yet know which profile tasks will be - * available). - * - * @param $install_state - * An array of information about the current installation state. - * - * @return - * A list of tasks, with associated metadata. - */ -function install_tasks($install_state) { - // Determine whether translation import tasks will need to be performed. - $needs_translations = count($install_state['locales']) > 1 && !empty($install_state['parameters']['locale']) && $install_state['parameters']['locale'] != 'en'; - - // Start with the core installation tasks that run before handing control - // to the install profile. - $tasks = array( - 'install_select_profile' => array( - 'display_name' => st('Choose profile'), - 'display' => count($install_state['profiles']) != 1, - 'run' => INSTALL_TASK_RUN_IF_REACHED, - ), - 'install_select_locale' => array( - 'display_name' => st('Choose language'), - 'run' => INSTALL_TASK_RUN_IF_REACHED, - ), - 'install_load_profile' => array( - 'run' => INSTALL_TASK_RUN_IF_REACHED, - ), - 'install_verify_requirements' => array( - 'display_name' => st('Verify requirements'), - ), - 'install_settings_form' => array( - 'display_name' => st('Set up database'), - 'type' => 'form', - 'run' => $install_state['settings_verified'] ? INSTALL_TASK_SKIP : INSTALL_TASK_RUN_IF_NOT_COMPLETED, - ), - 'install_system_module' => array( - ), - 'install_bootstrap_full' => array( - 'run' => INSTALL_TASK_RUN_IF_REACHED, - ), - 'install_profile_modules' => array( - 'display_name' => count($install_state['profiles']) == 1 ? st('Install site') : st('Install profile'), - 'type' => 'batch', - ), - 'install_import_locales' => array( - 'display_name' => st('Set up translations'), - 'display' => $needs_translations, - 'type' => 'batch', - 'run' => $needs_translations ? INSTALL_TASK_RUN_IF_NOT_COMPLETED : INSTALL_TASK_SKIP, - ), - 'install_configure_form' => array( - 'display_name' => st('Configure site'), - 'type' => 'form', - ), - ); - - // Now add any tasks defined by the installation profile. - if (!empty($install_state['parameters']['profile'])) { - $function = $install_state['parameters']['profile'] . '_install_tasks'; - if (function_exists($function)) { - $result = $function($install_state); - if (is_array($result)) { - $tasks += $result; - } - } - } - - // Finish by adding the remaining core tasks. - $tasks += array( - 'install_import_locales_remaining' => array( - 'display_name' => st('Finish translations'), - 'display' => $needs_translations, - 'type' => 'batch', - 'run' => $needs_translations ? INSTALL_TASK_RUN_IF_NOT_COMPLETED : INSTALL_TASK_SKIP, - ), - 'install_finished' => array( - 'display_name' => st('Finished'), - ), - ); - - // Allow the installation profile to modify the full list of tasks. - if (!empty($install_state['parameters']['profile'])) { - $profile_file = DRUPAL_ROOT . '/profiles/' . $install_state['parameters']['profile'] . '/' . $install_state['parameters']['profile'] . '.profile'; - if (is_file($profile_file)) { - include_once $profile_file; - $function = $install_state['parameters']['profile'] . '_install_tasks_alter'; - if (function_exists($function)) { - $function($tasks, $install_state); - } - } - } - - // Fill in default parameters for each task before returning the list. - foreach ($tasks as $task_name => &$task) { - $task += array( - 'display_name' => NULL, - 'display' => !empty($task['display_name']), - 'type' => 'normal', - 'run' => INSTALL_TASK_RUN_IF_NOT_COMPLETED, - 'function' => $task_name, - ); - } - return $tasks; -} - -/** - * Returns a list of tasks that should be displayed to the end user. - * - * The output of this function is a list suitable for sending to - * theme_task_list(). - * - * @param $install_state - * An array of information about the current installation state. - * - * @return - * A list of tasks, with keys equal to the machine-readable task name and - * values equal to the name that should be displayed. - * - * @see theme_task_list() - */ -function install_tasks_to_display($install_state) { - $displayed_tasks = array(); - foreach (install_tasks($install_state) as $name => $task) { - if ($task['display']) { - $displayed_tasks[$name] = $task['display_name']; - } - } - return $displayed_tasks; -} - -/** - * Returns the URL that should be redirected to during an installation request. - * - * The output of this function is suitable for sending to install_goto(). - * - * @param $install_state - * An array of information about the current installation state. - * - * @return - * The URL to redirect to. - * - * @see install_full_redirect_url() - */ -function install_redirect_url($install_state) { - return 'install.php?' . drupal_http_build_query($install_state['parameters']); -} - -/** - * Returns the complete URL redirected to during an installation request. - * - * @param $install_state - * An array of information about the current installation state. - * - * @return - * The complete URL to redirect to. - * - * @see install_redirect_url() - */ -function install_full_redirect_url($install_state) { - global $base_url; - return $base_url . '/' . install_redirect_url($install_state); -} - -/** - * Displays themed installer output and ends the page request. - * - * Installation tasks should use drupal_set_title() to set the desired page - * title, but otherwise this function takes care of theming the overall page - * output during every step of the installation. - * - * @param $output - * The content to display on the main part of the page. - * @param $install_state - * An array of information about the current installation state. - */ -function install_display_output($output, $install_state) { - drupal_page_header(); - // Only show the task list if there is an active task; otherwise, the page - // request has ended before tasks have even been started, so there is nothing - // meaningful to show. - if (isset($install_state['active_task'])) { - // Let the theming function know when every step of the installation has - // been completed. - $active_task = $install_state['installation_finished'] ? NULL : $install_state['active_task']; - drupal_add_region_content('sidebar_first', theme('task_list', array('items' => install_tasks_to_display($install_state), 'active' => $active_task))); - } - print theme('install_page', array('content' => $output)); - exit; -} - -/** - * Installation task; verify the requirements for installing Drupal. - * - * @param $install_state - * An array of information about the current installation state. - * - * @return - * A themed status report, or an exception if there are requirement errors. - * Otherwise, no output is returned, so that the next task can be run - * in the same page request. - */ -function install_verify_requirements(&$install_state) { - // Check the installation requirements for Drupal and this profile. - $requirements = install_check_requirements($install_state); - - // Verify existence of all required modules. - $requirements += drupal_verify_profile($install_state); - - // Check the severity of the requirements reported. - $severity = drupal_requirements_severity($requirements); - - if ($severity == REQUIREMENT_ERROR) { - if ($install_state['interactive']) { - drupal_set_title(st('Requirements problem')); - $status_report = theme('status_report', array('requirements' => $requirements)); - $status_report .= st('Check the error messages and proceed with the installation.', array('!url' => check_url(request_uri()))); - return $status_report; - } - else { - // Throw an exception showing all unmet requirements. - $failures = array(); - foreach ($requirements as $requirement) { - if (isset($requirement['severity']) && $requirement['severity'] == REQUIREMENT_ERROR) { - $failures[] = $requirement['title'] . ': ' . $requirement['value'] . "\n\n" . $requirement['description']; - } - } - throw new Exception(implode("\n\n", $failures)); - } - } -} - -/** - * Installation task; install the Drupal system module. - * - * @param $install_state - * An array of information about the current installation state. - */ -function install_system_module(&$install_state) { - // Install system.module. - drupal_install_system(); - - // Enable the user module so that sessions can be recorded during the - // upcoming bootstrap step. - module_enable(array('user'), FALSE); - - // Save the list of other modules to install for the upcoming tasks. - // variable_set() can be used now that system.module is installed. - $modules = $install_state['profile_info']['dependencies']; - - // The install profile is also a module, which needs to be installed - // after all the dependencies have been installed. - $modules[] = drupal_get_profile(); - - variable_set('install_profile_modules', array_diff($modules, array('system'))); - $install_state['database_tables_exist'] = TRUE; -} - -/** - * Verify and return the last installation task that was completed. - * - * @return - * The last completed task, if there is one. An exception is thrown if Drupal - * is already installed. - */ -function install_verify_completed_task() { - try { - if ($result = db_query("SELECT value FROM {variable} WHERE name = :name", array('name' => 'install_task'))) { - $task = unserialize($result->fetchField()); - } - } - // Do not trigger an error if the database query fails, since the database - // might not be set up yet. - catch (Exception $e) { - } - if (isset($task)) { - if ($task == 'done') { - throw new Exception(install_already_done_error()); - } - return $task; - } -} - -/** - * Verifies the existing settings in settings.php. - */ -function install_verify_settings() { - global $databases; - - // Verify existing settings (if any). - if (!empty($databases) && install_verify_pdo()) { - $database = $databases['default']['default']; - drupal_static_reset('conf_path'); - $settings_file = './' . conf_path(FALSE) . '/settings.php'; - $errors = install_database_errors($database, $settings_file); - if (empty($errors)) { - return TRUE; - } - } - return FALSE; -} - -/** - * Verify PDO library. - */ -function install_verify_pdo() { - // PDO was moved to PHP core in 5.2.0, but the old extension (targeting 5.0 - // and 5.1) is still available from PECL, and can still be built without - // errors. To verify that the correct version is in use, we check the - // PDO::ATTR_DEFAULT_FETCH_MODE constant, which is not available in the - // PECL extension. - return extension_loaded('pdo') && defined('PDO::ATTR_DEFAULT_FETCH_MODE'); -} - -/** - * Installation task; define a form to configure and rewrite settings.php. - * - * @param $form_state - * An associative array containing the current state of the form. - * @param $install_state - * An array of information about the current installation state. - * - * @return - * The form API definition for the database configuration form. - */ -function install_settings_form($form, &$form_state, &$install_state) { - global $databases; - $profile = $install_state['parameters']['profile']; - $install_locale = $install_state['parameters']['locale']; - - drupal_static_reset('conf_path'); - $conf_path = './' . conf_path(FALSE); - $settings_file = $conf_path . '/settings.php'; - $database = isset($databases['default']['default']) ? $databases['default']['default'] : array(); - - drupal_set_title(st('Database configuration')); - - $drivers = drupal_get_database_types(); - $drivers_keys = array_keys($drivers); - - $form['driver'] = array( - '#type' => 'radios', - '#title' => st('Database type'), - '#required' => TRUE, - '#default_value' => !empty($database['driver']) ? $database['driver'] : current($drivers_keys), - '#description' => st('The type of database your @drupal data will be stored in.', array('@drupal' => drupal_install_profile_distribution_name())), - ); - if (count($drivers) == 1) { - $form['driver']['#disabled'] = TRUE; - $form['driver']['#description'] .= ' ' . st('Your PHP configuration only supports a single database type, so it has been automatically selected.'); - } - - // Add driver specific configuration options. - foreach ($drivers as $key => $driver) { - $form['driver']['#options'][$key] = $driver->name(); - - $form['settings'][$key] = $driver->getFormOptions($database); - $form['settings'][$key]['#prefix'] = '

    ' . st('@driver_name settings', array('@driver_name' => $driver->name())) . '

    '; - $form['settings'][$key]['#type'] = 'container'; - $form['settings'][$key]['#tree'] = TRUE; - $form['settings'][$key]['advanced_options']['#parents'] = array($key); - $form['settings'][$key]['#states'] = array( - 'visible' => array( - ':input[name=driver]' => array('value' => $key), - ) - ); - } - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['save'] = array( - '#type' => 'submit', - '#value' => st('Save and continue'), - '#limit_validation_errors' => array( - array('driver'), - array(isset($form_state['input']['driver']) ? $form_state['input']['driver'] : current($drivers_keys)), - ), - '#submit' => array('install_settings_form_submit'), - ); - - $form['errors'] = array(); - $form['settings_file'] = array('#type' => 'value', '#value' => $settings_file); - - return $form; -} - -/** - * Form API validate for install_settings form. - */ -function install_settings_form_validate($form, &$form_state) { - $driver = $form_state['values']['driver']; - $database = $form_state['values'][$driver]; - $database['driver'] = $driver; - - // TODO: remove when PIFR will be updated to use 'db_prefix' instead of - // 'prefix' in the database settings form. - $database['prefix'] = $database['db_prefix']; - unset($database['db_prefix']); - - $form_state['storage']['database'] = $database; - $errors = install_database_errors($database, $form_state['values']['settings_file']); - foreach ($errors as $name => $message) { - form_set_error($name, $message); - } -} - -/** - * Checks a database connection and returns any errors. - */ -function install_database_errors($database, $settings_file) { - global $databases; - $errors = array(); - - // Check database type. - $database_types = drupal_get_database_types(); - $driver = $database['driver']; - if (!isset($database_types[$driver])) { - $errors['driver'] = st("In your %settings_file file you have configured @drupal to use a %driver server, however your PHP installation currently does not support this database type.", array('%settings_file' => $settings_file, '@drupal' => drupal_install_profile_distribution_name(), '%driver' => $driver)); - } - else { - // Run driver specific validation - $errors += $database_types[$driver]->validateDatabaseSettings($database); - - // Run tasks associated with the database type. Any errors are caught in the - // calling function. - $databases['default']['default'] = $database; - // Just changing the global doesn't get the new information processed. - // We tell tell the Database class to re-parse $databases. - Database::parseConnectionInfo(); - - try { - db_run_tasks($driver); - } - catch (DatabaseTaskException $e) { - // These are generic errors, so we do not have any specific key of the - // database connection array to attach them to; therefore, we just put - // them in the error array with standard numeric keys. - $errors[$driver . '][0'] = $e->getMessage(); - } - } - return $errors; -} - -/** - * Form API submit for install_settings form. - */ -function install_settings_form_submit($form, &$form_state) { - global $install_state; - - // Update global settings array and save. - $settings['databases'] = array( - 'value' => array('default' => array('default' => $form_state['storage']['database'])), - 'required' => TRUE, - ); - $settings['drupal_hash_salt'] = array( - 'value' => drupal_hash_base64(drupal_random_bytes(55)), - 'required' => TRUE, - ); - drupal_rewrite_settings($settings); - // Indicate that the settings file has been verified, and check the database - // for the last completed task, now that we have a valid connection. This - // last step is important since we want to trigger an error if the new - // database already has Drupal installed. - $install_state['settings_verified'] = TRUE; - $install_state['completed_task'] = install_verify_completed_task(); -} - -/** - * Finds all .profile files. - */ -function install_find_profiles() { - return file_scan_directory('./profiles', '/\.profile$/', array('key' => 'name')); -} - -/** - * Installation task; select which profile to install. - * - * @param $install_state - * An array of information about the current installation state. The chosen - * profile will be added here, if it was not already selected previously, as - * will a list of all available profiles. - * - * @return - * For interactive installations, a form allowing the profile to be selected, - * if the user has a choice that needs to be made. Otherwise, an exception is - * thrown if a profile cannot be chosen automatically. - */ -function install_select_profile(&$install_state) { - $install_state['profiles'] += install_find_profiles(); - if (empty($install_state['parameters']['profile'])) { - // Try to find a profile. - $profile = _install_select_profile($install_state['profiles']); - if (empty($profile)) { - // We still don't have a profile, so display a form for selecting one. - // Only do this in the case of interactive installations, since this is - // not a real form with submit handlers (the database isn't even set up - // yet), rather just a convenience method for setting parameters in the - // URL. - if ($install_state['interactive']) { - include_once DRUPAL_ROOT . '/includes/form.inc'; - drupal_set_title(st('Select an installation profile')); - $form = drupal_get_form('install_select_profile_form', $install_state['profiles']); - return drupal_render($form); - } - else { - throw new Exception(install_no_profile_error()); - } - } - else { - $install_state['parameters']['profile'] = $profile; - } - } -} - -/** - * Helper function for automatically selecting an installation profile from a - * list or from a selection passed in via $_POST. - */ -function _install_select_profile($profiles) { - if (sizeof($profiles) == 0) { - throw new Exception(install_no_profile_error()); - } - // Don't need to choose profile if only one available. - if (sizeof($profiles) == 1) { - $profile = array_pop($profiles); - // TODO: is this right? - require_once DRUPAL_ROOT . '/' . $profile->uri; - return $profile->name; - } - else { - foreach ($profiles as $profile) { - if (!empty($_POST['profile']) && ($_POST['profile'] == $profile->name)) { - return $profile->name; - } - } - } -} - -/** - * Form API array definition for the profile selection form. - * - * @param $form_state - * Array of metadata about state of form processing. - * @param $profile_files - * Array of .profile files, as returned from file_scan_directory(). - */ -function install_select_profile_form($form, &$form_state, $profile_files) { - $profiles = array(); - $names = array(); - - foreach ($profile_files as $profile) { - // TODO: is this right? - include_once DRUPAL_ROOT . '/' . $profile->uri; - - $details = install_profile_info($profile->name); - // Don't show hidden profiles. This is used by to hide the testing profile, - // which only exists to speed up test runs. - if ($details['hidden'] === TRUE) { - continue; - } - $profiles[$profile->name] = $details; - - // Determine the name of the profile; default to file name if defined name - // is unspecified. - $name = isset($details['name']) ? $details['name'] : $profile->name; - $names[$profile->name] = $name; - } - - // Display radio buttons alphabetically by human-readable name, but always - // put the core profiles first (if they are present in the filesystem). - natcasesort($names); - if (isset($names['minimal'])) { - // If the expert ("Minimal") core profile is present, put it in front of - // any non-core profiles rather than including it with them alphabetically, - // since the other profiles might be intended to group together in a - // particular way. - $names = array('minimal' => $names['minimal']) + $names; - } - if (isset($names['standard'])) { - // If the default ("Standard") core profile is present, put it at the very - // top of the list. This profile will have its radio button pre-selected, - // so we want it to always appear at the top. - $names = array('standard' => $names['standard']) + $names; - } - - foreach ($names as $profile => $name) { - $form['profile'][$name] = array( - '#type' => 'radio', - '#value' => 'standard', - '#return_value' => $profile, - '#title' => $name, - '#description' => isset($profiles[$profile]['description']) ? $profiles[$profile]['description'] : '', - '#parents' => array('profile'), - ); - } - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => st('Save and continue'), - ); - return $form; -} - -/** - * Find all .po files for the current profile. - */ -function install_find_locales($profilename) { - $locales = file_scan_directory('./profiles/' . $profilename . '/translations', '/\.po$/', array('recurse' => FALSE)); - array_unshift($locales, (object) array('name' => 'en')); - foreach ($locales as $key => $locale) { - // The locale (file name) might be drupal-7.2.cs.po instead of cs.po. - $locales[$key]->langcode = preg_replace('!^(.+\.)?([^\.]+)$!', '\2', $locale->name); - // Language codes cannot exceed 12 characters to fit into the {languages} - // table. - if (strlen($locales[$key]->langcode) > 12) { - unset($locales[$key]); - } - } - return $locales; -} - -/** - * Installation task; select which locale to use for the current profile. - * - * @param $install_state - * An array of information about the current installation state. The chosen - * locale will be added here, if it was not already selected previously, as - * will a list of all available locales. - * - * @return - * For interactive installations, a form or other page output allowing the - * locale to be selected or providing information about locale selection, if - * a locale has not been chosen. Otherwise, an exception is thrown if a - * locale cannot be chosen automatically. - */ -function install_select_locale(&$install_state) { - // Find all available locales. - $profilename = $install_state['parameters']['profile']; - $locales = install_find_locales($profilename); - $install_state['locales'] += $locales; - - if (!empty($_POST['locale'])) { - foreach ($locales as $locale) { - if ($_POST['locale'] == $locale->langcode) { - $install_state['parameters']['locale'] = $locale->langcode; - return; - } - } - } - - if (empty($install_state['parameters']['locale'])) { - // If only the built-in (English) language is available, and we are - // performing an interactive installation, inform the user that the - // installer can be localized. Otherwise we assume the user knows what he - // is doing. - if (count($locales) == 1) { - if ($install_state['interactive']) { - drupal_set_title(st('Choose language')); - if (!empty($install_state['parameters']['localize'])) { - $output = '

    Follow these steps to translate Drupal into your language:

    '; - $output .= '
      '; - $output .= '
    1. Download a translation from the translation server.
    2. '; - $output .= '
    3. Place it into the following directory: -
      -/profiles/' . $profilename . '/translations/
      -
    4. '; - $output .= '
    '; - $output .= '

    For more information on installing Drupal in different languages, visit the drupal.org handbook page.

    '; - $output .= '

    How should the installation continue?

    '; - $output .= ''; - } - else { - include_once DRUPAL_ROOT . '/includes/form.inc'; - $elements = drupal_get_form('install_select_locale_form', $locales, $profilename); - $output = drupal_render($elements); - } - return $output; - } - // One language, but not an interactive installation. Assume the user - // knows what he is doing. - $locale = current($locales); - $install_state['parameters']['locale'] = $locale->name; - return; - } - else { - // Allow profile to pre-select the language, skipping the selection. - $function = $profilename . '_profile_details'; - if (function_exists($function)) { - $details = $function(); - if (isset($details['language'])) { - foreach ($locales as $locale) { - if ($details['language'] == $locale->name) { - $install_state['parameters']['locale'] = $locale->name; - return; - } - } - } - } - - // We still don't have a locale, so display a form for selecting one. - // Only do this in the case of interactive installations, since this is - // not a real form with submit handlers (the database isn't even set up - // yet), rather just a convenience method for setting parameters in the - // URL. - if ($install_state['interactive']) { - drupal_set_title(st('Choose language')); - include_once DRUPAL_ROOT . '/includes/form.inc'; - $elements = drupal_get_form('install_select_locale_form', $locales, $profilename); - return drupal_render($elements); - } - else { - throw new Exception(st('Sorry, you must select a language to continue the installation.')); - } - } - } -} - -/** - * Form API array definition for language selection. - */ -function install_select_locale_form($form, &$form_state, $locales, $profilename) { - include_once DRUPAL_ROOT . '/includes/iso.inc'; - $languages = _locale_get_predefined_list(); - foreach ($locales as $locale) { - $name = $locale->langcode; - if (isset($languages[$name])) { - $name = $languages[$name][0] . (isset($languages[$name][1]) ? ' ' . st('(@language)', array('@language' => $languages[$name][1])) : ''); - } - $form['locale'][$locale->langcode] = array( - '#type' => 'radio', - '#return_value' => $locale->langcode, - '#default_value' => $locale->langcode == 'en' ? 'en' : '', - '#title' => $name . ($locale->langcode == 'en' ? ' ' . st('(built-in)') : ''), - '#parents' => array('locale') - ); - } - if (count($locales) == 1) { - $form['help'] = array( - '#markup' => '

    ' . st('Learn how to install Drupal in other languages') . '

    ', - ); - } - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => st('Save and continue'), - ); - return $form; -} - -/** - * Indicates that there are no profiles available. - */ -function install_no_profile_error() { - drupal_set_title(st('No profiles available')); - return st('We were unable to find any installation profiles. Installation profiles tell us what modules to enable and what schema to install in the database. A profile is necessary to continue with the installation process.'); -} - -/** - * Indicates that Drupal has already been installed. - */ -function install_already_done_error() { - global $base_url; - - drupal_set_title(st('Drupal already installed')); - return st('
    • To start over, you must empty your existing database.
    • To install to a different database, edit the appropriate settings.php file in the sites folder.
    • To upgrade an existing installation, proceed to the update script.
    • View your existing site.
    ', array('@base-url' => $base_url)); -} - -/** - * Installation task; load information about the chosen profile. - * - * @param $install_state - * An array of information about the current installation state. The loaded - * profile information will be added here, or an exception will be thrown if - * the profile cannot be loaded. - */ -function install_load_profile(&$install_state) { - $profile_file = DRUPAL_ROOT . '/profiles/' . $install_state['parameters']['profile'] . '/' . $install_state['parameters']['profile'] . '.profile'; - if (is_file($profile_file)) { - include_once $profile_file; - $install_state['profile_info'] = install_profile_info($install_state['parameters']['profile'], $install_state['parameters']['locale']); - } - else { - throw new Exception(st('Sorry, the profile you have chosen cannot be loaded.')); - } -} - -/** - * Installation task; perform a full bootstrap of Drupal. - * - * @param $install_state - * An array of information about the current installation state. - */ -function install_bootstrap_full(&$install_state) { - drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); -} - -/** - * Installation task; install required modules via a batch process. - * - * @param $install_state - * An array of information about the current installation state. - * - * @return - * The batch definition. - */ -function install_profile_modules(&$install_state) { - $modules = variable_get('install_profile_modules', array()); - $files = system_rebuild_module_data(); - variable_del('install_profile_modules'); - - // Always install required modules first. Respect the dependencies between - // the modules. - $required = array(); - $non_required = array(); - // Although the profile module is marked as required, it needs to go after - // every dependency, including non-required ones. So clear its required - // flag for now to allow it to install late. - $files[$install_state['parameters']['profile']]->info['required'] = FALSE; - // Add modules that other modules depend on. - foreach ($modules as $module) { - if ($files[$module]->requires) { - $modules = array_merge($modules, array_keys($files[$module]->requires)); - } - } - $modules = array_unique($modules); - foreach ($modules as $module) { - if (!empty($files[$module]->info['required'])) { - $required[$module] = $files[$module]->sort; - } - else { - $non_required[$module] = $files[$module]->sort; - } - } - arsort($required); - arsort($non_required); - - $operations = array(); - foreach ($required + $non_required as $module => $weight) { - $operations[] = array('_install_module_batch', array($module, $files[$module]->info['name'])); - } - $batch = array( - 'operations' => $operations, - 'title' => st('Installing @drupal', array('@drupal' => drupal_install_profile_distribution_name())), - 'error_message' => st('The installation has encountered an error.'), - 'finished' => '_install_profile_modules_finished', - ); - return $batch; -} - -/** - * Installation task; import languages via a batch process. - * - * @param $install_state - * An array of information about the current installation state. - * - * @return - * The batch definition, if there are language files to import. - */ -function install_import_locales(&$install_state) { - include_once DRUPAL_ROOT . '/includes/locale.inc'; - $install_locale = $install_state['parameters']['locale']; - - include_once DRUPAL_ROOT . '/includes/iso.inc'; - $predefined = _locale_get_predefined_list(); - if (!isset($predefined[$install_locale])) { - // Drupal does not know about this language, so we prefill its values with - // our best guess. The user will be able to edit afterwards. - locale_add_language($install_locale, $install_locale, $install_locale, LANGUAGE_LTR, '', '', TRUE, TRUE); - } - else { - // A known predefined language, details will be filled in properly. - locale_add_language($install_locale, NULL, NULL, NULL, '', '', TRUE, TRUE); - } - - // Collect files to import for this language. - $batch = locale_batch_by_language($install_locale, NULL); - if (!empty($batch)) { - // Remember components we cover in this batch set. - variable_set('install_locale_batch_components', $batch['#components']); - return $batch; - } -} - -/** - * Installation task; configure settings for the new site. - * - * @param $form_state - * An associative array containing the current state of the form. - * @param $install_state - * An array of information about the current installation state. - * - * @return - * The form API definition for the site configuration form. - */ -function install_configure_form($form, &$form_state, &$install_state) { - if (variable_get('site_name', FALSE) || variable_get('site_mail', FALSE)) { - // Site already configured: This should never happen, means re-running the - // installer, possibly by an attacker after the 'install_task' variable got - // accidentally blown somewhere. Stop it now. - throw new Exception(install_already_done_error()); - } - - drupal_set_title(st('Configure site')); - - // Warn about settings.php permissions risk - $settings_dir = conf_path(); - $settings_file = $settings_dir . '/settings.php'; - // Check that $_POST is empty so we only show this message when the form is - // first displayed, not on the next page after it is submitted. (We do not - // want to repeat it multiple times because it is a general warning that is - // not related to the rest of the installation process; it would also be - // especially out of place on the last page of the installer, where it would - // distract from the message that the Drupal installation has completed - // successfully.) - if (empty($_POST) && (!drupal_verify_install_file(DRUPAL_ROOT . '/' . $settings_file, FILE_EXIST|FILE_READABLE|FILE_NOT_WRITABLE) || !drupal_verify_install_file(DRUPAL_ROOT . '/' . $settings_dir, FILE_NOT_WRITABLE, 'dir'))) { - drupal_set_message(st('All necessary changes to %dir and %file have been made, so you should remove write permissions to them now in order to avoid security risks. If you are unsure how to do so, consult the online handbook.', array('%dir' => $settings_dir, '%file' => $settings_file, '@handbook_url' => 'http://drupal.org/server-permissions')), 'warning'); - } - - drupal_add_js(drupal_get_path('module', 'system') . '/system.js'); - // Add JavaScript time zone detection. - drupal_add_js('misc/timezone.js'); - // We add these strings as settings because JavaScript translation does not - // work on install time. - drupal_add_js(array('copyFieldValue' => array('edit-site-mail' => array('edit-account-mail'))), 'setting'); - drupal_add_js('jQuery(function () { Drupal.cleanURLsInstallCheck(); });', 'inline'); - // Add JS to show / hide the 'Email administrator about site updates' elements - drupal_add_js('jQuery(function () { Drupal.hideEmailAdministratorCheckbox() });', 'inline'); - // Build menu to allow clean URL check. - menu_rebuild(); - - // Cache a fully-built schema. This is necessary for any invocation of - // index.php because: (1) setting cache table entries requires schema - // information, (2) that occurs during bootstrap before any module are - // loaded, so (3) if there is no cached schema, drupal_get_schema() will - // try to generate one but with no loaded modules will return nothing. - // - // This logically could be done during the 'install_finished' task, but the - // clean URL check requires it now. - drupal_get_schema(NULL, TRUE); - - // Return the form. - return _install_configure_form($form, $form_state, $install_state); -} - -/** - * Installation task; import remaining languages via a batch process. - * - * @param $install_state - * An array of information about the current installation state. - * - * @return - * The batch definition, if there are language files to import. - */ -function install_import_locales_remaining(&$install_state) { - include_once DRUPAL_ROOT . '/includes/locale.inc'; - // Collect files to import for this language. Skip components already covered - // in the initial batch set. - $install_locale = $install_state['parameters']['locale']; - $batch = locale_batch_by_language($install_locale, NULL, variable_get('install_locale_batch_components', array())); - // Remove temporary variable. - variable_del('install_locale_batch_components'); - return $batch; -} - -/** - * Installation task; perform final steps and display a 'finished' page. - * - * @param $install_state - * An array of information about the current installation state. - * - * @return - * A message informing the user that the installation is complete. - */ -function install_finished(&$install_state) { - drupal_set_title(st('@drupal installation complete', array('@drupal' => drupal_install_profile_distribution_name())), PASS_THROUGH); - $messages = drupal_set_message(); - $output = '

    ' . st('Congratulations, you installed @drupal!', array('@drupal' => drupal_install_profile_distribution_name())) . '

    '; - $output .= '

    ' . (isset($messages['error']) ? st('Review the messages above before visiting your new site.', array('@url' => url(''))) : st('Visit your new site.', array('@url' => url('')))) . '

    '; - - // Flush all caches to ensure that any full bootstraps during the installer - // do not leave stale cached data, and that any content types or other items - // registered by the install profile are registered correctly. - drupal_flush_all_caches(); - - // Remember the profile which was used. - variable_set('install_profile', drupal_get_profile()); - - // Install profiles are always loaded last - db_update('system') - ->fields(array('weight' => 1000)) - ->condition('type', 'module') - ->condition('name', drupal_get_profile()) - ->execute(); - - // Cache a fully-built schema. - drupal_get_schema(NULL, TRUE); - - // Run cron to populate update status tables (if available) so that users - // will be warned if they've installed an out of date Drupal version. - // Will also trigger indexing of profile-supplied content or feeds. - drupal_cron_run(); - - return $output; -} - -/** - * Batch callback for batch installation of modules. - */ -function _install_module_batch($module, $module_name, &$context) { - // Install and enable the module right away, so that the module will be - // loaded by drupal_bootstrap in subsequent batch requests, and other - // modules possibly depending on it can safely perform their installation - // steps. - module_enable(array($module), FALSE); - $context['results'][] = $module; - $context['message'] = st('Installed %module module.', array('%module' => $module_name)); -} - -/** - * 'Finished' callback for module installation batch. - */ -function _install_profile_modules_finished($success, $results, $operations) { - // Flush all caches to complete the module installation process. Subsequent - // installation tasks will now have full access to the profile's modules. - drupal_flush_all_caches(); -} - -/** - * Checks installation requirements and reports any errors. - */ -function install_check_requirements($install_state) { - $profile = $install_state['parameters']['profile']; - - // Check the profile requirements. - $requirements = drupal_check_profile($profile); - - // If Drupal is not set up already, we need to create a settings file. - if (!$install_state['settings_verified']) { - $writable = FALSE; - $conf_path = './' . conf_path(FALSE, TRUE); - $settings_file = $conf_path . '/settings.php'; - $default_settings_file = './sites/default/default.settings.php'; - $file = $conf_path; - $exists = FALSE; - // Verify that the directory exists. - if (drupal_verify_install_file($conf_path, FILE_EXIST, 'dir')) { - // Check if a settings.php file already exists. - $file = $settings_file; - if (drupal_verify_install_file($settings_file, FILE_EXIST)) { - // If it does, make sure it is writable. - $writable = drupal_verify_install_file($settings_file, FILE_READABLE|FILE_WRITABLE); - $exists = TRUE; - } - } - - // If default.settings.php does not exist, or is not readable, throw an - // error. - if (!drupal_verify_install_file($default_settings_file, FILE_EXIST|FILE_READABLE)) { - $requirements['default settings file exists'] = array( - 'title' => st('Default settings file'), - 'value' => st('The default settings file does not exist.'), - 'severity' => REQUIREMENT_ERROR, - 'description' => st('The @drupal installer requires that the %default-file file not be modified in any way from the original download.', array('@drupal' => drupal_install_profile_distribution_name(), '%default-file' => $default_settings_file)), - ); - } - // Otherwise, if settings.php does not exist yet, we can try to copy - // default.settings.php to create it. - elseif (!$exists) { - $copied = drupal_verify_install_file($conf_path, FILE_EXIST|FILE_WRITABLE, 'dir') && @copy($default_settings_file, $settings_file); - if ($copied) { - // If the new settings file has the same owner as default.settings.php, - // this means default.settings.php is owned by the webserver user. - // This is an inherent security weakness because it allows a malicious - // webserver process to append arbitrary PHP code and then execute it. - // However, it is also a common configuration on shared hosting, and - // there is nothing Drupal can do to prevent it. In this situation, - // having settings.php also owned by the webserver does not introduce - // any additional security risk, so we keep the file in place. - if (fileowner($default_settings_file) === fileowner($settings_file)) { - $writable = drupal_verify_install_file($settings_file, FILE_READABLE|FILE_WRITABLE); - $exists = TRUE; - } - // If settings.php and default.settings.php have different owners, this - // probably means the server is set up "securely" (with the webserver - // running as its own user, distinct from the user who owns all the - // Drupal PHP files), although with either a group or world writable - // sites directory. Keeping settings.php owned by the webserver would - // therefore introduce a security risk. It would also cause a usability - // problem, since site owners who do not have root access to the file - // system would be unable to edit their settings file later on. We - // therefore must delete the file we just created and force the - // administrator to log on to the server and create it manually. - else { - $deleted = @drupal_unlink($settings_file); - // We expect deleting the file to be successful (since we just - // created it ourselves above), but if it fails somehow, we set a - // variable so we can display a one-time error message to the - // administrator at the bottom of the requirements list. We also try - // to make the file writable, to eliminate any conflicting error - // messages in the requirements list. - $exists = !$deleted; - if ($exists) { - $settings_file_ownership_error = TRUE; - $writable = drupal_verify_install_file($settings_file, FILE_READABLE|FILE_WRITABLE); - } - } - } - } - - // If settings.php does not exist, throw an error. - if (!$exists) { - $requirements['settings file exists'] = array( - 'title' => st('Settings file'), - 'value' => st('The settings file does not exist.'), - 'severity' => REQUIREMENT_ERROR, - 'description' => st('The @drupal installer requires that you create a settings file as part of the installation process. Copy the %default_file file to %file. More details about installing Drupal are available in INSTALL.txt.', array('@drupal' => drupal_install_profile_distribution_name(), '%file' => $file, '%default_file' => $default_settings_file, '@install_txt' => base_path() . 'INSTALL.txt')), - ); - } - else { - $requirements['settings file exists'] = array( - 'title' => st('Settings file'), - 'value' => st('The %file file exists.', array('%file' => $file)), - ); - // If settings.php is not writable, throw an error. - if (!$writable) { - $requirements['settings file writable'] = array( - 'title' => st('Settings file'), - 'value' => st('The settings file is not writable.'), - 'severity' => REQUIREMENT_ERROR, - 'description' => st('The @drupal installer requires write permissions to %file during the installation process. If you are unsure how to grant file permissions, consult the online handbook.', array('@drupal' => drupal_install_profile_distribution_name(), '%file' => $file, '@handbook_url' => 'http://drupal.org/server-permissions')), - ); - } - else { - $requirements['settings file'] = array( - 'title' => st('Settings file'), - 'value' => st('The settings file is writable.'), - ); - } - if (!empty($settings_file_ownership_error)) { - $requirements['settings file ownership'] = array( - 'title' => st('Settings file'), - 'value' => st('The settings file is owned by the web server.'), - 'severity' => REQUIREMENT_ERROR, - 'description' => st('The @drupal installer failed to create a settings file with proper file ownership. Log on to your web server, remove the existing %file file, and create a new one by copying the %default_file file to %file. More details about installing Drupal are available in INSTALL.txt. If you have problems with the file permissions on your server, consult the online handbook.', array('@drupal' => drupal_install_profile_distribution_name(), '%file' => $file, '%default_file' => $default_settings_file, '@install_txt' => base_path() . 'INSTALL.txt', '@handbook_url' => 'http://drupal.org/server-permissions')), - ); - } - } - } - return $requirements; -} - -/** - * Forms API array definition for site configuration. - */ -function _install_configure_form($form, &$form_state, &$install_state) { - include_once DRUPAL_ROOT . '/includes/locale.inc'; - - $form['site_information'] = array( - '#type' => 'fieldset', - '#title' => st('Site information'), - '#collapsible' => FALSE, - ); - $form['site_information']['site_name'] = array( - '#type' => 'textfield', - '#title' => st('Site name'), - '#required' => TRUE, - '#weight' => -20, - ); - $form['site_information']['site_mail'] = array( - '#type' => 'textfield', - '#title' => st('Site e-mail address'), - '#default_value' => ini_get('sendmail_from'), - '#description' => st("Automated e-mails, such as registration information, will be sent from this address. Use an address ending in your site's domain to help prevent these e-mails from being flagged as spam."), - '#required' => TRUE, - '#weight' => -15, - ); - $form['admin_account'] = array( - '#type' => 'fieldset', - '#title' => st('Site maintenance account'), - '#collapsible' => FALSE, - ); - - $form['admin_account']['account']['#tree'] = TRUE; - $form['admin_account']['account']['name'] = array('#type' => 'textfield', - '#title' => st('Username'), - '#maxlength' => USERNAME_MAX_LENGTH, - '#description' => st('Spaces are allowed; punctuation is not allowed except for periods, hyphens, and underscores.'), - '#required' => TRUE, - '#weight' => -10, - '#attributes' => array('class' => array('username')), - ); - - $form['admin_account']['account']['mail'] = array('#type' => 'textfield', - '#title' => st('E-mail address'), - '#maxlength' => EMAIL_MAX_LENGTH, - '#required' => TRUE, - '#weight' => -5, - ); - $form['admin_account']['account']['pass'] = array( - '#type' => 'password_confirm', - '#required' => TRUE, - '#size' => 25, - '#weight' => 0, - ); - - $form['server_settings'] = array( - '#type' => 'fieldset', - '#title' => st('Server settings'), - '#collapsible' => FALSE, - ); - - $countries = country_get_list(); - $form['server_settings']['site_default_country'] = array( - '#type' => 'select', - '#title' => st('Default country'), - '#empty_value' => '', - '#default_value' => variable_get('site_default_country', NULL), - '#options' => $countries, - '#description' => st('Select the default country for the site.'), - '#weight' => 0, - ); - - $form['server_settings']['date_default_timezone'] = array( - '#type' => 'select', - '#title' => st('Default time zone'), - '#default_value' => date_default_timezone_get(), - '#options' => system_time_zones(), - '#description' => st('By default, dates in this site will be displayed in the chosen time zone.'), - '#weight' => 5, - '#attributes' => array('class' => array('timezone-detect')), - ); - - $form['server_settings']['clean_url'] = array( - '#type' => 'hidden', - '#default_value' => 0, - '#attributes' => array('id' => 'edit-clean-url', 'class' => array('install')), - ); - - $form['update_notifications'] = array( - '#type' => 'fieldset', - '#title' => st('Update notifications'), - '#collapsible' => FALSE, - ); - $form['update_notifications']['update_status_module'] = array( - '#type' => 'checkboxes', - '#options' => array( - 1 => st('Check for updates automatically'), - 2 => st('Receive e-mail notifications'), - ), - '#default_value' => array(1, 2), - '#description' => st('The system will notify you when updates and important security releases are available for installed components. Anonymous information about your site is sent to Drupal.org.', array('@drupal' => 'http://drupal.org')), - '#weight' => 15, - ); - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => st('Save and continue'), - '#weight' => 15, - ); - - return $form; -} - -/** - * Forms API validate for the site configuration form. - */ -function install_configure_form_validate($form, &$form_state) { - if ($error = user_validate_name($form_state['values']['account']['name'])) { - form_error($form['admin_account']['account']['name'], $error); - } - if ($error = user_validate_mail($form_state['values']['account']['mail'])) { - form_error($form['admin_account']['account']['mail'], $error); - } - if ($error = user_validate_mail($form_state['values']['site_mail'])) { - form_error($form['site_information']['site_mail'], $error); - } -} - -/** - * Forms API submit for the site configuration form. - */ -function install_configure_form_submit($form, &$form_state) { - global $user; - - variable_set('site_name', $form_state['values']['site_name']); - variable_set('site_mail', $form_state['values']['site_mail']); - variable_set('date_default_timezone', $form_state['values']['date_default_timezone']); - variable_set('site_default_country', $form_state['values']['site_default_country']); - - // Enable update.module if this option was selected. - if ($form_state['values']['update_status_module'][1]) { - module_enable(array('update'), FALSE); - - // Add the site maintenance account's email address to the list of - // addresses to be notified when updates are available, if selected. - if ($form_state['values']['update_status_module'][2]) { - variable_set('update_notify_emails', array($form_state['values']['account']['mail'])); - } - } - - // We precreated user 1 with placeholder values. Let's save the real values. - $account = user_load(1); - $merge_data = array('init' => $form_state['values']['account']['mail'], 'roles' => !empty($account->roles) ? $account->roles : array(), 'status' => 1); - user_save($account, array_merge($form_state['values']['account'], $merge_data)); - // Load global $user and perform final login tasks. - $user = user_load(1); - user_login_finalize(); - - if (isset($form_state['values']['clean_url'])) { - variable_set('clean_url', $form_state['values']['clean_url']); - } - - // Record when this install ran. - variable_set('install_time', $_SERVER['REQUEST_TIME']); -} diff --git a/includes/install.inc b/includes/install.inc deleted file mode 100644 index 3634660..0000000 --- a/includes/install.inc +++ /dev/null @@ -1,1245 +0,0 @@ - $schema_version) { - if ($schema_version > -1) { - module_load_install($module); - } - } -} - -/** - * Returns an array of available schema versions for a module. - * - * @param $module - * A module name. - * @return - * If the module has updates, an array of available updates sorted by version. - * Otherwise, FALSE. - */ -function drupal_get_schema_versions($module) { - $updates = &drupal_static(__FUNCTION__, NULL); - if (!isset($updates[$module])) { - $updates = array(); - - foreach (module_list() as $loaded_module) { - $updates[$loaded_module] = array(); - } - - // Prepare regular expression to match all possible defined hook_update_N(). - $regexp = '/^(?P.+)_update_(?P\d+)$/'; - $functions = get_defined_functions(); - // Narrow this down to functions ending with an integer, since all - // hook_update_N() functions end this way, and there are other - // possible functions which match '_update_'. We use preg_grep() here - // instead of foreaching through all defined functions, since the loop - // through all PHP functions can take significant page execution time - // and this function is called on every administrative page via - // system_requirements(). - foreach (preg_grep('/_\d+$/', $functions['user']) as $function) { - // If this function is a module update function, add it to the list of - // module updates. - if (preg_match($regexp, $function, $matches)) { - $updates[$matches['module']][] = $matches['version']; - } - } - // Ensure that updates are applied in numerical order. - foreach ($updates as &$module_updates) { - sort($module_updates, SORT_NUMERIC); - } - } - return empty($updates[$module]) ? FALSE : $updates[$module]; -} - -/** - * Returns the currently installed schema version for a module. - * - * @param $module - * A module name. - * @param $reset - * Set to TRUE after modifying the system table. - * @param $array - * Set to TRUE if you want to get information about all modules in the - * system. - * @return - * The currently installed schema version, or SCHEMA_UNINSTALLED if the - * module is not installed. - */ -function drupal_get_installed_schema_version($module, $reset = FALSE, $array = FALSE) { - static $versions = array(); - - if ($reset) { - $versions = array(); - } - - if (!$versions) { - $versions = array(); - $result = db_query("SELECT name, schema_version FROM {system} WHERE type = :type", array(':type' => 'module')); - foreach ($result as $row) { - $versions[$row->name] = $row->schema_version; - } - } - - if ($array) { - return $versions; - } - else { - return isset($versions[$module]) ? $versions[$module] : SCHEMA_UNINSTALLED; - } -} - -/** - * Update the installed version information for a module. - * - * @param $module - * A module name. - * @param $version - * The new schema version. - */ -function drupal_set_installed_schema_version($module, $version) { - db_update('system') - ->fields(array('schema_version' => $version)) - ->condition('name', $module) - ->execute(); - - // Reset the static cache of module schema versions. - drupal_get_installed_schema_version(NULL, TRUE); -} - -/** - * Loads the install profile, extracting its defined distribution name. - * - * @return - * The distribution name defined in the profile's .info file. Defaults to - * "Drupal" if none is explicitly provided by the install profile. - * - * @see install_profile_info() - */ -function drupal_install_profile_distribution_name() { - // During installation, the profile information is stored in the global - // installation state (it might not be saved anywhere yet). - if (drupal_installation_attempted()) { - global $install_state; - return $install_state['profile_info']['distribution_name']; - } - // At all other times, we load the profile via standard methods. - else { - $profile = drupal_get_profile(); - $info = system_get_info('module', $profile); - return $info['distribution_name']; - } -} - -/** - * Auto detect the base_url with PHP predefined variables. - * - * @param $file - * The name of the file calling this function so we can strip it out of - * the URI when generating the base_url. - * @return - * The auto-detected $base_url that should be configured in settings.php - */ -function drupal_detect_baseurl($file = 'install.php') { - $proto = $_SERVER['HTTPS'] ? 'https://' : 'http://'; - $host = $_SERVER['SERVER_NAME']; - $port = ($_SERVER['SERVER_PORT'] == 80 ? '' : ':' . $_SERVER['SERVER_PORT']); - $uri = preg_replace("/\?.*/", '', $_SERVER['REQUEST_URI']); - $dir = str_replace("/$file", '', $uri); - - return "$proto$host$port$dir"; -} - -/** - * Detect all supported databases that are compiled into PHP. - * - * @return - * An array of database types compiled into PHP. - */ -function drupal_detect_database_types() { - $databases = drupal_get_database_types(); - - foreach ($databases as $driver => $installer) { - $databases[$driver] = $installer->name(); - } - - return $databases; -} - -/** - * Return all supported database installer objects that are compiled into PHP. - * - * @return - * An array of database installer objects compiled into PHP. - */ -function drupal_get_database_types() { - $databases = array(); - - // We define a driver as a directory in /includes/database that in turn - // contains a database.inc file. That allows us to drop in additional drivers - // without modifying the installer. - // Because we have no registry yet, we need to also include the install.inc - // file for the driver explicitly. - require_once DRUPAL_ROOT . '/includes/database/database.inc'; - foreach (file_scan_directory(DRUPAL_ROOT . '/includes/database', '/^[a-z]*$/i', array('recurse' => FALSE)) as $file) { - if (file_exists($file->uri . '/database.inc') && file_exists($file->uri . '/install.inc')) { - $drivers[$file->filename] = $file->uri; - } - } - - foreach ($drivers as $driver => $file) { - $installer = db_installer_object($driver); - if ($installer->installable()) { - $databases[$driver] = $installer; - } - } - - // Usability: unconditionally put the MySQL driver on top. - if (isset($databases['mysql'])) { - $mysql_database = $databases['mysql']; - unset($databases['mysql']); - $databases = array('mysql' => $mysql_database) + $databases; - } - - return $databases; -} - -/** - * Database installer structure. - * - * Defines basic Drupal requirements for databases. - */ -abstract class DatabaseTasks { - - /** - * Structure that describes each task to run. - * - * @var array - * - * Each value of the tasks array is an associative array defining the function - * to call (optional) and any arguments to be passed to the function. - */ - protected $tasks = array( - array( - 'function' => 'checkEngineVersion', - 'arguments' => array(), - ), - array( - 'arguments' => array( - 'CREATE TABLE {drupal_install_test} (id int NULL)', - 'Drupal can use CREATE TABLE database commands.', - 'Failed to CREATE a test table on your database server with the command %query. The server reports the following message: %error.

    Are you sure the configured username has the necessary permissions to create tables in the database?

    ', - TRUE, - ), - ), - array( - 'arguments' => array( - 'INSERT INTO {drupal_install_test} (id) VALUES (1)', - 'Drupal can use INSERT database commands.', - 'Failed to INSERT a value into a test table on your database server. We tried inserting a value with the command %query and the server reported the following error: %error.', - ), - ), - array( - 'arguments' => array( - 'UPDATE {drupal_install_test} SET id = 2', - 'Drupal can use UPDATE database commands.', - 'Failed to UPDATE a value in a test table on your database server. We tried updating a value with the command %query and the server reported the following error: %error.', - ), - ), - array( - 'arguments' => array( - 'DELETE FROM {drupal_install_test}', - 'Drupal can use DELETE database commands.', - 'Failed to DELETE a value from a test table on your database server. We tried deleting a value with the command %query and the server reported the following error: %error.', - ), - ), - array( - 'arguments' => array( - 'DROP TABLE {drupal_install_test}', - 'Drupal can use DROP TABLE database commands.', - 'Failed to DROP a test table from your database server. We tried dropping a table with the command %query and the server reported the following error %error.', - ), - ), - ); - - /** - * Results from tasks. - * - * @var array - */ - protected $results = array(); - - /** - * Ensure the PDO driver is supported by the version of PHP in use. - */ - protected function hasPdoDriver() { - return in_array($this->pdoDriver, PDO::getAvailableDrivers()); - } - - /** - * Assert test as failed. - */ - protected function fail($message) { - $this->results[$message] = FALSE; - } - - /** - * Assert test as a pass. - */ - protected function pass($message) { - $this->results[$message] = TRUE; - } - - /** - * Check whether Drupal is installable on the database. - */ - public function installable() { - return $this->hasPdoDriver() && empty($this->error); - } - - /** - * Return the human-readable name of the driver. - */ - abstract public function name(); - - /** - * Return the minimum required version of the engine. - * - * @return - * A version string. If not NULL, it will be checked against the version - * reported by the Database engine using version_compare(). - */ - public function minimumVersion() { - return NULL; - } - - /** - * Run database tasks and tests to see if Drupal can run on the database. - */ - public function runTasks() { - // We need to establish a connection before we can run tests. - if ($this->connect()) { - foreach ($this->tasks as $task) { - if (!isset($task['function'])) { - $task['function'] = 'runTestQuery'; - } - if (method_exists($this, $task['function'])) { - // Returning false is fatal. No other tasks can run. - if (FALSE === call_user_func_array(array($this, $task['function']), $task['arguments'])) { - break; - } - } - else { - throw new DatabaseTaskException(st("Failed to run all tasks against the database server. The task %task wasn't found.", array('%task' => $task['function']))); - } - } - } - // Check for failed results and compile message - $message = ''; - foreach ($this->results as $result => $success) { - if (!$success) { - $message .= '

    ' . $result . '

    '; - } - } - if (!empty($message)) { - $message = '

    In order for Drupal to work, and to continue with the installation process, you must resolve all issues reported below. For more help with configuring your database server, see the installation handbook. If you are unsure what any of this means you should probably contact your hosting provider.

    ' . $message; - throw new DatabaseTaskException($message); - } - } - - /** - * Check if we can connect to the database. - */ - protected function connect() { - try { - // This doesn't actually test the connection. - db_set_active(); - // Now actually do a check. - Database::getConnection(); - $this->pass('Drupal can CONNECT to the database ok.'); - } - catch (Exception $e) { - $this->fail(st('Failed to connect to your database server. The server reports the following message: %error.
    • Is the database server running?
    • Does the database exist, and have you entered the correct database name?
    • Have you entered the correct username and password?
    • Have you entered the correct database hostname?
    ', array('%error' => $e->getMessage()))); - return FALSE; - } - return TRUE; - } - - /** - * Run SQL tests to ensure the database can execute commands with the current user. - */ - protected function runTestQuery($query, $pass, $fail, $fatal = FALSE) { - try { - db_query($query); - $this->pass(st($pass)); - } - catch (Exception $e) { - $this->fail(st($fail, array('%query' => $query, '%error' => $e->getMessage(), '%name' => $this->name()))); - return !$fatal; - } - } - - /** - * Check the engine version. - */ - protected function checkEngineVersion() { - if ($this->minimumVersion() && version_compare(Database::getConnection()->version(), $this->minimumVersion(), '<')) { - $this->fail(st("The database version %version is less than the minimum required version %minimum_version.", array('%version' => Database::getConnection()->version(), '%minimum_version' => $this->minimumVersion()))); - } - } - - /** - * Return driver specific configuration options. - * - * @param $database - * An array of driver specific configuration options. - * - * @return - * The options form array. - */ - public function getFormOptions($database) { - $form['database'] = array( - '#type' => 'textfield', - '#title' => st('Database name'), - '#default_value' => empty($database['database']) ? '' : $database['database'], - '#size' => 45, - '#required' => TRUE, - '#description' => st('The name of the database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('@drupal' => drupal_install_profile_distribution_name())), - ); - - $form['username'] = array( - '#type' => 'textfield', - '#title' => st('Database username'), - '#default_value' => empty($database['username']) ? '' : $database['username'], - '#required' => TRUE, - '#size' => 45, - ); - - $form['password'] = array( - '#type' => 'password', - '#title' => st('Database password'), - '#default_value' => empty($database['password']) ? '' : $database['password'], - '#required' => FALSE, - '#size' => 45, - ); - - $form['advanced_options'] = array( - '#type' => 'fieldset', - '#title' => st('Advanced options'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#description' => st("These options are only necessary for some sites. If you're not sure what you should enter here, leave the default settings or check with your hosting provider."), - '#weight' => 10, - ); - - $profile = drupal_get_profile(); - $db_prefix = ($profile == 'standard') ? 'drupal_' : $profile . '_'; - $form['advanced_options']['db_prefix'] = array( - '#type' => 'textfield', - '#title' => st('Table prefix'), - '#default_value' => '', - '#size' => 45, - '#description' => st('If more than one application will be sharing this database, enter a table prefix such as %prefix for your @drupal site here.', array('@drupal' => drupal_install_profile_distribution_name(), '%prefix' => $db_prefix)), - '#weight' => 10, - ); - - $form['advanced_options']['host'] = array( - '#type' => 'textfield', - '#title' => st('Database host'), - '#default_value' => empty($database['host']) ? 'localhost' : $database['host'], - '#size' => 45, - // Hostnames can be 255 characters long. - '#maxlength' => 255, - '#required' => TRUE, - '#description' => st('If your database is located on a different server, change this.'), - ); - - $form['advanced_options']['port'] = array( - '#type' => 'textfield', - '#title' => st('Database port'), - '#default_value' => empty($database['port']) ? '' : $database['port'], - '#size' => 45, - // The maximum port number is 65536, 5 digits. - '#maxlength' => 5, - '#description' => st('If your database server is listening to a non-standard port, enter its number.'), - ); - - return $form; - } - - /** - * Validates driver specific configuration settings. - * - * Checks to ensure correct basic database settings and that a proper - * connection to the database can be established. - * - * @param $database - * An array of driver specific configuration options. - * - * @return - * An array of driver configuration errors, keyed by form element name. - */ - public function validateDatabaseSettings($database) { - $errors = array(); - - // Verify the table prefix. - if (!empty($database['prefix']) && is_string($database['prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['prefix'])) { - $errors[$database['driver'] . '][advanced_options][db_prefix'] = st('The database table prefix you have entered, %prefix, is invalid. The table prefix can only contain alphanumeric characters, periods, or underscores.', array('%prefix' => $database['prefix'])); - } - - // Verify the database port. - if (!empty($database['port']) && !is_numeric($database['port'])) { - $errors[$database['driver'] . '][advanced_options][port'] = st('Database port must be a number.'); - } - - return $errors; - } - -} - -/** - * Exception thrown if the database installer fails. - */ -class DatabaseTaskException extends Exception { -} - -/** - * Replace values in settings.php with values in the submitted array. - * - * @param $settings - * An array of settings that need to be updated. - */ -function drupal_rewrite_settings($settings = array(), $prefix = '') { - $default_settings = 'sites/default/default.settings.php'; - drupal_static_reset('conf_path'); - $settings_file = conf_path(FALSE) . '/' . $prefix . 'settings.php'; - - // Build list of setting names and insert the values into the global namespace. - $keys = array(); - foreach ($settings as $setting => $data) { - $GLOBALS[$setting] = $data['value']; - $keys[] = $setting; - } - - $buffer = NULL; - $first = TRUE; - if ($fp = fopen(DRUPAL_ROOT . '/' . $default_settings, 'r')) { - // Step line by line through settings.php. - while (!feof($fp)) { - $line = fgets($fp); - if ($first && substr($line, 0, 5) != ' $data) { - if ($data['required']) { - $buffer .= "\$$setting = " . var_export($data['value'], TRUE) . ";\n"; - } - } - - $fp = fopen(DRUPAL_ROOT . '/' . $settings_file, 'w'); - if ($fp && fwrite($fp, $buffer) === FALSE) { - throw new Exception(st('Failed to modify %settings. Verify the file permissions.', array('%settings' => $settings_file))); - } - } - else { - throw new Exception(st('Failed to open %settings. Verify the file permissions.', array('%settings' => $default_settings))); - } -} - -/** - * Verify an install profile for installation. - * - * @param $install_state - * An array of information about the current installation state. - * @return - * The list of modules to install. - */ -function drupal_verify_profile($install_state) { - $profile = $install_state['parameters']['profile']; - $locale = $install_state['parameters']['locale']; - - include_once DRUPAL_ROOT . '/includes/file.inc'; - include_once DRUPAL_ROOT . '/includes/common.inc'; - - $profile_file = DRUPAL_ROOT . "/profiles/$profile/$profile.profile"; - - if (!isset($profile) || !file_exists($profile_file)) { - throw new Exception(install_no_profile_error()); - } - $info = $install_state['profile_info']; - - // Get a list of modules that exist in Drupal's assorted subdirectories. - $present_modules = array(); - foreach (drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0) as $present_module) { - $present_modules[] = $present_module->name; - } - - // The install profile is also a module, which needs to be installed after all the other dependencies - // have been installed. - $present_modules[] = drupal_get_profile(); - - // Verify that all of the profile's required modules are present. - $missing_modules = array_diff($info['dependencies'], $present_modules); - - $requirements = array(); - - if (count($missing_modules)) { - $modules = array(); - foreach ($missing_modules as $module) { - $modules[] = '' . drupal_ucfirst($module) . ''; - } - $requirements['required_modules'] = array( - 'title' => st('Required modules'), - 'value' => st('Required modules not found.'), - 'severity' => REQUIREMENT_ERROR, - 'description' => st('The following modules are required but were not found. Move them into the appropriate modules subdirectory, such as sites/all/modules. Missing modules: !modules', array('!modules' => implode(', ', $modules))), - ); - } - return $requirements; -} - -/** - * Callback to install the system module. - * - * Separated from the installation of other modules so core system - * functions can be made available while other modules are installed. - */ -function drupal_install_system() { - $system_path = drupal_get_path('module', 'system'); - require_once DRUPAL_ROOT . '/' . $system_path . '/system.install'; - module_invoke('system', 'install'); - - $system_versions = drupal_get_schema_versions('system'); - $system_version = $system_versions ? max($system_versions) : SCHEMA_INSTALLED; - db_insert('system') - ->fields(array('filename', 'name', 'type', 'owner', 'status', 'schema_version', 'bootstrap')) - ->values(array( - 'filename' => $system_path . '/system.module', - 'name' => 'system', - 'type' => 'module', - 'owner' => '', - 'status' => 1, - 'schema_version' => $system_version, - 'bootstrap' => 0, - )) - ->execute(); - system_rebuild_module_data(); -} - -/** - * Uninstalls a given list of modules. - * - * @param $module_list - * The modules to uninstall. - * @param $uninstall_dependents - * If TRUE, the function will check that all modules which depend on the - * passed-in module list either are already uninstalled or contained in the - * list, and it will ensure that the modules are uninstalled in the correct - * order. This incurs a significant performance cost, so use FALSE if you - * know $module_list is already complete and in the correct order. - * - * @return - * FALSE if one or more dependent modules are missing from the list, TRUE - * otherwise. - */ -function drupal_uninstall_modules($module_list = array(), $uninstall_dependents = TRUE) { - if ($uninstall_dependents) { - // Get all module data so we can find dependents and sort. - $module_data = system_rebuild_module_data(); - // Create an associative array with weights as values. - $module_list = array_flip(array_values($module_list)); - - $profile = drupal_get_profile(); - while (list($module) = each($module_list)) { - if (!isset($module_data[$module]) || drupal_get_installed_schema_version($module) == SCHEMA_UNINSTALLED) { - // This module doesn't exist or is already uninstalled, skip it. - unset($module_list[$module]); - continue; - } - $module_list[$module] = $module_data[$module]->sort; - - // If the module has any dependents which are not already uninstalled and - // not included in the passed-in list, abort. It is not safe to uninstall - // them automatically because uninstalling a module is a destructive - // operation. - foreach (array_keys($module_data[$module]->required_by) as $dependent) { - if (!isset($module_list[$dependent]) && drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED && $dependent != $profile) { - return FALSE; - } - } - } - - // Sort the module list by pre-calculated weights. - asort($module_list); - $module_list = array_keys($module_list); - } - - foreach ($module_list as $module) { - // Uninstall the module. - module_load_install($module); - module_invoke($module, 'uninstall'); - drupal_uninstall_schema($module); - - watchdog('system', '%module module uninstalled.', array('%module' => $module), LOG_INFO); - drupal_set_installed_schema_version($module, SCHEMA_UNINSTALLED); - } - - if (!empty($module_list)) { - // Call hook_module_uninstall to let other modules act - module_invoke_all('modules_uninstalled', $module_list); - } - - return TRUE; -} - -/** - * Verify the state of the specified file. - * - * @param $file - * The file to check for. - * @param $mask - * An optional bitmask created from various FILE_* constants. - * @param $type - * The type of file. Can be file (default), dir, or link. - * @return - * TRUE on success or FALSE on failure. A message is set for the latter. - */ -function drupal_verify_install_file($file, $mask = NULL, $type = 'file') { - $return = TRUE; - // Check for files that shouldn't be there. - if (isset($mask) && ($mask & FILE_NOT_EXIST) && file_exists($file)) { - return FALSE; - } - // Verify that the file is the type of file it is supposed to be. - if (isset($type) && file_exists($file)) { - $check = 'is_' . $type; - if (!function_exists($check) || !$check($file)) { - $return = FALSE; - } - } - - // Verify file permissions. - if (isset($mask)) { - $masks = array(FILE_EXIST, FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE); - foreach ($masks as $current_mask) { - if ($mask & $current_mask) { - switch ($current_mask) { - case FILE_EXIST: - if (!file_exists($file)) { - if ($type == 'dir') { - drupal_install_mkdir($file, $mask); - } - if (!file_exists($file)) { - $return = FALSE; - } - } - break; - case FILE_READABLE: - if (!is_readable($file) && !drupal_install_fix_file($file, $mask)) { - $return = FALSE; - } - break; - case FILE_WRITABLE: - if (!is_writable($file) && !drupal_install_fix_file($file, $mask)) { - $return = FALSE; - } - break; - case FILE_EXECUTABLE: - if (!is_executable($file) && !drupal_install_fix_file($file, $mask)) { - $return = FALSE; - } - break; - case FILE_NOT_READABLE: - if (is_readable($file) && !drupal_install_fix_file($file, $mask)) { - $return = FALSE; - } - break; - case FILE_NOT_WRITABLE: - if (is_writable($file) && !drupal_install_fix_file($file, $mask)) { - $return = FALSE; - } - break; - case FILE_NOT_EXECUTABLE: - if (is_executable($file) && !drupal_install_fix_file($file, $mask)) { - $return = FALSE; - } - break; - } - } - } - } - return $return; -} - -/** - * Create a directory with specified permissions. - * - * @param $file - * The name of the directory to create; - * @param $mask - * The permissions of the directory to create. - * @param $message - * (optional) Whether to output messages. Defaults to TRUE. - * @return - * TRUE/FALSE whether or not the directory was successfully created. - */ -function drupal_install_mkdir($file, $mask, $message = TRUE) { - $mod = 0; - $masks = array(FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE); - foreach ($masks as $m) { - if ($mask & $m) { - switch ($m) { - case FILE_READABLE: - $mod |= 0444; - break; - case FILE_WRITABLE: - $mod |= 0222; - break; - case FILE_EXECUTABLE: - $mod |= 0111; - break; - } - } - } - - if (@drupal_mkdir($file, $mod)) { - return TRUE; - } - else { - return FALSE; - } -} - -/** - * Attempt to fix file permissions. - * - * The general approach here is that, because we do not know the security - * setup of the webserver, we apply our permission changes to all three - * digits of the file permission (i.e. user, group and all). - * - * To ensure that the values behave as expected (and numbers don't carry - * from one digit to the next) we do the calculation on the octal value - * using bitwise operations. This lets us remove, for example, 0222 from - * 0700 and get the correct value of 0500. - * - * @param $file - * The name of the file with permissions to fix. - * @param $mask - * The desired permissions for the file. - * @param $message - * (optional) Whether to output messages. Defaults to TRUE. - * @return - * TRUE/FALSE whether or not we were able to fix the file's permissions. - */ -function drupal_install_fix_file($file, $mask, $message = TRUE) { - // If $file does not exist, fileperms() issues a PHP warning. - if (!file_exists($file)) { - return FALSE; - } - - $mod = fileperms($file) & 0777; - $masks = array(FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE); - - // FILE_READABLE, FILE_WRITABLE, and FILE_EXECUTABLE permission strings - // can theoretically be 0400, 0200, and 0100 respectively, but to be safe - // we set all three access types in case the administrator intends to - // change the owner of settings.php after installation. - foreach ($masks as $m) { - if ($mask & $m) { - switch ($m) { - case FILE_READABLE: - if (!is_readable($file)) { - $mod |= 0444; - } - break; - case FILE_WRITABLE: - if (!is_writable($file)) { - $mod |= 0222; - } - break; - case FILE_EXECUTABLE: - if (!is_executable($file)) { - $mod |= 0111; - } - break; - case FILE_NOT_READABLE: - if (is_readable($file)) { - $mod &= ~0444; - } - break; - case FILE_NOT_WRITABLE: - if (is_writable($file)) { - $mod &= ~0222; - } - break; - case FILE_NOT_EXECUTABLE: - if (is_executable($file)) { - $mod &= ~0111; - } - break; - } - } - } - - // chmod() will work if the web server is running as owner of the file. - // If PHP safe_mode is enabled the currently executing script must also - // have the same owner. - if (@chmod($file, $mod)) { - return TRUE; - } - else { - return FALSE; - } -} - - -/** - * Send the user to a different installer page. - * - * This issues an on-site HTTP redirect. Messages (and errors) are erased. - * - * @param $path - * An installer path. - */ -function install_goto($path) { - global $base_url; - include_once DRUPAL_ROOT . '/includes/common.inc'; - header('Location: ' . $base_url . '/' . $path); - header('Cache-Control: no-cache'); // Not a permanent redirect. - drupal_exit(); -} - -/** - * Functional equivalent of t(), used when some systems are not available. - * - * Used during the install process, when database, theme, and localization - * system is possibly not yet available. - * - * 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 get_t() - * @ingroup sanitization - */ -function st($string, array $args = array(), array $options = array()) { - static $locale_strings = NULL; - global $install_state; - - if (empty($options['context'])) { - $options['context'] = ''; - } - - if (!isset($locale_strings)) { - $locale_strings = array(); - if (isset($install_state['parameters']['profile']) && isset($install_state['parameters']['locale'])) { - // If the given locale was selected, there should be at least one .po file - // with its name ending in {$install_state['parameters']['locale']}.po - // This might or might not be the entire filename. It is also possible - // that multiple files end with the same extension, even if unlikely. - $po_files = file_scan_directory('./profiles/' . $install_state['parameters']['profile'] . '/translations', '/'. $install_state['parameters']['locale'] .'\.po$/', array('recurse' => FALSE)); - if (count($po_files)) { - require_once DRUPAL_ROOT . '/includes/locale.inc'; - foreach ($po_files as $po_file) { - _locale_import_read_po('mem-store', $po_file); - } - $locale_strings = _locale_import_one_string('mem-report'); - } - } - } - - require_once DRUPAL_ROOT . '/includes/theme.inc'; - // Transform arguments before inserting them - foreach ($args as $key => $value) { - switch ($key[0]) { - // Escaped only - case '@': - $args[$key] = check_plain($value); - break; - // Escaped and placeholder - case '%': - default: - $args[$key] = '' . check_plain($value) . ''; - break; - // Pass-through - case '!': - } - } - return strtr((!empty($locale_strings[$options['context']][$string]) ? $locale_strings[$options['context']][$string] : $string), $args); -} - -/** - * Check an install profile's requirements. - * - * @param $profile - * Name of install profile to check. - * @return - * Array of the install profile's requirements. - */ -function drupal_check_profile($profile) { - include_once DRUPAL_ROOT . '/includes/file.inc'; - - $profile_file = DRUPAL_ROOT . "/profiles/$profile/$profile.profile"; - - if (!isset($profile) || !file_exists($profile_file)) { - throw new Exception(install_no_profile_error()); - } - - $info = install_profile_info($profile); - - // Collect requirement testing results. - $requirements = array(); - foreach ($info['dependencies'] as $module) { - module_load_install($module); - $function = $module . '_requirements'; - if (function_exists($function)) { - $requirements = array_merge($requirements, $function('install')); - } - } - return $requirements; -} - -/** - * Extract highest severity from requirements array. - * - * @param $requirements - * An array of requirements, in the same format as is returned by - * hook_requirements(). - * @return - * The highest severity in the array. - */ -function drupal_requirements_severity(&$requirements) { - $severity = REQUIREMENT_OK; - foreach ($requirements as $requirement) { - if (isset($requirement['severity'])) { - $severity = max($severity, $requirement['severity']); - } - } - return $severity; -} - -/** - * Check a module's requirements. - * - * @param $module - * Machine name of module to check. - * @return - * TRUE/FALSE depending on the requirements are in place. - */ -function drupal_check_module($module) { - module_load_install($module); - if (module_hook($module, 'requirements')) { - // Check requirements - $requirements = module_invoke($module, 'requirements', 'install'); - if (is_array($requirements) && drupal_requirements_severity($requirements) == REQUIREMENT_ERROR) { - // Print any error messages - foreach ($requirements as $requirement) { - if (isset($requirement['severity']) && $requirement['severity'] == REQUIREMENT_ERROR) { - $message = $requirement['description']; - if (isset($requirement['value']) && $requirement['value']) { - $message .= ' (' . t('Currently using !item !version', array('!item' => $requirement['title'], '!version' => $requirement['value'])) . ')'; - } - drupal_set_message($message, 'error'); - } - } - return FALSE; - } - } - return TRUE; -} - -/** - * Retrieve info about an install profile from its .info file. - * - * The information stored in a profile .info file is similar to that stored in - * a normal Drupal module .info file. For example: - * - name: The real name of the install profile for display purposes. - * - description: A brief description of the profile. - * - dependencies: An array of shortnames of other modules this install profile requires. - * - * Additional, less commonly-used information that can appear in a profile.info - * file but not in a normal Drupal module .info file includes: - * - distribution_name: The name of the Drupal distribution that is being - * installed, to be shown throughout the installation process. Defaults to - * 'Drupal'. - * - * Note that this function does an expensive file system scan to get info file - * information for dependencies. If you only need information from the info - * file itself, use system_get_info(). - * - * Example of .info file: - * @code - * name = Minimal - * description = Start fresh, with only a few modules enabled. - * dependencies[] = block - * dependencies[] = dblog - * @endcode - * - * @param profile - * Name of profile. - * @param locale - * Name of locale used (if any). - * @return - * The info array. - */ -function install_profile_info($profile, $locale = 'en') { - $cache = &drupal_static(__FUNCTION__, array()); - - if (!isset($cache[$profile])) { - // Set defaults for module info. - $defaults = array( - 'dependencies' => array(), - 'description' => '', - 'distribution_name' => 'Drupal', - 'version' => NULL, - 'hidden' => FALSE, - 'php' => DRUPAL_MINIMUM_PHP, - ); - $info = drupal_parse_info_file("profiles/$profile/$profile.info") + $defaults; - $info['dependencies'] = array_unique(array_merge( - drupal_required_modules(), - $info['dependencies'], - ($locale != 'en' && !empty($locale) ? array('locale') : array())) - ); - - // drupal_required_modules() includes the current profile as a dependency. - // Since a module can't depend on itself we remove that element of the array. - array_shift($info['dependencies']); - - $cache[$profile] = $info; - } - return $cache[$profile]; -} - -/** - * Ensures the environment for a Drupal database on a predefined connection. - * - * This will run tasks that check that Drupal can perform all of the functions - * on a database, that Drupal needs. Tasks include simple checks like CREATE - * TABLE to database specific functions like stored procedures and client - * encoding. - */ -function db_run_tasks($driver) { - db_installer_object($driver)->runTasks(); - return TRUE; -} - -/** - * Returns a database installer object. - * - * @param $driver - * The name of the driver. - */ -function db_installer_object($driver) { - Database::loadDriverFile($driver, array('install.inc')); - $task_class = 'DatabaseTasks_' . $driver; - return new $task_class(); -} diff --git a/includes/locale.inc b/includes/locale.inc deleted file mode 100644 index 3fb4707..0000000 --- a/includes/locale.inc +++ /dev/null @@ -1,2293 +0,0 @@ -language) ? $language->language : FALSE; -} - -/** - * Identify language from the Accept-language HTTP header we got. - * - * We perform browser accept-language parsing only if page cache is disabled, - * otherwise we would cache a user-specific preference. - * - * @param $languages - * An array of valid language objects. - * - * @return - * A valid language code on success, FALSE otherwise. - */ -function locale_language_from_browser($languages) { - // Specified by the user via the browser's Accept Language setting - // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5" - $browser_langs = array(); - - if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { - $browser_accept = explode(",", $_SERVER['HTTP_ACCEPT_LANGUAGE']); - foreach ($browser_accept as $langpart) { - // The language part is either a code or a code with a quality. - // We cannot do anything with a * code, so it is skipped. - // If the quality is missing, it is assumed to be 1 according to the RFC. - if (preg_match("!([a-z-]+)(;q=([0-9\\.]+))?!", trim($langpart), $found)) { - $browser_langs[$found[1]] = (isset($found[3]) ? (float) $found[3] : 1.0); - } - } - } - - // Order the codes by quality - arsort($browser_langs); - - // Try to find the first preferred language we have - foreach ($browser_langs as $langcode => $q) { - if (isset($languages[$langcode])) { - return $langcode; - } - } - - return FALSE; -} - -/** - * Identify language from the user preferences. - * - * @param $languages - * An array of valid language objects. - * - * @return - * A valid language code on success, FALSE otherwise. - */ -function locale_language_from_user($languages) { - // User preference (only for logged users). - global $user; - - if ($user->uid) { - return $user->language; - } - - // No language preference from the user. - return FALSE; -} - -/** - * Identify language from a request/session parameter. - * - * @param $languages - * An array of valid language objects. - * - * @return - * A valid language code on success, FALSE otherwise. - */ -function locale_language_from_session($languages) { - $param = variable_get('locale_language_negotiation_session_param', 'language'); - - // Request parameter: we need to update the session parameter only if we have - // an authenticated user. - if (isset($_GET[$param]) && isset($languages[$langcode = $_GET[$param]])) { - global $user; - if ($user->uid) { - $_SESSION[$param] = $langcode; - } - return $langcode; - } - - // Session parameter. - if (isset($_SESSION[$param])) { - return $_SESSION[$param]; - } - - return FALSE; -} - -/** - * Identify language via URL prefix or domain. - * - * @param $languages - * An array of valid language objects. - * - * @return - * A valid language code on success, FALSE otherwise. - */ -function locale_language_from_url($languages) { - $language_url = FALSE; - - if (!language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_URL)) { - return $language_url; - } - - switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) { - case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX: - // $_GET['q'] might not be available at this time, because - // path initialization runs after the language bootstrap phase. - list($language, $_GET['q']) = language_url_split_prefix(isset($_GET['q']) ? $_GET['q'] : NULL, $languages); - if ($language !== FALSE) { - $language_url = $language->language; - } - break; - - case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN: - foreach ($languages as $language) { - $host = parse_url($language->domain, PHP_URL_HOST); - if ($host && ($_SERVER['HTTP_HOST'] == $host)) { - $language_url = $language->language; - break; - } - } - break; - } - - return $language_url; -} - -/** - * Determines the language to be assigned to URLs when none is detected. - * - * The language negotiation process has a fallback chain that ends with the - * default language provider. Each built-in language type has a separate - * initialization: - * - Interface language, which is the only configurable one, always gets a valid - * value. If no request-specific language is detected, the default language - * will be used. - * - Content language merely inherits the interface language by default. - * - URL language is detected from the requested URL and will be used to rewrite - * URLs appearing in the page being rendered. If no language can be detected, - * there are two possibilities: - * - If the default language has no configured path prefix or domain, then the - * default language is used. This guarantees that (missing) URL prefixes are - * preserved when navigating through the site. - * - If the default language has a configured path prefix or domain, a - * requested URL having an empty prefix or domain is an anomaly that must be - * fixed. This is done by introducing a prefix or domain in the rendered - * page matching the detected interface language. - * - * @param $languages - * (optional) An array of valid language objects. This is passed by - * language_provider_invoke() to every language provider callback, but it is - * not actually needed here. Defaults to NULL. - * @param $language_type - * (optional) The language type to fall back to. Defaults to the interface - * language. - * - * @return - * A valid language code. - */ -function locale_language_url_fallback($language = NULL, $language_type = LANGUAGE_TYPE_INTERFACE) { - $default = language_default(); - $prefix = (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX) == LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX); - - // If the default language is not configured to convey language information, - // a missing URL language information indicates that URL language should be - // the default one, otherwise we fall back to an already detected language. - if (($prefix && empty($default->prefix)) || (!$prefix && empty($default->domain))) { - return $default->language; - } - else { - return $GLOBALS[$language_type]->language; - } -} - -/** - * Return the URL language switcher block. Translation links may be provided by - * other modules. - */ -function locale_language_switcher_url($type, $path) { - $languages = language_list('enabled'); - $links = array(); - - foreach ($languages[1] as $language) { - $links[$language->language] = array( - 'href' => $path, - 'title' => $language->native, - 'language' => $language, - 'attributes' => array('class' => array('language-link')), - ); - } - - return $links; -} - -/** - * Return the session language switcher block. - */ -function locale_language_switcher_session($type, $path) { - drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css'); - - $param = variable_get('locale_language_negotiation_session_param', 'language'); - $language_query = isset($_SESSION[$param]) ? $_SESSION[$param] : $GLOBALS[$type]->language; - - $languages = language_list('enabled'); - $links = array(); - - $query = $_GET; - unset($query['q']); - - foreach ($languages[1] as $language) { - $langcode = $language->language; - $links[$langcode] = array( - 'href' => $path, - 'title' => $language->native, - 'attributes' => array('class' => array('language-link')), - 'query' => $query, - ); - if ($language_query != $langcode) { - $links[$langcode]['query'][$param] = $langcode; - } - else { - $links[$langcode]['attributes']['class'][] = ' session-active'; - } - } - - return $links; -} - -/** - * Rewrite URLs for the URL language provider. - */ -function locale_language_url_rewrite_url(&$path, &$options) { - static $drupal_static_fast; - if (!isset($drupal_static_fast)) { - $drupal_static_fast['languages'] = &drupal_static(__FUNCTION__); - } - $languages = &$drupal_static_fast['languages']; - - if (!isset($languages)) { - $languages = language_list('enabled'); - $languages = array_flip(array_keys($languages[1])); - } - - // Language can be passed as an option, or we go for current URL language. - if (!isset($options['language'])) { - global $language_url; - $options['language'] = $language_url; - } - // We allow only enabled languages here. - elseif (!isset($languages[$options['language']->language])) { - unset($options['language']); - return; - } - - if (isset($options['language'])) { - switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) { - case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN: - if ($options['language']->domain) { - // Ask for an absolute URL with our modified base_url. - $options['absolute'] = TRUE; - $options['base_url'] = $options['language']->domain; - } - break; - - case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX: - if (!empty($options['language']->prefix)) { - $options['prefix'] = $options['language']->prefix . '/'; - } - break; - } - } -} - -/** - * Rewrite URLs for the Session language provider. - */ -function locale_language_url_rewrite_session(&$path, &$options) { - static $query_rewrite, $query_param, $query_value; - - // The following values are not supposed to change during a single page - // request processing. - if (!isset($query_rewrite)) { - global $user; - if (!$user->uid) { - $languages = language_list('enabled'); - $languages = $languages[1]; - $query_param = check_plain(variable_get('locale_language_negotiation_session_param', 'language')); - $query_value = isset($_GET[$query_param]) ? check_plain($_GET[$query_param]) : NULL; - $query_rewrite = isset($languages[$query_value]) && language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_SESSION); - } - else { - $query_rewrite = FALSE; - } - } - - // If the user is anonymous, the user language provider is enabled, and the - // corresponding option has been set, we must preserve any explicit user - // language preference even with cookies disabled. - if ($query_rewrite) { - if (is_string($options['query'])) { - $options['query'] = drupal_get_query_array($options['query']); - } - if (!isset($options['query'][$query_param])) { - $options['query'][$query_param] = $query_value; - } - } -} - -/** - * @} End of "locale-languages-negotiation" - */ - -/** - * Check that a string is safe to be added or imported as a translation. - * - * This test can be used to detect possibly bad translation strings. It should - * not have any false positives. But it is only a test, not a transformation, - * as it destroys valid HTML. We cannot reliably filter translation strings - * on import because some strings are irreversibly corrupted. For example, - * a & in the translation would get encoded to &amp; by filter_xss() - * before being put in the database, and thus would be displayed incorrectly. - * - * The allowed tag list is like filter_xss_admin(), but omitting div and img as - * not needed for translation and likely to cause layout issues (div) or a - * possible attack vector (img). - */ -function locale_string_is_safe($string) { - return decode_entities($string) == decode_entities(filter_xss($string, array('a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'ins', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'ul', 'var'))); -} - -/** - * @defgroup locale-api-add Language addition API - * @{ - * Add a language. - * - * The language addition API is used to create languages and store them. - */ - -/** - * API function to add a language. - * - * @param $langcode - * Language code. - * @param $name - * English name of the language - * @param $native - * Native name of the language - * @param $direction - * LANGUAGE_LTR or LANGUAGE_RTL - * @param $domain - * Optional custom domain name with protocol, without - * trailing slash (eg. http://de.example.com). - * @param $prefix - * Optional path prefix for the language. Defaults to the - * language code if omitted. - * @param $enabled - * Optionally TRUE to enable the language when created or FALSE to disable. - * @param $default - * Optionally set this language to be the default. - */ -function locale_add_language($langcode, $name = NULL, $native = NULL, $direction = LANGUAGE_LTR, $domain = '', $prefix = '', $enabled = TRUE, $default = FALSE) { - // Default prefix on language code. - if (empty($prefix)) { - $prefix = $langcode; - } - - // If name was not set, we add a predefined language. - if (!isset($name)) { - include_once DRUPAL_ROOT . '/includes/iso.inc'; - $predefined = _locale_get_predefined_list(); - $name = $predefined[$langcode][0]; - $native = isset($predefined[$langcode][1]) ? $predefined[$langcode][1] : $predefined[$langcode][0]; - $direction = isset($predefined[$langcode][2]) ? $predefined[$langcode][2] : LANGUAGE_LTR; - } - - db_insert('languages') - ->fields(array( - 'language' => $langcode, - 'name' => $name, - 'native' => $native, - 'direction' => $direction, - 'domain' => $domain, - 'prefix' => $prefix, - 'enabled' => $enabled, - )) - ->execute(); - - // Only set it as default if enabled. - if ($enabled && $default) { - variable_set('language_default', (object) array('language' => $langcode, 'name' => $name, 'native' => $native, 'direction' => $direction, 'enabled' => (int) $enabled, 'plurals' => 0, 'formula' => '', 'domain' => '', 'prefix' => $prefix, 'weight' => 0, 'javascript' => '')); - } - - if ($enabled) { - // Increment enabled language count if we are adding an enabled language. - variable_set('language_count', variable_get('language_count', 1) + 1); - } - - // Kill the static cache in language_list(). - drupal_static_reset('language_list'); - - // Force JavaScript translation file creation for the newly added language. - _locale_invalidate_js($langcode); - - watchdog('locale', 'The %language language (%code) has been created.', array('%language' => $name, '%code' => $langcode)); - - module_invoke_all('multilingual_settings_changed'); -} -/** - * @} End of "locale-api-add" - */ - -/** - * @defgroup locale-api-import-export Translation import/export API. - * @{ - * Functions to import and export translations. - * - * These functions provide the ability to import translations from - * external files and to export translations and translation templates. - */ - -/** - * Parses Gettext Portable Object file information and inserts into database - * - * @param $file - * Drupal file object corresponding to the PO file to import. - * @param $langcode - * Language code. - * @param $mode - * Should existing translations be replaced LOCALE_IMPORT_KEEP or - * LOCALE_IMPORT_OVERWRITE. - * @param $group - * Text group to import PO file into (eg. 'default' for interface - * translations). - */ -function _locale_import_po($file, $langcode, $mode, $group = NULL) { - // Try to allocate enough time to parse and import the data. - drupal_set_time_limit(240); - - // Check if we have the language already in the database. - if (!db_query("SELECT COUNT(language) FROM {languages} WHERE language = :language", array(':language' => $langcode))->fetchField()) { - drupal_set_message(t('The language selected for import is not supported.'), 'error'); - return FALSE; - } - - // Get strings from file (returns on failure after a partial import, or on success) - $status = _locale_import_read_po('db-store', $file, $mode, $langcode, $group); - if ($status === FALSE) { - // Error messages are set in _locale_import_read_po(). - return FALSE; - } - - // Get status information on import process. - list($header_done, $additions, $updates, $deletes, $skips) = _locale_import_one_string('db-report'); - - if (!$header_done) { - drupal_set_message(t('The translation file %filename appears to have a missing or malformed header.', array('%filename' => $file->filename)), 'error'); - } - - // Clear cache and force refresh of JavaScript translations. - _locale_invalidate_js($langcode); - cache_clear_all('locale:', 'cache', TRUE); - - // Rebuild the menu, strings may have changed. - menu_rebuild(); - - drupal_set_message(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes))); - watchdog('locale', 'Imported %file into %locale: %number new strings added, %update updated and %delete removed.', array('%file' => $file->filename, '%locale' => $langcode, '%number' => $additions, '%update' => $updates, '%delete' => $deletes)); - if ($skips) { - $skip_message = format_plural($skips, 'One translation string was skipped because it contains disallowed HTML.', '@count translation strings were skipped because they contain disallowed HTML.'); - drupal_set_message($skip_message); - watchdog('locale', '@count disallowed HTML string(s) in %file', array('@count' => $skips, '%file' => $file->uri), LOG_WARNING); - } - return TRUE; -} - -/** - * Parses Gettext Portable Object file into an array - * - * @param $op - * Storage operation type: db-store or mem-store. - * @param $file - * Drupal file object corresponding to the PO file to import. - * @param $mode - * Should existing translations be replaced LOCALE_IMPORT_KEEP or - * LOCALE_IMPORT_OVERWRITE. - * @param $lang - * Language code. - * @param $group - * Text group to import PO file into (eg. 'default' for interface - * translations). - */ -function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL, $group = 'default') { - - // The file will get closed by PHP on returning from this function. - $fd = fopen($file->uri, 'rb'); - if (!$fd) { - _locale_import_message('The translation import failed, because the file %filename could not be read.', $file); - return FALSE; - } - - /* - * The parser context. Can be: - * - 'COMMENT' (#) - * - 'MSGID' (msgid) - * - 'MSGID_PLURAL' (msgid_plural) - * - 'MSGCTXT' (msgctxt) - * - 'MSGSTR' (msgstr or msgstr[]) - * - 'MSGSTR_ARR' (msgstr_arg) - */ - $context = 'COMMENT'; - - // Current entry being read. - $current = array(); - - // Current plurality for 'msgstr[]'. - $plural = 0; - - // Current line. - $lineno = 0; - - while (!feof($fd)) { - // A line should not be longer than 10 * 1024. - $line = fgets($fd, 10 * 1024); - - if ($lineno == 0) { - // The first line might come with a UTF-8 BOM, which should be removed. - $line = str_replace("\xEF\xBB\xBF", '', $line); - } - - $lineno++; - - // Trim away the linefeed. - $line = trim(strtr($line, array("\\\n" => ""))); - - if (!strncmp('#', $line, 1)) { - // Lines starting with '#' are comments. - - if ($context == 'COMMENT') { - // Already in comment token, insert the comment. - $current['#'][] = substr($line, 1); - } - elseif (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { - // We are currently in string token, close it out. - _locale_import_one_string($op, $current, $mode, $lang, $file, $group); - - // Start a new entry for the comment. - $current = array(); - $current['#'][] = substr($line, 1); - - $context = 'COMMENT'; - } - else { - // A comment following any other token is a syntax error. - _locale_import_message('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', $file, $lineno); - return FALSE; - } - } - elseif (!strncmp('msgid_plural', $line, 12)) { - // A plural form for the current message. - - if ($context != 'MSGID') { - // A plural form cannot be added to anything else but the id directly. - _locale_import_message('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', $file, $lineno); - return FALSE; - } - - // Remove 'msgid_plural' and trim away whitespace. - $line = trim(substr($line, 12)); - // At this point, $line should now contain only the plural form. - - $quoted = _locale_import_parse_quoted($line); - if ($quoted === FALSE) { - // The plural form must be wrapped in quotes. - _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); - return FALSE; - } - - // Append the plural form to the current entry. - $current['msgid'] .= "\0" . $quoted; - - $context = 'MSGID_PLURAL'; - } - elseif (!strncmp('msgid', $line, 5)) { - // Starting a new message. - - if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { - // We are currently in a message string, close it out. - _locale_import_one_string($op, $current, $mode, $lang, $file, $group); - - // Start a new context for the id. - $current = array(); - } - elseif ($context == 'MSGID') { - // We are currently already in the context, meaning we passed an id with no data. - _locale_import_message('The translation file %filename contains an error: "msgid" is unexpected on line %line.', $file, $lineno); - return FALSE; - } - - // Remove 'msgid' and trim away whitespace. - $line = trim(substr($line, 5)); - // At this point, $line should now contain only the message id. - - $quoted = _locale_import_parse_quoted($line); - if ($quoted === FALSE) { - // The message id must be wrapped in quotes. - _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); - return FALSE; - } - - $current['msgid'] = $quoted; - $context = 'MSGID'; - } - elseif (!strncmp('msgctxt', $line, 7)) { - // Starting a new context. - - if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { - // We are currently in a message, start a new one. - _locale_import_one_string($op, $current, $mode, $lang, $file, $group); - $current = array(); - } - elseif (!empty($current['msgctxt'])) { - // A context cannot apply to another context. - _locale_import_message('The translation file %filename contains an error: "msgctxt" is unexpected on line %line.', $file, $lineno); - return FALSE; - } - - // Remove 'msgctxt' and trim away whitespaces. - $line = trim(substr($line, 7)); - // At this point, $line should now contain the context. - - $quoted = _locale_import_parse_quoted($line); - if ($quoted === FALSE) { - // The context string must be quoted. - _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); - return FALSE; - } - - $current['msgctxt'] = $quoted; - - $context = 'MSGCTXT'; - } - elseif (!strncmp('msgstr[', $line, 7)) { - // A message string for a specific plurality. - - if (($context != 'MSGID') && ($context != 'MSGCTXT') && ($context != 'MSGID_PLURAL') && ($context != 'MSGSTR_ARR')) { - // Message strings must come after msgid, msgxtxt, msgid_plural, or other msgstr[] entries. - _locale_import_message('The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.', $file, $lineno); - return FALSE; - } - - // Ensure the plurality is terminated. - if (strpos($line, ']') === FALSE) { - _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); - return FALSE; - } - - // Extract the plurality. - $frombracket = strstr($line, '['); - $plural = substr($frombracket, 1, strpos($frombracket, ']') - 1); - - // Skip to the next whitespace and trim away any further whitespace, bringing $line to the message data. - $line = trim(strstr($line, " ")); - - $quoted = _locale_import_parse_quoted($line); - if ($quoted === FALSE) { - // The string must be quoted. - _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); - return FALSE; - } - - $current['msgstr'][$plural] = $quoted; - - $context = 'MSGSTR_ARR'; - } - elseif (!strncmp("msgstr", $line, 6)) { - // A string for the an id or context. - - if (($context != 'MSGID') && ($context != 'MSGCTXT')) { - // Strings are only valid within an id or context scope. - _locale_import_message('The translation file %filename contains an error: "msgstr" is unexpected on line %line.', $file, $lineno); - return FALSE; - } - - // Remove 'msgstr' and trim away away whitespaces. - $line = trim(substr($line, 6)); - // At this point, $line should now contain the message. - - $quoted = _locale_import_parse_quoted($line); - if ($quoted === FALSE) { - // The string must be quoted. - _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); - return FALSE; - } - - $current['msgstr'] = $quoted; - - $context = 'MSGSTR'; - } - elseif ($line != '') { - // Anything that is not a token may be a continuation of a previous token. - - $quoted = _locale_import_parse_quoted($line); - if ($quoted === FALSE) { - // The string must be quoted. - _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); - return FALSE; - } - - // Append the string to the current context. - if (($context == 'MSGID') || ($context == 'MSGID_PLURAL')) { - $current['msgid'] .= $quoted; - } - elseif ($context == 'MSGCTXT') { - $current['msgctxt'] .= $quoted; - } - elseif ($context == 'MSGSTR') { - $current['msgstr'] .= $quoted; - } - elseif ($context == 'MSGSTR_ARR') { - $current['msgstr'][$plural] .= $quoted; - } - else { - // No valid context to append to. - _locale_import_message('The translation file %filename contains an error: there is an unexpected string on line %line.', $file, $lineno); - return FALSE; - } - } - } - - // End of PO file, closed out the last entry. - if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { - _locale_import_one_string($op, $current, $mode, $lang, $file, $group); - } - elseif ($context != 'COMMENT') { - _locale_import_message('The translation file %filename ended unexpectedly at line %line.', $file, $lineno); - return FALSE; - } -} - -/** - * Sets an error message occurred during locale file parsing. - * - * @param $message - * The message to be translated. - * @param $file - * Drupal file object corresponding to the PO file to import. - * @param $lineno - * An optional line number argument. - */ -function _locale_import_message($message, $file, $lineno = NULL) { - $vars = array('%filename' => $file->filename); - if (isset($lineno)) { - $vars['%line'] = $lineno; - } - $t = get_t(); - drupal_set_message($t($message, $vars), 'error'); -} - -/** - * Imports a string into the database - * - * @param $op - * Operation to perform: 'db-store', 'db-report', 'mem-store' or 'mem-report'. - * @param $value - * Details of the string stored. - * @param $mode - * Should existing translations be replaced LOCALE_IMPORT_KEEP or - * LOCALE_IMPORT_OVERWRITE. - * @param $lang - * Language to store the string in. - * @param $file - * Object representation of file being imported, only required when op is - * 'db-store'. - * @param $group - * Text group to import PO file into (eg. 'default' for interface - * translations). - */ -function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NULL, $file = NULL, $group = 'default') { - $report = &drupal_static(__FUNCTION__, array('additions' => 0, 'updates' => 0, 'deletes' => 0, 'skips' => 0)); - $header_done = &drupal_static(__FUNCTION__ . ':header_done', FALSE); - $strings = &drupal_static(__FUNCTION__ . ':strings', array()); - - switch ($op) { - // Return stored strings - case 'mem-report': - return $strings; - - // Store string in memory (only supports single strings) - case 'mem-store': - $strings[isset($value['msgctxt']) ? $value['msgctxt'] : ''][$value['msgid']] = $value['msgstr']; - return; - - // Called at end of import to inform the user - case 'db-report': - return array($header_done, $report['additions'], $report['updates'], $report['deletes'], $report['skips']); - - // Store the string we got in the database. - case 'db-store': - // We got header information. - if ($value['msgid'] == '') { - $languages = language_list(); - if (($mode != LOCALE_IMPORT_KEEP) || empty($languages[$lang]->plurals)) { - // Since we only need to parse the header if we ought to update the - // plural formula, only run this if we don't need to keep existing - // data untouched or if we don't have an existing plural formula. - $header = _locale_import_parse_header($value['msgstr']); - - // Get the plural formula and update in database. - if (isset($header["Plural-Forms"]) && $p = _locale_import_parse_plural_forms($header["Plural-Forms"], $file->uri)) { - list($nplurals, $plural) = $p; - db_update('languages') - ->fields(array( - 'plurals' => $nplurals, - 'formula' => $plural, - )) - ->condition('language', $lang) - ->execute(); - } - else { - db_update('languages') - ->fields(array( - 'plurals' => 0, - 'formula' => '', - )) - ->condition('language', $lang) - ->execute(); - } - } - $header_done = TRUE; - } - - else { - // Some real string to import. - $comments = _locale_import_shorten_comments(empty($value['#']) ? array() : $value['#']); - - if (strpos($value['msgid'], "\0")) { - // This string has plural versions. - $english = explode("\0", $value['msgid'], 2); - $entries = array_keys($value['msgstr']); - for ($i = 3; $i <= count($entries); $i++) { - $english[] = $english[1]; - } - $translation = array_map('_locale_import_append_plural', $value['msgstr'], $entries); - $english = array_map('_locale_import_append_plural', $english, $entries); - foreach ($translation as $key => $trans) { - if ($key == 0) { - $plid = 0; - } - $plid = _locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english[$key], $trans, $group, $comments, $mode, $plid, $key); - } - } - - else { - // A simple string to import. - $english = $value['msgid']; - $translation = $value['msgstr']; - _locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english, $translation, $group, $comments, $mode); - } - } - } // end of db-store operation -} - -/** - * Import one string into the database. - * - * @param $report - * Report array summarizing the number of changes done in the form: - * array(inserts, updates, deletes). - * @param $langcode - * Language code to import string into. - * @param $context - * The context of this string. - * @param $source - * Source string. - * @param $translation - * Translation to language specified in $langcode. - * @param $textgroup - * Name of textgroup to store translation in. - * @param $location - * Location value to save with source string. - * @param $mode - * Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE. - * @param $plid - * Optional plural ID to use. - * @param $plural - * Optional plural value to use. - * - * @return - * The string ID of the existing string modified or the new string added. - */ -function _locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $textgroup, $location, $mode, $plid = 0, $plural = 0) { - $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = :textgroup", array(':source' => $source, ':context' => $context, ':textgroup' => $textgroup))->fetchField(); - - if (!empty($translation)) { - // Skip this string unless it passes a check for dangerous code. - // Text groups other than default still can contain HTML tags - // (i.e. translatable blocks). - if ($textgroup == "default" && !locale_string_is_safe($translation)) { - $report['skips']++; - $lid = 0; - } - elseif ($lid) { - // We have this source string saved already. - db_update('locales_source') - ->fields(array( - 'location' => $location, - )) - ->condition('lid', $lid) - ->execute(); - - $exists = db_query("SELECT COUNT(lid) FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchField(); - - if (!$exists) { - // No translation in this language. - db_insert('locales_target') - ->fields(array( - 'lid' => $lid, - 'language' => $langcode, - 'translation' => $translation, - 'plid' => $plid, - 'plural' => $plural, - )) - ->execute(); - - $report['additions']++; - } - elseif ($mode == LOCALE_IMPORT_OVERWRITE) { - // Translation exists, only overwrite if instructed. - db_update('locales_target') - ->fields(array( - 'translation' => $translation, - 'plid' => $plid, - 'plural' => $plural, - )) - ->condition('language', $langcode) - ->condition('lid', $lid) - ->execute(); - - $report['updates']++; - } - } - else { - // No such source string in the database yet. - $lid = db_insert('locales_source') - ->fields(array( - 'location' => $location, - 'source' => $source, - 'context' => (string) $context, - 'textgroup' => $textgroup, - )) - ->execute(); - - db_insert('locales_target') - ->fields(array( - 'lid' => $lid, - 'language' => $langcode, - 'translation' => $translation, - 'plid' => $plid, - 'plural' => $plural - )) - ->execute(); - - $report['additions']++; - } - } - elseif ($mode == LOCALE_IMPORT_OVERWRITE) { - // Empty translation, remove existing if instructed. - db_delete('locales_target') - ->condition('language', $langcode) - ->condition('lid', $lid) - ->condition('plid', $plid) - ->condition('plural', $plural) - ->execute(); - - $report['deletes']++; - } - - return $lid; -} - -/** - * Parses a Gettext Portable Object file header - * - * @param $header - * A string containing the complete header. - * - * @return - * An associative array of key-value pairs. - */ -function _locale_import_parse_header($header) { - $header_parsed = array(); - $lines = array_map('trim', explode("\n", $header)); - foreach ($lines as $line) { - if ($line) { - list($tag, $contents) = explode(":", $line, 2); - $header_parsed[trim($tag)] = trim($contents); - } - } - return $header_parsed; -} - -/** - * Parses a Plural-Forms entry from a Gettext Portable Object file header - * - * @param $pluralforms - * A string containing the Plural-Forms entry. - * @param $filepath - * A string containing the filepath. - * - * @return - * An array containing the number of plurals and a - * formula in PHP for computing the plural form. - */ -function _locale_import_parse_plural_forms($pluralforms, $filepath) { - // First, delete all whitespace - $pluralforms = strtr($pluralforms, array(" " => "", "\t" => "")); - - // Select the parts that define nplurals and plural - $nplurals = strstr($pluralforms, "nplurals="); - if (strpos($nplurals, ";")) { - $nplurals = substr($nplurals, 9, strpos($nplurals, ";") - 9); - } - else { - return FALSE; - } - $plural = strstr($pluralforms, "plural="); - if (strpos($plural, ";")) { - $plural = substr($plural, 7, strpos($plural, ";") - 7); - } - else { - return FALSE; - } - - // Get PHP version of the plural formula - $plural = _locale_import_parse_arithmetic($plural); - - if ($plural !== FALSE) { - return array($nplurals, $plural); - } - else { - drupal_set_message(t('The translation file %filepath contains an error: the plural formula could not be parsed.', array('%filepath' => $filepath)), 'error'); - return FALSE; - } -} - -/** - * Parses and sanitizes an arithmetic formula into a PHP expression - * - * While parsing, we ensure, that the operators have the right - * precedence and associativity. - * - * @param $string - * A string containing the arithmetic formula. - * - * @return - * The PHP version of the formula. - */ -function _locale_import_parse_arithmetic($string) { - // Operator precedence table - $precedence = array("(" => -1, ")" => -1, "?" => 1, ":" => 1, "||" => 3, "&&" => 4, "==" => 5, "!=" => 5, "<" => 6, ">" => 6, "<=" => 6, ">=" => 6, "+" => 7, "-" => 7, "*" => 8, "/" => 8, "%" => 8); - // Right associativity - $right_associativity = array("?" => 1, ":" => 1); - - $tokens = _locale_import_tokenize_formula($string); - - // Parse by converting into infix notation then back into postfix - // Operator stack - holds math operators and symbols - $operator_stack = array(); - // Element Stack - holds data to be operated on - $element_stack = array(); - - foreach ($tokens as $token) { - $current_token = $token; - - // Numbers and the $n variable are simply pushed into $element_stack - if (is_numeric($token)) { - $element_stack[] = $current_token; - } - elseif ($current_token == "n") { - $element_stack[] = '$n'; - } - elseif ($current_token == "(") { - $operator_stack[] = $current_token; - } - elseif ($current_token == ")") { - $topop = array_pop($operator_stack); - while (isset($topop) && ($topop != "(")) { - $element_stack[] = $topop; - $topop = array_pop($operator_stack); - } - } - elseif (!empty($precedence[$current_token])) { - // If it's an operator, then pop from $operator_stack into $element_stack until the - // precedence in $operator_stack is less than current, then push into $operator_stack - $topop = array_pop($operator_stack); - while (isset($topop) && ($precedence[$topop] >= $precedence[$current_token]) && !(($precedence[$topop] == $precedence[$current_token]) && !empty($right_associativity[$topop]) && !empty($right_associativity[$current_token]))) { - $element_stack[] = $topop; - $topop = array_pop($operator_stack); - } - if ($topop) { - $operator_stack[] = $topop; // Return element to top - } - $operator_stack[] = $current_token; // Parentheses are not needed - } - else { - return FALSE; - } - } - - // Flush operator stack - $topop = array_pop($operator_stack); - while ($topop != NULL) { - $element_stack[] = $topop; - $topop = array_pop($operator_stack); - } - - // Now extract formula from stack - $previous_size = count($element_stack) + 1; - while (count($element_stack) < $previous_size) { - $previous_size = count($element_stack); - for ($i = 2; $i < count($element_stack); $i++) { - $op = $element_stack[$i]; - if (!empty($precedence[$op])) { - $f = ""; - if ($op == ":") { - $f = $element_stack[$i - 2] . "):" . $element_stack[$i - 1] . ")"; - } - elseif ($op == "?") { - $f = "(" . $element_stack[$i - 2] . "?(" . $element_stack[$i - 1]; - } - else { - $f = "(" . $element_stack[$i - 2] . $op . $element_stack[$i - 1] . ")"; - } - array_splice($element_stack, $i - 2, 3, $f); - break; - } - } - } - - // If only one element is left, the number of operators is appropriate - if (count($element_stack) == 1) { - return $element_stack[0]; - } - else { - return FALSE; - } -} - -/** - * Backward compatible implementation of token_get_all() for formula parsing - * - * @param $string - * A string containing the arithmetic formula. - * - * @return - * The PHP version of the formula. - */ -function _locale_import_tokenize_formula($formula) { - $formula = str_replace(" ", "", $formula); - $tokens = array(); - for ($i = 0; $i < strlen($formula); $i++) { - if (is_numeric($formula[$i])) { - $num = $formula[$i]; - $j = $i + 1; - while ($j < strlen($formula) && is_numeric($formula[$j])) { - $num .= $formula[$j]; - $j++; - } - $i = $j - 1; - $tokens[] = $num; - } - elseif ($pos = strpos(" =<>!&|", $formula[$i])) { // We won't have a space - $next = $formula[$i + 1]; - switch ($pos) { - case 1: - case 2: - case 3: - case 4: - if ($next == '=') { - $tokens[] = $formula[$i] . '='; - $i++; - } - else { - $tokens[] = $formula[$i]; - } - break; - case 5: - if ($next == '&') { - $tokens[] = '&&'; - $i++; - } - else { - $tokens[] = $formula[$i]; - } - break; - case 6: - if ($next == '|') { - $tokens[] = '||'; - $i++; - } - else { - $tokens[] = $formula[$i]; - } - break; - } - } - else { - $tokens[] = $formula[$i]; - } - } - return $tokens; -} - -/** - * Modify a string to contain proper count indices - * - * This is a callback function used via array_map() - * - * @param $entry - * An array element. - * @param $key - * Index of the array element. - */ -function _locale_import_append_plural($entry, $key) { - // No modifications for 0, 1 - if ($key == 0 || $key == 1) { - return $entry; - } - - // First remove any possibly false indices, then add new ones - $entry = preg_replace('/(@count)\[[0-9]\]/', '\\1', $entry); - return preg_replace('/(@count)/', "\\1[$key]", $entry); -} - -/** - * Generate a short, one string version of the passed comment array - * - * @param $comment - * An array of strings containing a comment. - * - * @return - * Short one string version of the comment. - */ -function _locale_import_shorten_comments($comment) { - $comm = ''; - while (count($comment)) { - $test = $comm . substr(array_shift($comment), 1) . ', '; - if (strlen($comm) < 130) { - $comm = $test; - } - else { - break; - } - } - return trim(substr($comm, 0, -2)); -} - -/** - * Parses a string in quotes - * - * @param $string - * A string specified with enclosing quotes. - * - * @return - * The string parsed from inside the quotes. - */ -function _locale_import_parse_quoted($string) { - if (substr($string, 0, 1) != substr($string, -1, 1)) { - return FALSE; // Start and end quotes must be the same - } - $quote = substr($string, 0, 1); - $string = substr($string, 1, -1); - if ($quote == '"') { // Double quotes: strip slashes - return stripcslashes($string); - } - elseif ($quote == "'") { // Simple quote: return as-is - return $string; - } - else { - return FALSE; // Unrecognized quote - } -} -/** - * @} End of "locale-api-import-export" - */ - -/** - * Parses a JavaScript file, extracts strings wrapped in Drupal.t() and - * Drupal.formatPlural() and inserts them into the database. - */ -function _locale_parse_js_file($filepath) { - global $language; - - // The file path might contain a query string, so make sure we only use the - // actual file. - $parsed_url = drupal_parse_url($filepath); - $filepath = $parsed_url['path']; - // Load the JavaScript file. - $file = file_get_contents($filepath); - - // Match all calls to Drupal.t() in an array. - // Note: \s also matches newlines with the 's' modifier. - preg_match_all('~[^\w]Drupal\s*\.\s*t\s*\(\s*(' . LOCALE_JS_STRING . ')\s*[,\)]~s', $file, $t_matches); - - // Match all Drupal.formatPlural() calls in another array. - preg_match_all('~[^\w]Drupal\s*\.\s*formatPlural\s*\(\s*.+?\s*,\s*(' . LOCALE_JS_STRING . ')\s*,\s*((?:(?:\'(?:\\\\\'|[^\'])*@count(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*@count(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+)\s*[,\)]~s', $file, $plural_matches); - - // Loop through all matches and process them. - $all_matches = array_merge($plural_matches[1], $t_matches[1]); - foreach ($all_matches as $key => $string) { - $strings = array($string); - - // If there is also a plural version of this string, add it to the strings array. - if (isset($plural_matches[2][$key])) { - $strings[] = $plural_matches[2][$key]; - } - - foreach ($strings as $key => $string) { - // Remove the quotes and string concatenations from the string. - $string = implode('', preg_split('~(? $string))->fetchObject(); - if ($source) { - // We already have this source string and now have to add the location - // to the location column, if this file is not yet present in there. - $locations = preg_split('~\s*;\s*~', $source->location); - - if (!in_array($filepath, $locations)) { - $locations[] = $filepath; - $locations = implode('; ', $locations); - - // Save the new locations string to the database. - db_update('locales_source') - ->fields(array( - 'location' => $locations, - )) - ->condition('lid', $source->lid) - ->execute(); - } - } - else { - // We don't have the source string yet, thus we insert it into the database. - db_insert('locales_source') - ->fields(array( - 'location' => $filepath, - 'source' => $string, - 'context' => '', - 'textgroup' => 'default', - )) - ->execute(); - } - } - } -} - -/** - * @addtogroup locale-api-import-export - * @{ - */ - -/** - * Generates a structured array of all strings with translations in - * $language, if given. This array can be used to generate an export - * of the string in the database. - * - * @param $language - * Language object to generate the output for, or NULL if generating - * translation template. - * @param $group - * Text group to export PO file from (eg. 'default' for interface - * translations). - */ -function _locale_export_get_strings($language = NULL, $group = 'default') { - if (isset($language)) { - $result = db_query("SELECT s.lid, s.source, s.context, s.location, t.translation, t.plid, t.plural FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.textgroup = :textgroup ORDER BY t.plid, t.plural", array(':language' => $language->language, ':textgroup' => $group)); - } - else { - $result = db_query("SELECT s.lid, s.source, s.context, s.location, t.plid, t.plural FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid WHERE s.textgroup = :textgroup ORDER BY t.plid, t.plural", array(':textgroup' => $group)); - } - $strings = array(); - foreach ($result as $child) { - $string = array( - 'comment' => $child->location, - 'source' => $child->source, - 'context' => $child->context, - 'translation' => isset($child->translation) ? $child->translation : '', - ); - if ($child->plid) { - // Has a parent lid. Since we process in the order of plids, - // we already have the parent in the array, so we can add the - // lid to the next plural version to it. This builds a linked - // list of plurals. - $string['child'] = TRUE; - $strings[$child->plid]['plural'] = $child->lid; - } - $strings[$child->lid] = $string; - } - return $strings; -} - -/** - * Generates the PO(T) file contents for given strings. - * - * @param $language - * Language object to generate the output for, or NULL if generating - * translation template. - * @param $strings - * Array of strings to export. See _locale_export_get_strings() - * on how it should be formatted. - * @param $header - * The header portion to use for the output file. Defaults - * are provided for PO and POT files. - */ -function _locale_export_po_generate($language = NULL, $strings = array(), $header = NULL) { - global $user; - - if (!isset($header)) { - if (isset($language)) { - $header = '# ' . $language->name . ' translation of ' . variable_get('site_name', 'Drupal') . "\n"; - $header .= '# Generated by ' . $user->name . ' <' . $user->mail . ">\n"; - $header .= "#\n"; - $header .= "msgid \"\"\n"; - $header .= "msgstr \"\"\n"; - $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; - $header .= "\"POT-Creation-Date: " . date("Y-m-d H:iO") . "\\n\"\n"; - $header .= "\"PO-Revision-Date: " . date("Y-m-d H:iO") . "\\n\"\n"; - $header .= "\"Last-Translator: NAME \\n\"\n"; - $header .= "\"Language-Team: LANGUAGE \\n\"\n"; - $header .= "\"MIME-Version: 1.0\\n\"\n"; - $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; - $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; - if ($language->formula && $language->plurals) { - $header .= "\"Plural-Forms: nplurals=" . $language->plurals . "; plural=" . strtr($language->formula, array('$' => '')) . ";\\n\"\n"; - } - } - else { - $header = "# LANGUAGE translation of PROJECT\n"; - $header .= "# Copyright (c) YEAR NAME \n"; - $header .= "#\n"; - $header .= "msgid \"\"\n"; - $header .= "msgstr \"\"\n"; - $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; - $header .= "\"POT-Creation-Date: " . date("Y-m-d H:iO") . "\\n\"\n"; - $header .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n"; - $header .= "\"Last-Translator: NAME \\n\"\n"; - $header .= "\"Language-Team: LANGUAGE \\n\"\n"; - $header .= "\"MIME-Version: 1.0\\n\"\n"; - $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; - $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; - $header .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n"; - } - } - - $output = $header . "\n"; - - foreach ($strings as $lid => $string) { - // Only process non-children, children are output below their parent. - if (!isset($string['child'])) { - if ($string['comment']) { - $output .= '#: ' . $string['comment'] . "\n"; - } - if (!empty($string['context'])) { - $output .= 'msgctxt ' . _locale_export_string($string['context']); - } - $output .= 'msgid ' . _locale_export_string($string['source']); - if (!empty($string['plural'])) { - $plural = $string['plural']; - $output .= 'msgid_plural ' . _locale_export_string($strings[$plural]['source']); - if (isset($language)) { - $translation = $string['translation']; - for ($i = 0; $i < $language->plurals; $i++) { - $output .= 'msgstr[' . $i . '] ' . _locale_export_string($translation); - if ($plural) { - $translation = _locale_export_remove_plural($strings[$plural]['translation']); - $plural = isset($strings[$plural]['plural']) ? $strings[$plural]['plural'] : 0; - } - else { - $translation = ''; - } - } - } - else { - $output .= 'msgstr[0] ""' . "\n"; - $output .= 'msgstr[1] ""' . "\n"; - } - } - else { - $output .= 'msgstr ' . _locale_export_string($string['translation']); - } - $output .= "\n"; - } - } - return $output; -} - -/** - * Write a generated PO or POT file to the output. - * - * @param $language - * Language object to generate the output for, or NULL if generating - * translation template. - * @param $output - * The PO(T) file to output as a string. See _locale_export_generate_po() - * on how it can be generated. - */ -function _locale_export_po($language = NULL, $output = NULL) { - // Log the export event. - if (isset($language)) { - $filename = $language->language . '.po'; - watchdog('locale', 'Exported %locale translation file: %filename.', array('%locale' => $language->name, '%filename' => $filename)); - } - else { - $filename = 'drupal.pot'; - watchdog('locale', 'Exported translation file: %filename.', array('%filename' => $filename)); - } - // Download the file for the client. - header("Content-Disposition: attachment; filename=$filename"); - header("Content-Type: text/plain; charset=utf-8"); - print $output; - drupal_exit(); -} - -/** - * Print out a string on multiple lines - */ -function _locale_export_string($str) { - $stri = addcslashes($str, "\0..\37\\\""); - $parts = array(); - - // Cut text into several lines - while ($stri != "") { - $i = strpos($stri, "\\n"); - if ($i === FALSE) { - $curstr = $stri; - $stri = ""; - } - else { - $curstr = substr($stri, 0, $i + 2); - $stri = substr($stri, $i + 2); - } - $curparts = explode("\n", _locale_export_wrap($curstr, 70)); - $parts = array_merge($parts, $curparts); - } - - // Multiline string - if (count($parts) > 1) { - return "\"\"\n\"" . implode("\"\n\"", $parts) . "\"\n"; - } - // Single line string - elseif (count($parts) == 1) { - return "\"$parts[0]\"\n"; - } - // No translation - else { - return "\"\"\n"; - } -} - -/** - * Custom word wrapping for Portable Object (Template) files. - */ -function _locale_export_wrap($str, $len) { - $words = explode(' ', $str); - $return = array(); - - $cur = ""; - $nstr = 1; - while (count($words)) { - $word = array_shift($words); - if ($nstr) { - $cur = $word; - $nstr = 0; - } - elseif (strlen("$cur $word") > $len) { - $return[] = $cur . " "; - $cur = $word; - } - else { - $cur = "$cur $word"; - } - } - $return[] = $cur; - - return implode("\n", $return); -} - -/** - * Removes plural index information from a string - */ -function _locale_export_remove_plural($entry) { - return preg_replace('/(@count)\[[0-9]\]/', '\\1', $entry); -} -/** - * @} End of "locale-api-import-export" - */ - -/** - * @defgroup locale-api-seek Translation search API - * @{ - * Functions to search in translation files. - * - * These functions provide the functionality to search for specific - * translations. - */ - -/** - * Perform a string search and display results in a table - */ -function _locale_translate_seek() { - $output = ''; - - // We have at least one criterion to match - if (!($query = _locale_translate_seek_query())) { - $query = array( - 'translation' => 'all', - 'group' => 'all', - 'language' => 'all', - 'string' => '', - ); - } - - $sql_query = db_select('locales_source', 's'); - $sql_query->leftJoin('locales_target', 't', 't.lid = s.lid'); - $sql_query->fields('s', array('source', 'location', 'context', 'lid', 'textgroup')); - $sql_query->fields('t', array('translation', 'language')); - - // Compute LIKE section. - switch ($query['translation']) { - case 'translated': - $sql_query->condition('t.translation', '%' . db_like($query['string']) . '%', 'LIKE'); - $sql_query->orderBy('t.translation', 'DESC'); - break; - case 'untranslated': - $sql_query->condition(db_and() - ->condition('s.source', '%' . db_like($query['string']) . '%', 'LIKE') - ->isNull('t.translation') - ); - $sql_query->orderBy('s.source'); - break; - case 'all' : - default: - $condition = db_or() - ->condition('s.source', '%' . db_like($query['string']) . '%', 'LIKE'); - if ($query['language'] != 'en') { - // Only search in translations if the language is not forced to English. - $condition->condition('t.translation', '%' . db_like($query['string']) . '%', 'LIKE'); - } - $sql_query->condition($condition); - break; - } - - $limit_language = NULL; - if ($query['language'] != 'en' && $query['language'] != 'all') { - $sql_query->condition('language', $query['language']); - $limit_language = $query['language']; - } - - // Add a condition on the text group. - if (!empty($query['group']) && $query['group'] != 'all') { - $sql_query->condition('s.textgroup', $query['group']); - } - - $sql_query = $sql_query->extend('PagerDefault')->limit(50); - $locales = $sql_query->execute(); - - $groups = module_invoke_all('locale', 'groups'); - $header = array(t('Text group'), t('String'), t('Context'), ($limit_language) ? t('Language') : t('Languages'), array('data' => t('Operations'), 'colspan' => '2')); - - $strings = array(); - foreach ($locales as $locale) { - if (!isset($strings[$locale->lid])) { - $strings[$locale->lid] = array( - 'group' => $locale->textgroup, - 'languages' => array(), - 'location' => $locale->location, - 'source' => $locale->source, - 'context' => $locale->context, - ); - } - if (isset($locale->language)) { - $strings[$locale->lid]['languages'][$locale->language] = $locale->translation; - } - } - - $rows = array(); - foreach ($strings as $lid => $string) { - $rows[] = array( - $groups[$string['group']], - array('data' => check_plain(truncate_utf8($string['source'], 150, FALSE, TRUE)) . '
    ' . $string['location'] . ''), - $string['context'], - array('data' => _locale_translate_language_list($string['languages'], $limit_language), 'align' => 'center'), - array('data' => l(t('edit'), "admin/config/regional/translate/edit/$lid", array('query' => drupal_get_destination())), 'class' => array('nowrap')), - array('data' => l(t('delete'), "admin/config/regional/translate/delete/$lid", array('query' => drupal_get_destination())), 'class' => array('nowrap')), - ); - } - - $output .= theme('table', array('header' => $header, 'rows' => $rows, 'empty' => t('No strings available.'))); - $output .= theme('pager'); - - return $output; -} - -/** - * Build array out of search criteria specified in request variables - */ -function _locale_translate_seek_query() { - $query = &drupal_static(__FUNCTION__); - if (!isset($query)) { - $query = array(); - $fields = array('string', 'language', 'translation', 'group'); - foreach ($fields as $field) { - if (isset($_SESSION['locale_translation_filter'][$field])) { - $query[$field] = $_SESSION['locale_translation_filter'][$field]; - } - } - } - return $query; -} - -/** - * Force the JavaScript translation file(s) to be refreshed. - * - * This function sets a refresh flag for a specified language, or all - * languages except English, if none specified. JavaScript translation - * files are rebuilt (with locale_update_js_files()) the next time a - * request is served in that language. - * - * @param $langcode - * The language code for which the file needs to be refreshed. - * - * @return - * New content of the 'javascript_parsed' variable. - */ -function _locale_invalidate_js($langcode = NULL) { - $parsed = variable_get('javascript_parsed', array()); - - if (empty($langcode)) { - // Invalidate all languages. - $languages = language_list(); - unset($languages['en']); - foreach ($languages as $lcode => $data) { - $parsed['refresh:' . $lcode] = 'waiting'; - } - } - else { - // Invalidate single language. - $parsed['refresh:' . $langcode] = 'waiting'; - } - - variable_set('javascript_parsed', $parsed); - return $parsed; -} - -/** - * (Re-)Creates the JavaScript translation file for a language. - * - * @param $language - * The language, the translation file should be (re)created for. - */ -function _locale_rebuild_js($langcode = NULL) { - if (!isset($langcode)) { - global $language; - } - else { - // Get information about the locale. - $languages = language_list(); - $language = $languages[$langcode]; - } - - // Construct the array for JavaScript translations. - // Only add strings with a translation to the translations array. - $result = db_query("SELECT s.lid, s.source, t.translation FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.location LIKE '%.js%' AND s.textgroup = :textgroup", array(':language' => $language->language, ':textgroup' => 'default')); - - $translations = array(); - foreach ($result as $data) { - $translations[$data->source] = $data->translation; - } - - // Construct the JavaScript file, if there are translations. - $data_hash = NULL; - $data = $status = ''; - if (!empty($translations)) { - - $data = "Drupal.locale = { "; - - if (!empty($language->formula)) { - $data .= "'pluralFormula': function (\$n) { return Number({$language->formula}); }, "; - } - - $data .= "'strings': " . drupal_json_encode($translations) . " };"; - $data_hash = drupal_hash_base64($data); - } - - // Construct the filepath where JS translation files are stored. - // There is (on purpose) no front end to edit that variable. - $dir = 'public://' . variable_get('locale_js_directory', 'languages'); - - // Delete old file, if we have no translations anymore, or a different file to be saved. - $changed_hash = $language->javascript != $data_hash; - if (!empty($language->javascript) && (!$data || $changed_hash)) { - file_unmanaged_delete($dir . '/' . $language->language . '_' . $language->javascript . '.js'); - $language->javascript = ''; - $status = 'deleted'; - } - - // Only create a new file if the content has changed or the original file got - // lost. - $dest = $dir . '/' . $language->language . '_' . $data_hash . '.js'; - if ($data && ($changed_hash || !file_exists($dest))) { - // Ensure that the directory exists and is writable, if possible. - file_prepare_directory($dir, FILE_CREATE_DIRECTORY); - - // Save the file. - if (file_unmanaged_save_data($data, $dest)) { - $language->javascript = $data_hash; - // If we deleted a previous version of the file and we replace it with a - // new one we have an update. - if ($status == 'deleted') { - $status = 'updated'; - } - // If the file did not exist previously and the data has changed we have - // a fresh creation. - elseif ($changed_hash) { - $status = 'created'; - } - // If the data hash is unchanged the translation was lost and has to be - // rebuilt. - else { - $status = 'rebuilt'; - } - } - else { - $language->javascript = ''; - $status = 'error'; - } - } - - // Save the new JavaScript hash (or an empty value if the file just got - // deleted). Act only if some operation was executed that changed the hash - // code. - if ($status && $changed_hash) { - db_update('languages') - ->fields(array( - 'javascript' => $language->javascript, - )) - ->condition('language', $language->language) - ->execute(); - - // Update the default language variable if the default language has been altered. - // This is necessary to keep the variable consistent with the database - // version of the language and to prevent checking against an outdated hash. - $default_langcode = language_default('language'); - if ($default_langcode == $language->language) { - $default = db_query("SELECT * FROM {languages} WHERE language = :language", array(':language' => $default_langcode))->fetchObject(); - variable_set('language_default', $default); - } - } - - // Log the operation and return success flag. - switch ($status) { - case 'updated': - watchdog('locale', 'Updated JavaScript translation file for the language %language.', array('%language' => t($language->name))); - return TRUE; - case 'rebuilt': - watchdog('locale', 'JavaScript translation file %file.js was lost.', array('%file' => $language->javascript), LOG_WARNING); - // Proceed to the 'created' case as the JavaScript translation file has - // been created again. - case 'created': - watchdog('locale', 'Created JavaScript translation file for the language %language.', array('%language' => t($language->name))); - return TRUE; - case 'deleted': - watchdog('locale', 'Removed JavaScript translation file for the language %language, because no translations currently exist for that language.', array('%language' => t($language->name))); - return TRUE; - case 'error': - watchdog('locale', 'An error occurred during creation of the JavaScript translation file for the language %language.', array('%language' => t($language->name)), LOG_ERR); - return FALSE; - default: - // No operation needed. - return TRUE; - } -} - -/** - * List languages in search result table - */ -function _locale_translate_language_list($translation, $limit_language) { - // Add CSS. - drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css'); - - $languages = language_list(); - unset($languages['en']); - $output = ''; - foreach ($languages as $langcode => $language) { - if (!$limit_language || $limit_language == $langcode) { - $output .= (!empty($translation[$langcode])) ? $langcode . ' ' : "$langcode "; - } - } - - return $output; -} -/** - * @} End of "locale-api-seek" - */ - -/** - * @defgroup locale-api-predefined List of predefined languages - * @{ - * API to provide a list of predefined languages. - */ - -/** - * Prepares the language code list for a select form item with only the unsupported ones - */ -function _locale_prepare_predefined_list() { - include_once DRUPAL_ROOT . '/includes/iso.inc'; - $languages = language_list(); - $predefined = _locale_get_predefined_list(); - foreach ($predefined as $key => $value) { - if (isset($languages[$key])) { - unset($predefined[$key]); - continue; - } - // Include native name in output, if possible - if (count($value) > 1) { - $tname = t($value[0]); - $predefined[$key] = ($tname == $value[1]) ? $tname : "$tname ($value[1])"; - } - else { - $predefined[$key] = t($value[0]); - } - } - asort($predefined); - return $predefined; -} - -/** - * @} End of "locale-api-languages-predefined" - */ - -/** - * @defgroup locale-autoimport Automatic interface translation import - * @{ - * Functions to create batches for importing translations. - * - * These functions can be used to import translations for installed - * modules. - */ - -/** - * Prepare a batch to import translations for all enabled - * modules in a given language. - * - * @param $langcode - * Language code to import translations for. - * @param $finished - * Optional finished callback for the batch. - * @param $skip - * Array of component names to skip. Used in the installer for the - * second pass import, when most components are already imported. - * - * @return - * A batch structure or FALSE if no files found. - */ -function locale_batch_by_language($langcode, $finished = NULL, $skip = array()) { - // Collect all files to import for all enabled modules and themes. - $files = array(); - $components = array(); - $query = db_select('system', 's'); - $query->fields('s', array('name', 'filename')); - $query->condition('s.status', 1); - if (count($skip)) { - $query->condition('name', $skip, 'NOT IN'); - } - $result = $query->execute(); - foreach ($result as $component) { - // Collect all files for all components, names as $langcode.po or - // with names ending with $langcode.po. This allows for filenames - // like node-module.de.po to let translators use small files and - // be able to import in smaller chunks. - $files = array_merge($files, file_scan_directory(dirname($component->filename) . '/translations', '/(^|\.)' . $langcode . '\.po$/', array('recurse' => FALSE))); - $components[] = $component->name; - } - - return _locale_batch_build($files, $finished, $components); -} - -/** - * Prepare a batch to run when installing modules or enabling themes. - * - * This batch will import translations for the newly added components - * in all the languages already set up on the site. - * - * @param $components - * An array of component (theme and/or module) names to import - * translations for. - * @param $finished - * Optional finished callback for the batch. - */ -function locale_batch_by_component($components, $finished = '_locale_batch_system_finished') { - $files = array(); - $languages = language_list('enabled'); - unset($languages[1]['en']); - if (count($languages[1])) { - $language_list = join('|', array_keys($languages[1])); - // Collect all files to import for all $components. - $result = db_query("SELECT name, filename FROM {system} WHERE status = 1"); - foreach ($result as $component) { - if (in_array($component->name, $components)) { - // Collect all files for this component in all enabled languages, named - // as $langcode.po or with names ending with $langcode.po. This allows - // for filenames like node-module.de.po to let translators use small - // files and be able to import in smaller chunks. - $files = array_merge($files, file_scan_directory(dirname($component->filename) . '/translations', '/(^|\.)(' . $language_list . ')\.po$/', array('recurse' => FALSE))); - } - } - return _locale_batch_build($files, $finished); - } - return FALSE; -} - -/** - * Build a locale batch from an array of files. - * - * @param $files - * Array of files to import. - * @param $finished - * Optional finished callback for the batch. - * @param $components - * Optional list of component names the batch covers. Used in the installer. - * - * @return - * A batch structure. - */ -function _locale_batch_build($files, $finished = NULL, $components = array()) { - $t = get_t(); - if (count($files)) { - $operations = array(); - foreach ($files as $file) { - // We call _locale_batch_import for every batch operation. - $operations[] = array('_locale_batch_import', array($file->uri)); - } - $batch = array( - 'operations' => $operations, - 'title' => $t('Importing interface translations'), - 'init_message' => $t('Starting import'), - 'error_message' => $t('Error importing interface translations'), - 'file' => 'includes/locale.inc', - // This is not a batch API construct, but data passed along to the - // installer, so we know what did we import already. - '#components' => $components, - ); - if (isset($finished)) { - $batch['finished'] = $finished; - } - return $batch; - } - return FALSE; -} - -/** - * Perform interface translation import as a batch step. - * - * @param $filepath - * Path to a file to import. - * @param $results - * Contains a list of files imported. - */ -function _locale_batch_import($filepath, &$context) { - // The filename is either {langcode}.po or {prefix}.{langcode}.po, so - // we can extract the language code to use for the import from the end. - if (preg_match('!(/|\.)([^\./]+)\.po$!', $filepath, $langcode)) { - $file = (object) array('filename' => basename($filepath), 'uri' => $filepath); - _locale_import_read_po('db-store', $file, LOCALE_IMPORT_KEEP, $langcode[2]); - $context['results'][] = $filepath; - } -} - -/** - * Finished callback of system page locale import batch. - * Inform the user of translation files imported. - */ -function _locale_batch_system_finished($success, $results) { - if ($success) { - drupal_set_message(format_plural(count($results), 'One translation file imported for the newly installed modules.', '@count translation files imported for the newly installed modules.')); - } -} - -/** - * Finished callback of language addition locale import batch. - * Inform the user of translation files imported. - */ -function _locale_batch_language_finished($success, $results) { - if ($success) { - drupal_set_message(format_plural(count($results), 'One translation file imported for the enabled modules.', '@count translation files imported for the enabled modules.')); - } -} - -/** - * @} End of "locale-autoimport" - */ - -/** - * Get list of all predefined and custom countries. - * - * @return - * An array of all country code => country name pairs. - */ -function country_get_list() { - include_once DRUPAL_ROOT . '/includes/iso.inc'; - $countries = _country_get_predefined_list(); - // Allow other modules to modify the country list. - drupal_alter('countries', $countries); - return $countries; -} - -/** - * Save locale specific date formats to the database. - * - * @param $langcode - * Language code, can be 2 characters, e.g. 'en' or 5 characters, e.g. - * 'en-CA'. - * @param $type - * Date format type, e.g. 'short', 'medium'. - * @param $format - * The date format string. - */ -function locale_date_format_save($langcode, $type, $format) { - $locale_format = array(); - $locale_format['language'] = $langcode; - $locale_format['type'] = $type; - $locale_format['format'] = $format; - - $is_existing = (bool) db_query_range('SELECT 1 FROM {date_format_locale} WHERE language = :langcode AND type = :type', 0, 1, array(':langcode' => $langcode, ':type' => $type))->fetchField(); - if ($is_existing) { - $keys = array('type', 'language'); - drupal_write_record('date_format_locale', $locale_format, $keys); - } - else { - drupal_write_record('date_format_locale', $locale_format); - } -} - -/** - * Select locale date format details from database. - * - * @param $languages - * An array of language codes. - * - * @return - * An array of date formats. - */ -function locale_get_localized_date_format($languages) { - $formats = array(); - - // Get list of different format types. - $format_types = system_get_date_types(); - $short_default = variable_get('date_format_short', 'm/d/Y - H:i'); - - // Loop through each language until we find one with some date formats - // configured. - foreach ($languages as $language) { - $date_formats = system_date_format_locale($language); - if (!empty($date_formats)) { - // We have locale-specific date formats, so check for their types. If - // we're missing a type, use the default setting instead. - foreach ($format_types as $type => $type_info) { - // If format exists for this language, use it. - if (!empty($date_formats[$type])) { - $formats['date_format_' . $type] = $date_formats[$type]; - } - // Otherwise get default variable setting. If this is not set, default - // to the short format. - else { - $formats['date_format_' . $type] = variable_get('date_format_' . $type, $short_default); - } - } - - // Return on the first match. - return $formats; - } - } - - // No locale specific formats found, so use defaults. - $system_types = array('short', 'medium', 'long'); - // Handle system types separately as they have defaults if no variable exists. - $formats['date_format_short'] = $short_default; - $formats['date_format_medium'] = variable_get('date_format_medium', 'D, m/d/Y - H:i'); - $formats['date_format_long'] = variable_get('date_format_long', 'l, F j, Y - H:i'); - - // For non-system types, get the default setting, otherwise use the short - // format. - foreach ($format_types as $type => $type_info) { - if (!in_array($type, $system_types)) { - $formats['date_format_' . $type] = variable_get('date_format_' . $type, $short_default); - } - } - - return $formats; -} diff --git a/includes/module.inc b/includes/module.inc deleted file mode 100644 index 23f2fa8..0000000 --- a/includes/module.inc +++ /dev/null @@ -1,1007 +0,0 @@ - $module) { - drupal_get_filename('module', $name, $module['filename']); - $list[$name] = $name; - } - } - else { - if ($refresh) { - // For the $refresh case, make sure that system_list() returns fresh - // data. - drupal_static_reset('system_list'); - } - if ($bootstrap_refresh) { - $list = system_list('bootstrap'); - } - else { - // Not using drupal_map_assoc() here as that requires common.inc. - $list = array_keys(system_list('module_enabled')); - $list = (!empty($list) ? array_combine($list, $list) : array()); - } - } - } - if ($sort) { - if (!isset($sorted_list)) { - $sorted_list = $list; - ksort($sorted_list); - } - return $sorted_list; - } - return $list; -} - -/** - * Build a list of bootstrap modules and enabled modules and themes. - * - * @param $type - * The type of list to return: - * - module_enabled: All enabled modules. - * - bootstrap: All enabled modules required for bootstrap. - * - theme: All themes. - * - * @return - * An associative array of modules or themes, keyed by name. For $type - * 'bootstrap', the array values equal the keys. For $type 'module_enabled' - * or 'theme', the array values are objects representing the respective - * database row, with the 'info' property already unserialized. - * - * @see module_list() - * @see list_themes() - */ -function system_list($type) { - $lists = &drupal_static(__FUNCTION__); - - // For bootstrap modules, attempt to fetch the list from cache if possible. - // if not fetch only the required information to fire bootstrap hooks - // in case we are going to serve the page from cache. - if ($type == 'bootstrap') { - if (isset($lists['bootstrap'])) { - return $lists['bootstrap']; - } - if ($cached = cache_get('bootstrap_modules', 'cache_bootstrap')) { - $bootstrap_list = $cached->data; - } - else { - $bootstrap_list = db_query("SELECT name, filename FROM {system} WHERE status = 1 AND bootstrap = 1 AND type = 'module' ORDER BY weight ASC, name ASC")->fetchAllAssoc('name'); - cache_set('bootstrap_modules', $bootstrap_list, 'cache_bootstrap'); - } - // To avoid a separate database lookup for the filepath, prime the - // drupal_get_filename() static cache for bootstrap modules only. - // The rest is stored separately to keep the bootstrap module cache small. - foreach ($bootstrap_list as $module) { - drupal_get_filename('module', $module->name, $module->filename); - } - // We only return the module names here since module_list() doesn't need - // the filename itself. - $lists['bootstrap'] = array_keys($bootstrap_list); - } - // Otherwise build the list for enabled modules and themes. - elseif (!isset($lists['module_enabled'])) { - if ($cached = cache_get('system_list', 'cache_bootstrap')) { - $lists = $cached->data; - } - else { - $lists = array( - 'module_enabled' => array(), - 'theme' => array(), - 'filepaths' => array(), - ); - // The module name (rather than the filename) is used as the fallback - // weighting in order to guarantee consistent behavior across different - // Drupal installations, which might have modules installed in different - // locations in the file system. The ordering here must also be - // consistent with the one used in module_implements(). - $result = db_query("SELECT * FROM {system} WHERE type = 'theme' OR (type = 'module' AND status = 1) ORDER BY weight ASC, name ASC"); - foreach ($result as $record) { - $record->info = unserialize($record->info); - // Build a list of all enabled modules. - if ($record->type == 'module') { - $lists['module_enabled'][$record->name] = $record; - } - // Build a list of themes. - if ($record->type == 'theme') { - $lists['theme'][$record->name] = $record; - } - // Build a list of filenames so drupal_get_filename can use it. - if ($record->status) { - $lists['filepaths'][] = array('type' => $record->type, 'name' => $record->name, 'filepath' => $record->filename); - } - } - cache_set('system_list', $lists, 'cache_bootstrap'); - } - // To avoid a separate database lookup for the filepath, prime the - // drupal_get_filename() static cache with all enabled modules and themes. - foreach ($lists['filepaths'] as $item) { - drupal_get_filename($item['type'], $item['name'], $item['filepath']); - } - } - - return $lists[$type]; -} - -/** - * Reset all system_list() caches. - */ -function system_list_reset() { - drupal_static_reset('system_list'); - drupal_static_reset('system_rebuild_module_data'); - drupal_static_reset('list_themes'); - cache_clear_all('bootstrap_modules', 'cache_bootstrap'); - cache_clear_all('system_list', 'cache_bootstrap'); -} - -/** - * Find dependencies any level deep and fill in required by information too. - * - * @param $files - * The array of filesystem objects used to rebuild the cache. - * - * @return - * The same array with the new keys for each module: - * - requires: An array with the keys being the modules that this module - * requires. - * - required_by: An array with the keys being the modules that will not work - * without this module. - */ -function _module_build_dependencies($files) { - require_once DRUPAL_ROOT . '/includes/graph.inc'; - foreach ($files as $filename => $file) { - $graph[$file->name]['edges'] = array(); - if (isset($file->info['dependencies']) && is_array($file->info['dependencies'])) { - foreach ($file->info['dependencies'] as $dependency) { - $dependency_data = drupal_parse_dependency($dependency); - $graph[$file->name]['edges'][$dependency_data['name']] = $dependency_data; - } - } - } - drupal_depth_first_search($graph); - foreach ($graph as $module => $data) { - $files[$module]->required_by = isset($data['reverse_paths']) ? $data['reverse_paths'] : array(); - $files[$module]->requires = isset($data['paths']) ? $data['paths'] : array(); - $files[$module]->sort = $data['weight']; - } - return $files; -} - -/** - * Determine whether a given module exists. - * - * @param $module - * The name of the module (without the .module extension). - * - * @return - * TRUE if the module is both installed and enabled. - */ -function module_exists($module) { - $list = module_list(); - return isset($list[$module]); -} - -/** - * Load a module's installation hooks. - * - * @param $module - * The name of the module (without the .module extension). - * - * @return - * The name of the module's install file, if successful; FALSE otherwise. - */ -function module_load_install($module) { - // Make sure the installation API is available - include_once DRUPAL_ROOT . '/includes/install.inc'; - - return module_load_include('install', $module); -} - -/** - * Load a module include file. - * - * Examples: - * @code - * // Load node.admin.inc from the node module. - * module_load_include('inc', 'node', 'node.admin'); - * // Load content_types.inc from the node module. - * module_load_include('inc', 'node', 'content_types'); - * @endcode - * - * Do not use this function to load an install file, use module_load_install() - * instead. Do not use this function in a global context since it requires - * Drupal to be fully bootstrapped, use require_once DRUPAL_ROOT . '/path/file' - * instead. - * - * @param $type - * The include file's type (file extension). - * @param $module - * The module to which the include file belongs. - * @param $name - * (optional) The base file name (without the $type extension). If omitted, - * $module is used; i.e., resulting in "$module.$type" by default. - * - * @return - * The name of the included file, if successful; FALSE otherwise. - */ -function module_load_include($type, $module, $name = NULL) { - if (!isset($name)) { - $name = $module; - } - - if (function_exists('drupal_get_path')) { - $file = DRUPAL_ROOT . '/' . drupal_get_path('module', $module) . "/$name.$type"; - if (is_file($file)) { - require_once $file; - return $file; - } - } - return FALSE; -} - -/** - * Load an include file for each of the modules that have been enabled in - * the system table. - */ -function module_load_all_includes($type, $name = NULL) { - $modules = module_list(); - foreach ($modules as $module) { - module_load_include($type, $module, $name); - } -} - -/** - * Enables or installs a given list of modules. - * - * Definitions: - * - "Enabling" is the process of activating a module for use by Drupal. - * - "Disabling" is the process of deactivating a module. - * - "Installing" is the process of enabling it for the first time or after it - * has been uninstalled. - * - "Uninstalling" is the process of removing all traces of a module. - * - * Order of events: - * - Gather and add module dependencies to $module_list (if applicable). - * - For each module that is being enabled: - * - Install module schema and update system registries and caches. - * - If the module is being enabled for the first time or had been - * uninstalled, invoke hook_install() and add it to the list of installed - * modules. - * - Invoke hook_enable(). - * - Invoke hook_modules_installed(). - * - Invoke hook_modules_enabled(). - * - * @param $module_list - * An array of module names. - * @param $enable_dependencies - * If TRUE, dependencies will automatically be added and enabled in the - * correct order. This incurs a significant performance cost, so use FALSE - * if you know $module_list is already complete and in the correct order. - * - * @return - * FALSE if one or more dependencies are missing, TRUE otherwise. - * - * @see hook_install() - * @see hook_enable() - * @see hook_modules_installed() - * @see hook_modules_enabled() - */ -function module_enable($module_list, $enable_dependencies = TRUE) { - if ($enable_dependencies) { - // Get all module data so we can find dependencies and sort. - $module_data = system_rebuild_module_data(); - // Create an associative array with weights as values. - $module_list = array_flip(array_values($module_list)); - - while (list($module) = each($module_list)) { - if (!isset($module_data[$module])) { - // This module is not found in the filesystem, abort. - return FALSE; - } - if ($module_data[$module]->status) { - // Skip already enabled modules. - unset($module_list[$module]); - continue; - } - $module_list[$module] = $module_data[$module]->sort; - - // Add dependencies to the list, with a placeholder weight. - // The new modules will be processed as the while loop continues. - foreach (array_keys($module_data[$module]->requires) as $dependency) { - if (!isset($module_list[$dependency])) { - $module_list[$dependency] = 0; - } - } - } - - if (!$module_list) { - // Nothing to do. All modules already enabled. - return TRUE; - } - - // Sort the module list by pre-calculated weights. - arsort($module_list); - $module_list = array_keys($module_list); - } - - // Required for module installation checks. - include_once DRUPAL_ROOT . '/includes/install.inc'; - - $modules_installed = array(); - $modules_enabled = array(); - foreach ($module_list as $module) { - // Only process modules that are not already enabled. - $existing = db_query("SELECT status FROM {system} WHERE type = :type AND name = :name", array( - ':type' => 'module', - ':name' => $module)) - ->fetchObject(); - if ($existing->status == 0) { - // Load the module's code. - drupal_load('module', $module); - module_load_install($module); - - // Update the database and module list to reflect the new module. This - // needs to be done first so that the module's hook implementations, - // hook_schema() in particular, can be called while it is being - // installed. - db_update('system') - ->fields(array('status' => 1)) - ->condition('type', 'module') - ->condition('name', $module) - ->execute(); - // Refresh the module list to include it. - system_list_reset(); - module_list(TRUE); - module_implements('', FALSE, TRUE); - _system_update_bootstrap_status(); - // Update the registry to include it. - registry_update(); - // Refresh the schema to include it. - drupal_get_schema(NULL, TRUE); - // Clear entity cache. - entity_info_cache_clear(); - - // Now install the module if necessary. - if (drupal_get_installed_schema_version($module, TRUE) == SCHEMA_UNINSTALLED) { - drupal_install_schema($module); - - // Set the schema version to the number of the last update provided - // by the module. - $versions = drupal_get_schema_versions($module); - $version = $versions ? max($versions) : SCHEMA_INSTALLED; - - // If the module has no current updates, but has some that were - // previously removed, set the version to the value of - // hook_update_last_removed(). - if ($last_removed = module_invoke($module, 'update_last_removed')) { - $version = max($version, $last_removed); - } - drupal_set_installed_schema_version($module, $version); - // Allow the module to perform install tasks. - module_invoke($module, 'install'); - // Record the fact that it was installed. - $modules_installed[] = $module; - watchdog('system', '%module module installed.', array('%module' => $module), LOG_INFO); - } - - // Enable the module. - module_invoke($module, 'enable'); - - // Record the fact that it was enabled. - $modules_enabled[] = $module; - watchdog('system', '%module module enabled.', array('%module' => $module), LOG_INFO); - } - } - - // If any modules were newly installed, invoke hook_modules_installed(). - if (!empty($modules_installed)) { - module_invoke_all('modules_installed', $modules_installed); - } - - // If any modules were newly enabled, invoke hook_modules_enabled(). - if (!empty($modules_enabled)) { - module_invoke_all('modules_enabled', $modules_enabled); - } - - return TRUE; -} - -/** - * Disable a given set of modules. - * - * @param $module_list - * An array of module names. - * @param $disable_dependents - * If TRUE, dependent modules will automatically be added and disabled in the - * correct order. This incurs a significant performance cost, so use FALSE - * if you know $module_list is already complete and in the correct order. - */ -function module_disable($module_list, $disable_dependents = TRUE) { - if ($disable_dependents) { - // Get all module data so we can find dependents and sort. - $module_data = system_rebuild_module_data(); - // Create an associative array with weights as values. - $module_list = array_flip(array_values($module_list)); - - $profile = drupal_get_profile(); - while (list($module) = each($module_list)) { - if (!isset($module_data[$module]) || !$module_data[$module]->status) { - // This module doesn't exist or is already disabled, skip it. - unset($module_list[$module]); - continue; - } - $module_list[$module] = $module_data[$module]->sort; - - // Add dependent modules to the list, with a placeholder weight. - // The new modules will be processed as the while loop continues. - foreach ($module_data[$module]->required_by as $dependent => $dependent_data) { - if (!isset($module_list[$dependent]) && $dependent != $profile) { - $module_list[$dependent] = 0; - } - } - } - - // Sort the module list by pre-calculated weights. - asort($module_list); - $module_list = array_keys($module_list); - } - - $invoke_modules = array(); - - foreach ($module_list as $module) { - if (module_exists($module)) { - // Check if node_access table needs rebuilding. - if (!node_access_needs_rebuild() && module_hook($module, 'node_grants')) { - node_access_needs_rebuild(TRUE); - } - - module_load_install($module); - module_invoke($module, 'disable'); - db_update('system') - ->fields(array('status' => 0)) - ->condition('type', 'module') - ->condition('name', $module) - ->execute(); - $invoke_modules[] = $module; - watchdog('system', '%module module disabled.', array('%module' => $module), LOG_INFO); - } - } - - if (!empty($invoke_modules)) { - // Refresh the module list to exclude the disabled modules. - system_list_reset(); - module_list(TRUE); - module_implements('', FALSE, TRUE); - // Invoke hook_modules_disabled before disabling modules, - // so we can still call module hooks to get information. - module_invoke_all('modules_disabled', $invoke_modules); - // Update the registry to remove the newly-disabled module. - registry_update(); - _system_update_bootstrap_status(); - } - - // If there remains no more node_access module, rebuilding will be - // straightforward, we can do it right now. - if (node_access_needs_rebuild() && count(module_implements('node_grants')) == 0) { - node_access_rebuild(); - } -} - -/** - * @defgroup hooks Hooks - * @{ - * Allow modules to interact with the Drupal core. - * - * Drupal's module system is based on the concept of "hooks". A hook is a PHP - * function that is named foo_bar(), where "foo" is the name of the module - * (whose filename is thus foo.module) and "bar" is the name of the hook. Each - * hook has a defined set of parameters and a specified result type. - * - * To extend Drupal, a module need simply implement a hook. When Drupal wishes - * to allow intervention from modules, it determines which modules implement a - * hook and calls that hook in all enabled modules that implement it. - * - * The available hooks to implement are explained here in the Hooks section of - * the developer documentation. The string "hook" is used as a placeholder for - * the module name in the hook definitions. For example, if the module file is - * called example.module, then hook_help() as implemented by that module would - * be defined as example_help(). - * - * The example functions included are not part of the Drupal core, they are - * just models that you can modify. Only the hooks implemented within modules - * are executed when running Drupal. - * - * See also @link themeable the themeable group page. @endlink - */ - -/** - * Determine whether a module implements a hook. - * - * @param $module - * The name of the module (without the .module extension). - * @param $hook - * The name of the hook (e.g. "help" or "menu"). - * - * @return - * TRUE if the module is both installed and enabled, and the hook is - * implemented in that module. - */ -function module_hook($module, $hook) { - $function = $module . '_' . $hook; - if (function_exists($function)) { - return TRUE; - } - // If the hook implementation does not exist, check whether it may live in an - // optional include file registered via hook_hook_info(). - $hook_info = module_hook_info(); - if (isset($hook_info[$hook]['group'])) { - module_load_include('inc', $module, $module . '.' . $hook_info[$hook]['group']); - if (function_exists($function)) { - return TRUE; - } - } - return FALSE; -} - -/** - * Determine which modules are implementing a hook. - * - * @param $hook - * The name of the hook (e.g. "help" or "menu"). - * @param $sort - * By default, modules are ordered by weight and filename, settings this option - * to TRUE, module list will be ordered by module name. - * @param $reset - * For internal use only: Whether to force the stored list of hook - * implementations to be regenerated (such as after enabling a new module, - * before processing hook_enable). - * - * @return - * An array with the names of the modules which are implementing this hook. - * - * @see module_implements_write_cache() - */ -function module_implements($hook, $sort = FALSE, $reset = FALSE) { - // Use the advanced drupal_static() pattern, since this is called very often. - static $drupal_static_fast; - if (!isset($drupal_static_fast)) { - $drupal_static_fast['implementations'] = &drupal_static(__FUNCTION__); - } - $implementations = &$drupal_static_fast['implementations']; - - // We maintain a persistent cache of hook implementations in addition to the - // static cache to avoid looping through every module and every hook on each - // request. Benchmarks show that the benefit of this caching outweighs the - // additional database hit even when using the default database caching - // backend and only a small number of modules are enabled. The cost of the - // cache_get() is more or less constant and reduced further when non-database - // caching backends are used, so there will be more significant gains when a - // large number of modules are installed or hooks invoked, since this can - // quickly lead to module_hook() being called several thousand times - // per request. - if ($reset) { - $implementations = array(); - cache_set('module_implements', array(), 'cache_bootstrap'); - drupal_static_reset('module_hook_info'); - drupal_static_reset('drupal_alter'); - cache_clear_all('hook_info', 'cache_bootstrap'); - return; - } - - // Fetch implementations from cache. - if (empty($implementations)) { - $implementations = cache_get('module_implements', 'cache_bootstrap'); - if ($implementations === FALSE) { - $implementations = array(); - } - else { - $implementations = $implementations->data; - } - } - - if (!isset($implementations[$hook])) { - // The hook is not cached, so ensure that whether or not it has - // implementations, that the cache is updated at the end of the request. - $implementations['#write_cache'] = TRUE; - $hook_info = module_hook_info(); - $implementations[$hook] = array(); - $list = module_list(FALSE, FALSE, $sort); - foreach ($list as $module) { - $include_file = isset($hook_info[$hook]['group']) && module_load_include('inc', $module, $module . '.' . $hook_info[$hook]['group']); - // Since module_hook() may needlessly try to load the include file again, - // function_exists() is used directly here. - if (function_exists($module . '_' . $hook)) { - $implementations[$hook][$module] = $include_file ? $hook_info[$hook]['group'] : FALSE; - } - } - // Allow modules to change the weight of specific implementations but avoid - // an infinite loop. - if ($hook != 'module_implements_alter') { - drupal_alter('module_implements', $implementations[$hook], $hook); - } - } - else { - foreach ($implementations[$hook] as $module => $group) { - // If this hook implementation is stored in a lazy-loaded file, so include - // that file first. - if ($group) { - module_load_include('inc', $module, "$module.$group"); - } - // It is possible that a module removed a hook implementation without the - // implementations cache being rebuilt yet, so we check whether the - // function exists on each request to avoid undefined function errors. - // Since module_hook() may needlessly try to load the include file again, - // function_exists() is used directly here. - if (!function_exists($module . '_' . $hook)) { - // Clear out the stale implementation from the cache and force a cache - // refresh to forget about no longer existing hook implementations. - unset($implementations[$hook][$module]); - $implementations['#write_cache'] = TRUE; - } - } - } - - return array_keys($implementations[$hook]); -} - -/** - * Retrieve a list of what hooks are explicitly declared. - */ -function module_hook_info() { - // This function is indirectly invoked from bootstrap_invoke_all(), in which - // case common.inc, subsystems, and modules are not loaded yet, so it does not - // make sense to support hook groups resp. lazy-loaded include files prior to - // full bootstrap. - if (drupal_bootstrap(NULL, FALSE) != DRUPAL_BOOTSTRAP_FULL) { - return array(); - } - $hook_info = &drupal_static(__FUNCTION__); - - if (!isset($hook_info)) { - $hook_info = array(); - $cache = cache_get('hook_info', 'cache_bootstrap'); - if ($cache === FALSE) { - // Rebuild the cache and save it. - // We can't use module_invoke_all() here or it would cause an infinite - // loop. - foreach (module_list() as $module) { - $function = $module . '_hook_info'; - if (function_exists($function)) { - $result = $function(); - if (isset($result) && is_array($result)) { - $hook_info = array_merge_recursive($hook_info, $result); - } - } - } - // We can't use drupal_alter() for the same reason as above. - foreach (module_list() as $module) { - $function = $module . '_hook_info_alter'; - if (function_exists($function)) { - $function($hook_info); - } - } - cache_set('hook_info', $hook_info, 'cache_bootstrap'); - } - else { - $hook_info = $cache->data; - } - } - - return $hook_info; -} - -/** - * Writes the hook implementation cache. - * - * @see module_implements() - */ -function module_implements_write_cache() { - $implementations = &drupal_static('module_implements'); - // Check whether we need to write the cache. We do not want to cache hooks - // which are only invoked on HTTP POST requests since these do not need to be - // optimized as tightly, and not doing so keeps the cache entry smaller. - if (isset($implementations['#write_cache']) && ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD')) { - unset($implementations['#write_cache']); - cache_set('module_implements', $implementations, 'cache_bootstrap'); - } -} - -/** - * Invoke a hook in a particular module. - * - * @param $module - * The name of the module (without the .module extension). - * @param $hook - * The name of the hook to invoke. - * @param ... - * Arguments to pass to the hook implementation. - * - * @return - * The return value of the hook implementation. - */ -function module_invoke() { - $args = func_get_args(); - $module = $args[0]; - $hook = $args[1]; - unset($args[0], $args[1]); - if (module_hook($module, $hook)) { - return call_user_func_array($module . '_' . $hook, $args); - } -} - -/** - * Invoke a hook in all enabled modules that implement it. - * - * @param $hook - * The name of the hook to invoke. - * @param ... - * Arguments to pass to the hook. - * - * @return - * An array of return values of the hook implementations. If modules return - * arrays from their implementations, those are merged into one array. - */ -function module_invoke_all() { - $args = func_get_args(); - $hook = $args[0]; - unset($args[0]); - $return = array(); - foreach (module_implements($hook) as $module) { - $function = $module . '_' . $hook; - if (function_exists($function)) { - $result = call_user_func_array($function, $args); - if (isset($result) && is_array($result)) { - $return = array_merge_recursive($return, $result); - } - elseif (isset($result)) { - $return[] = $result; - } - } - } - - return $return; -} - -/** - * @} End of "defgroup hooks". - */ - -/** - * Array of modules required by core. - */ -function drupal_required_modules() { - $files = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info$/', 'modules', 'name', 0); - $required = array(); - - // An install profile is required and one must always be loaded. - $required[] = drupal_get_profile(); - - foreach ($files as $name => $file) { - $info = drupal_parse_info_file($file->uri); - if (!empty($info) && !empty($info['required']) && $info['required']) { - $required[] = $name; - } - } - - return $required; -} - -/** - * Hands off alterable variables to type-specific *_alter implementations. - * - * This dispatch function hands off the passed-in variables to type-specific - * hook_TYPE_alter() implementations in modules. It ensures a consistent - * interface for all altering operations. - * - * A maximum of 2 alterable arguments is supported. In case more arguments need - * to be passed and alterable, modules provide additional variables assigned by - * reference in the last $context argument: - * @code - * $context = array( - * 'alterable' => &$alterable, - * 'unalterable' => $unalterable, - * 'foo' => 'bar', - * ); - * drupal_alter('mymodule_data', $alterable1, $alterable2, $context); - * @endcode - * - * Note that objects are always passed by reference in PHP5. If it is absolutely - * required that no implementation alters a passed object in $context, then an - * object needs to be cloned: - * @code - * $context = array( - * 'unalterable_object' => clone $object, - * ); - * drupal_alter('mymodule_data', $data, $context); - * @endcode - * - * @param $type - * A string describing the type of the alterable $data. 'form', 'links', - * 'node_content', and so on are several examples. Alternatively can be an - * array, in which case hook_TYPE_alter() is invoked for each value in the - * array, ordered first by module, and then for each module, in the order of - * values in $type. For example, when Form API is using drupal_alter() to - * execute both hook_form_alter() and hook_form_FORM_ID_alter() - * implementations, it passes array('form', 'form_' . $form_id) for $type. - * @param $data - * The variable that will be passed to hook_TYPE_alter() implementations to be - * altered. The type of this variable depends on the value of the $type - * argument. For example, when altering a 'form', $data will be a structured - * array. When altering a 'profile', $data will be an object. - * @param $context1 - * (optional) An additional variable that is passed by reference. - * @param $context2 - * (optional) An additional variable that is passed by reference. If more - * context needs to be provided to implementations, then this should be an - * associative array as described above. - */ -function drupal_alter($type, &$data, &$context1 = NULL, &$context2 = 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['functions'] = &drupal_static(__FUNCTION__); - } - $functions = &$drupal_static_fast['functions']; - - // Most of the time, $type is passed as a string, so for performance, - // normalize it to that. When passed as an array, usually the first item in - // the array is a generic type, and additional items in the array are more - // specific variants of it, as in the case of array('form', 'form_FORM_ID'). - if (is_array($type)) { - $cid = implode(',', $type); - $extra_types = $type; - $type = array_shift($extra_types); - // Allow if statements in this function to use the faster isset() rather - // than !empty() both when $type is passed as a string, or as an array with - // one item. - if (empty($extra_types)) { - unset($extra_types); - } - } - else { - $cid = $type; - } - - // Some alter hooks are invoked many times per page request, so statically - // cache the list of functions to call, and on subsequent calls, iterate - // through them quickly. - if (!isset($functions[$cid])) { - $functions[$cid] = array(); - $hook = $type . '_alter'; - $modules = module_implements($hook); - if (!isset($extra_types)) { - // For the more common case of a single hook, we do not need to call - // function_exists(), since module_implements() returns only modules with - // implementations. - foreach ($modules as $module) { - $functions[$cid][] = $module . '_' . $hook; - } - } - else { - // For multiple hooks, we need $modules to contain every module that - // implements at least one of them. - $extra_modules = array(); - foreach ($extra_types as $extra_type) { - $extra_modules = array_merge($extra_modules, module_implements($extra_type . '_alter')); - } - // If any modules implement one of the extra hooks that do not implement - // the primary hook, we need to add them to the $modules array in their - // appropriate order. - if (array_diff($extra_modules, $modules)) { - // Order the modules by the order returned by module_list(). - $modules = array_intersect(module_list(), array_merge($modules, $extra_modules)); - } - foreach ($modules as $module) { - // Since $modules is a merged array, for any given module, we do not - // know whether it has any particular implementation, so we need a - // function_exists(). - $function = $module . '_' . $hook; - if (function_exists($function)) { - $functions[$cid][] = $function; - } - foreach ($extra_types as $extra_type) { - $function = $module . '_' . $extra_type . '_alter'; - if (function_exists($function)) { - $functions[$cid][] = $function; - } - } - } - } - // Allow the theme to alter variables after the theme system has been - // initialized. - global $theme, $base_theme_info; - if (isset($theme)) { - $theme_keys = array(); - foreach ($base_theme_info as $base) { - $theme_keys[] = $base->name; - } - $theme_keys[] = $theme; - foreach ($theme_keys as $theme_key) { - $function = $theme_key . '_' . $hook; - if (function_exists($function)) { - $functions[$cid][] = $function; - } - if (isset($extra_types)) { - foreach ($extra_types as $extra_type) { - $function = $theme_key . '_' . $extra_type . '_alter'; - if (function_exists($function)) { - $functions[$cid][] = $function; - } - } - } - } - } - } - - foreach ($functions[$cid] as $function) { - $function($data, $context1, $context2); - } -} - diff --git a/includes/registry.inc b/includes/registry.inc deleted file mode 100644 index 3fb14fb..0000000 --- a/includes/registry.inc +++ /dev/null @@ -1,186 +0,0 @@ -fetchAll(); - // Get the list of files we are going to parse. - $files = array(); - foreach ($modules as &$module) { - $module->info = unserialize($module->info); - $dir = dirname($module->filename); - - // Store the module directory for use in hook_registry_files_alter(). - $module->dir = $dir; - - if ($module->status) { - // Add files for enabled modules to the registry. - foreach ($module->info['files'] as $file) { - $files["$dir/$file"] = array('module' => $module->name, 'weight' => $module->weight); - } - } - } - foreach (file_scan_directory('includes', '/\.inc$/') as $filename => $file) { - $files["$filename"] = array('module' => '', 'weight' => 0); - } - - $transaction = db_transaction(); - try { - // Allow modules to manually modify the list of files before the registry - // parses them. The $modules array provides the .info file information, which - // includes the list of files registered to each module. Any files in the - // list can then be added to the list of files that the registry will parse, - // or modify attributes of a file. - drupal_alter('registry_files', $files, $modules); - foreach (registry_get_parsed_files() as $filename => $file) { - // Add the hash for those files we have already parsed. - if (isset($files[$filename])) { - $files[$filename]['hash'] = $file['hash']; - } - else { - // Flush the registry of resources in files that are no longer on disc - // or are in files that no installed modules require to be parsed. - db_delete('registry') - ->condition('filename', $filename) - ->execute(); - db_delete('registry_file') - ->condition('filename', $filename) - ->execute(); - } - } - $parsed_files = _registry_parse_files($files); - - $unchanged_resources = array(); - $lookup_cache = array(); - if ($cache = cache_get('lookup_cache', 'cache_bootstrap')) { - $lookup_cache = $cache->data; - } - foreach ($lookup_cache as $key => $file) { - // If the file for this cached resource is carried over unchanged from - // the last registry build, then we can safely re-cache it. - if ($file && in_array($file, array_keys($files)) && !in_array($file, $parsed_files)) { - $unchanged_resources[$key] = $file; - } - } - module_implements('', FALSE, TRUE); - _registry_check_code(REGISTRY_RESET_LOOKUP_CACHE); - } - catch (Exception $e) { - $transaction->rollback(); - watchdog_exception('registry', $e); - throw $e; - } - - // We have some unchanged resources, warm up the cache - no need to pay - // for looking them up again. - if (count($unchanged_resources) > 0) { - cache_set('lookup_cache', $unchanged_resources, 'cache_bootstrap'); - } -} - -/** - * Return the list of files in registry_file - */ -function registry_get_parsed_files() { - $files = array(); - // We want the result as a keyed array. - $files = db_query("SELECT * FROM {registry_file}")->fetchAllAssoc('filename', PDO::FETCH_ASSOC); - return $files; -} - -/** - * Parse all files that have changed since the registry was last built, and save their function and class listings. - * - * @param $files - * The list of files to check and parse. - */ -function _registry_parse_files($files) { - $parsed_files = array(); - foreach ($files as $filename => $file) { - if (file_exists($filename)) { - $hash = hash_file('sha256', $filename); - if (empty($file['hash']) || $file['hash'] != $hash) { - // Delete registry entries for this file, so we can insert the new resources. - db_delete('registry') - ->condition('filename', $filename) - ->execute(); - $file['hash'] = $hash; - $parsed_files[$filename] = $file; - } - } - } - foreach ($parsed_files as $filename => $file) { - _registry_parse_file($filename, file_get_contents($filename), $file['module'], $file['weight']); - db_merge('registry_file') - ->key(array('filename' => $filename)) - ->fields(array( - 'hash' => $file['hash'], - )) - ->execute(); - } - return array_keys($parsed_files); -} - -/** - * Parse a file and save its function and class listings. - * - * @param $filename - * Name of the file we are going to parse. - * @param $contents - * Contents of the file we are going to parse as a string. - * @param $module - * (optional) Name of the module this file belongs to. - * @param $weight - * (optional) Weight of the module. - */ -function _registry_parse_file($filename, $contents, $module = '', $weight = 0) { - if (preg_match_all('/^\s*(?:abstract|final)?\s*(class|interface)\s+([a-zA-Z0-9_]+)/m', $contents, $matches)) { - $query = db_insert('registry')->fields(array('name', 'type', 'filename', 'module', 'weight')); - foreach ($matches[2] as $key => $name) { - $query->values(array( - 'name' => $name, - 'type' => $matches[1][$key], - 'filename' => $filename, - 'module' => $module, - 'weight' => $weight, - )); - } - $query->execute(); - } -} - -/** - * @} End of "defgroup registry". - */ - diff --git a/includes/session.inc b/includes/session.inc deleted file mode 100644 index 2ede2ff..0000000 --- a/includes/session.inc +++ /dev/null @@ -1,490 +0,0 @@ - $sid))->fetchObject(); - if (!$user) { - if (isset($_COOKIE[$insecure_session_name])) { - $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid AND s.uid = 0", array( - ':sid' => $_COOKIE[$insecure_session_name])) - ->fetchObject(); - } - } - } - else { - $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => $sid))->fetchObject(); - } - - // We found the client's session record and they are an authenticated, - // active user. - if ($user && $user->uid > 0 && $user->status == 1) { - // This is done to unserialize the data member of $user. - $user->data = unserialize($user->data); - - // Add roles element to $user. - $user->roles = array(); - $user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user'; - $user->roles += db_query("SELECT r.rid, r.name FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid = :uid", array(':uid' => $user->uid))->fetchAllKeyed(0, 1); - } - elseif ($user) { - // The user is anonymous or blocked. Only preserve two fields from the - // {sessions} table. - $account = drupal_anonymous_user(); - $account->session = $user->session; - $account->timestamp = $user->timestamp; - $user = $account; - } - else { - // The session has expired. - $user = drupal_anonymous_user(); - $user->session = ''; - } - - // Store the session that was read for comparison in _drupal_session_write(). - $last_read = &drupal_static('drupal_session_last_read'); - $last_read = array( - 'sid' => $sid, - 'value' => $user->session, - ); - - return $user->session; -} - -/** - * Session handler assigned by session_set_save_handler(). - * - * This function will be called by PHP to store the current user's - * session, which Drupal saves to the database. - * - * This function should not be called directly. Session data should - * instead be accessed via the $_SESSION superglobal. - * - * @param $sid - * Session ID. - * @param $value - * Serialized array of the session data. - * - * @return - * This function will always return TRUE. - */ -function _drupal_session_write($sid, $value) { - global $user, $is_https; - - // The exception handler is not active at this point, so we need to do it - // manually. - try { - if (!drupal_save_session()) { - // We don't have anything to do if we are not allowed to save the session. - return; - } - - // Check whether $_SESSION has been changed in this request. - $last_read = &drupal_static('drupal_session_last_read'); - $is_changed = !isset($last_read) || $last_read['sid'] != $sid || $last_read['value'] !== $value; - - // For performance reasons, do not update the sessions table, unless - // $_SESSION has changed or more than 180 has passed since the last update. - if ($is_changed || REQUEST_TIME - $user->timestamp > variable_get('session_write_interval', 180)) { - // Either ssid or sid or both will be added from $key below. - $fields = array( - 'uid' => $user->uid, - 'cache' => isset($user->cache) ? $user->cache : 0, - 'hostname' => ip_address(), - 'session' => $value, - 'timestamp' => REQUEST_TIME, - ); - - // Use the session ID as 'sid' and an empty string as 'ssid' by default. - // _drupal_session_read() does not allow empty strings so that's a safe - // default. - $key = array('sid' => $sid, 'ssid' => ''); - // On HTTPS connections, use the session ID as both 'sid' and 'ssid'. - if ($is_https) { - $key['ssid'] = $sid; - // The "secure pages" setting allows a site to simultaneously use both - // secure and insecure session cookies. If enabled and both cookies are - // presented then use both keys. - if (variable_get('https', FALSE)) { - $insecure_session_name = substr(session_name(), 1); - if (isset($_COOKIE[$insecure_session_name])) { - $key['sid'] = $_COOKIE[$insecure_session_name]; - } - } - } - - db_merge('sessions') - ->key($key) - ->fields($fields) - ->execute(); - } - - // Likewise, do not update access time more than once per 180 seconds. - if ($user->uid && REQUEST_TIME - $user->access > variable_get('session_write_interval', 180)) { - db_update('users') - ->fields(array( - 'access' => REQUEST_TIME - )) - ->condition('uid', $user->uid) - ->execute(); - } - - return TRUE; - } - catch (Exception $exception) { - require_once DRUPAL_ROOT . '/includes/errors.inc'; - // If we are displaying errors, then do so with no possibility of a further - // uncaught exception being thrown. - if (error_displayable()) { - print '

    Uncaught exception thrown in session handler.

    '; - print '

    ' . _drupal_render_exception_safe($exception) . '


    '; - } - return FALSE; - } -} - -/** - * Initializes the session handler, starting a session if needed. - */ -function drupal_session_initialize() { - global $user, $is_https; - - session_set_save_handler('_drupal_session_open', '_drupal_session_close', '_drupal_session_read', '_drupal_session_write', '_drupal_session_destroy', '_drupal_session_garbage_collection'); - - // We use !empty() in the following check to ensure that blank session IDs - // are not valid. - if (!empty($_COOKIE[session_name()]) || ($is_https && variable_get('https', FALSE) && !empty($_COOKIE[substr(session_name(), 1)]))) { - // If a session cookie exists, initialize the session. Otherwise the - // session is only started on demand in drupal_session_commit(), making - // anonymous users not use a session cookie unless something is stored in - // $_SESSION. This allows HTTP proxies to cache anonymous pageviews. - drupal_session_start(); - if (!empty($user->uid) || !empty($_SESSION)) { - drupal_page_is_cacheable(FALSE); - } - } - else { - // Set a session identifier for this request. This is necessary because - // we lazily start sessions at the end of this request, and some - // processes (like drupal_get_token()) needs to know the future - // session ID in advance. - $user = drupal_anonymous_user(); - // Less random sessions (which are much faster to generate) are used for - // anonymous users than are generated in drupal_session_regenerate() when - // a user becomes authenticated. - session_id(drupal_hash_base64(uniqid(mt_rand(), TRUE))); - } - date_default_timezone_set(drupal_get_user_timezone()); -} - -/** - * Forcefully starts a session, preserving already set session data. - * - * @ingroup php_wrappers - */ -function drupal_session_start() { - // Command line clients do not support cookies nor sessions. - if (!drupal_session_started() && !drupal_is_cli()) { - // Save current session data before starting it, as PHP will destroy it. - $session_data = isset($_SESSION) ? $_SESSION : NULL; - - session_start(); - drupal_session_started(TRUE); - - // Restore session data. - if (!empty($session_data)) { - $_SESSION += $session_data; - } - } -} - -/** - * Commits the current session, if necessary. - * - * If an anonymous user already have an empty session, destroy it. - */ -function drupal_session_commit() { - global $user; - - if (!drupal_save_session()) { - // We don't have anything to do if we are not allowed to save the session. - return; - } - - if (empty($user->uid) && empty($_SESSION)) { - // There is no session data to store, destroy the session if it was - // previously started. - if (drupal_session_started()) { - session_destroy(); - } - } - else { - // There is session data to store. Start the session if it is not already - // started. - if (!drupal_session_started()) { - drupal_session_start(); - } - // Write the session data. - session_write_close(); - } -} - -/** - * Returns whether a session has been started. - */ -function drupal_session_started($set = NULL) { - static $session_started = FALSE; - if (isset($set)) { - $session_started = $set; - } - return $session_started && session_id(); -} - -/** - * Called when an anonymous user becomes authenticated or vice-versa. - * - * @ingroup php_wrappers - */ -function drupal_session_regenerate() { - global $user, $is_https; - if ($is_https && variable_get('https', FALSE)) { - $insecure_session_name = substr(session_name(), 1); - if (isset($_COOKIE[$insecure_session_name])) { - $old_insecure_session_id = $_COOKIE[$insecure_session_name]; - } - $params = session_get_cookie_params(); - $session_id = drupal_hash_base64(uniqid(mt_rand(), TRUE) . drupal_random_bytes(55)); - // If a session cookie lifetime is set, the session will expire - // $params['lifetime'] seconds from the current request. If it is not set, - // it will expire when the browser is closed. - $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; - setcookie($insecure_session_name, $session_id, $expire, $params['path'], $params['domain'], FALSE, $params['httponly']); - $_COOKIE[$insecure_session_name] = $session_id; - } - - if (drupal_session_started()) { - $old_session_id = session_id(); - } - session_id(drupal_hash_base64(uniqid(mt_rand(), TRUE) . drupal_random_bytes(55))); - - if (isset($old_session_id)) { - $params = session_get_cookie_params(); - $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; - setcookie(session_name(), session_id(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']); - $fields = array('sid' => session_id()); - if ($is_https) { - $fields['ssid'] = session_id(); - // If the "secure pages" setting is enabled, use the newly-created - // insecure session identifier as the regenerated sid. - if (variable_get('https', FALSE)) { - $fields['sid'] = $session_id; - } - } - db_update('sessions') - ->fields($fields) - ->condition($is_https ? 'ssid' : 'sid', $old_session_id) - ->execute(); - } - elseif (isset($old_insecure_session_id)) { - // If logging in to the secure site, and there was no active session on the - // secure site but a session was active on the insecure site, update the - // insecure session with the new session identifiers. - db_update('sessions') - ->fields(array('sid' => $session_id, 'ssid' => session_id())) - ->condition('sid', $old_insecure_session_id) - ->execute(); - } - else { - // Start the session when it doesn't exist yet. - // Preserve the logged in user, as it will be reset to anonymous - // by _drupal_session_read. - $account = $user; - drupal_session_start(); - $user = $account; - } - date_default_timezone_set(drupal_get_user_timezone()); -} - -/** - * Session handler assigned by session_set_save_handler(). - * - * Cleans up a specific session. - * - * @param $sid - * Session ID. - */ -function _drupal_session_destroy($sid) { - global $user, $is_https; - - // Delete session data. - db_delete('sessions') - ->condition($is_https ? 'ssid' : 'sid', $sid) - ->execute(); - - // Reset $_SESSION and $user to prevent a new session from being started - // in drupal_session_commit(). - $_SESSION = array(); - $user = drupal_anonymous_user(); - - // Unset the session cookies. - _drupal_session_delete_cookie(session_name()); - if ($is_https) { - _drupal_session_delete_cookie(substr(session_name(), 1), TRUE); - } -} - -/** - * Deletes the session cookie. - * - * @param $name - * Name of session cookie to delete. - * @param $force_insecure - * Force cookie to be insecure. - */ -function _drupal_session_delete_cookie($name, $force_insecure = FALSE) { - if (isset($_COOKIE[$name])) { - $params = session_get_cookie_params(); - setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], !$force_insecure && $params['secure'], $params['httponly']); - unset($_COOKIE[$name]); - } -} - -/** - * Ends a specific user's session(s). - * - * @param $uid - * User ID. - */ -function drupal_session_destroy_uid($uid) { - db_delete('sessions') - ->condition('uid', $uid) - ->execute(); -} - -/** - * Session handler assigned by session_set_save_handler(). - * - * Cleans up stalled sessions. - * - * @param $lifetime - * The value of session.gc_maxlifetime, passed by PHP. - * Sessions not updated for more than $lifetime seconds will be removed. - */ -function _drupal_session_garbage_collection($lifetime) { - // Be sure to adjust 'php_value session.gc_maxlifetime' to a large enough - // value. For example, if you want user sessions to stay in your database - // for three weeks before deleting them, you need to set gc_maxlifetime - // to '1814400'. At that value, only after a user doesn't log in after - // three weeks (1814400 seconds) will his/her session be removed. - db_delete('sessions') - ->condition('timestamp', REQUEST_TIME - $lifetime, '<') - ->execute(); - return TRUE; -} - -/** - * Determines whether to save session data of the current request. - * - * This function allows the caller to temporarily disable writing of - * session data, should the request end while performing potentially - * dangerous operations, such as manipulating the global $user object. - * See http://drupal.org/node/218104 for usage. - * - * @param $status - * Disables writing of session data when FALSE, (re-)enables - * writing when TRUE. - * - * @return - * FALSE if writing session data has been disabled. Otherwise, TRUE. - */ -function drupal_save_session($status = NULL) { - $save_session = &drupal_static(__FUNCTION__, TRUE); - if (isset($status)) { - $save_session = $status; - } - return $save_session; -} diff --git a/includes/stream_wrappers.inc b/includes/stream_wrappers.inc deleted file mode 100644 index 7df1f9d..0000000 --- a/includes/stream_wrappers.inc +++ /dev/null @@ -1,824 +0,0 @@ -uri = $uri; - } - - /** - * Base implementation of getUri(). - */ - function getUri() { - return $this->uri; - } - - /** - * Returns the local writable target of the resource within the stream. - * - * This function should be used in place of calls to realpath() or similar - * functions when attempting to determine the location of a file. While - * functions like realpath() may return the location of a read-only file, this - * method may return a URI or path suitable for writing that is completely - * separate from the URI used for reading. - * - * @param $uri - * Optional URI. - * - * @return - * Returns a string representing a location suitable for writing of a file, - * or FALSE if unable to write to the file such as with read-only streams. - */ - protected function getTarget($uri = NULL) { - if (!isset($uri)) { - $uri = $this->uri; - } - - list($scheme, $target) = explode('://', $uri, 2); - - // Remove erroneous leading or trailing, forward-slashes and backslashes. - return trim($target, '\/'); - } - - /** - * Base implementation of getMimeType(). - */ - static function getMimeType($uri, $mapping = NULL) { - if (!isset($mapping)) { - // The default file map, defined in file.mimetypes.inc is quite big. - // We only load it when necessary. - include_once DRUPAL_ROOT . '/includes/file.mimetypes.inc'; - $mapping = file_mimetype_mapping(); - } - - $extension = ''; - $file_parts = explode('.', basename($uri)); - - // Remove the first part: a full filename should not match an extension. - array_shift($file_parts); - - // Iterate over the file parts, trying to find a match. - // For my.awesome.image.jpeg, we try: - // - jpeg - // - image.jpeg, and - // - awesome.image.jpeg - while ($additional_part = array_pop($file_parts)) { - $extension = strtolower($additional_part . ($extension ? '.' . $extension : '')); - if (isset($mapping['extensions'][$extension])) { - return $mapping['mimetypes'][$mapping['extensions'][$extension]]; - } - } - - return 'application/octet-stream'; - } - - /** - * Base implementation of chmod(). - */ - function chmod($mode) { - return @chmod($this->getLocalPath(), $mode); - } - - /** - * Base implementation of realpath(). - */ - function realpath() { - return $this->getLocalPath(); - } - - /** - * Return the local filesystem path. - * - * @param $uri - * Optional URI, supplied when doing a move or rename. - */ - protected function getLocalPath($uri = NULL) { - if (!isset($uri)) { - $uri = $this->uri; - } - $path = $this->getDirectoryPath() . '/' . $this->getTarget($uri); - $realpath = realpath($path); - if (!$realpath) { - // This file does not yet exist. - $realpath = realpath(dirname($path)) . '/' . basename($path); - } - $directory = realpath($this->getDirectoryPath()); - if (!$realpath || !$directory || strpos($realpath, $directory) !== 0) { - return FALSE; - } - return $realpath; - } - - /** - * Support for fopen(), file_get_contents(), file_put_contents() etc. - * - * @param $uri - * A string containing the URI to the file to open. - * @param $mode - * The file mode ("r", "wb" etc.). - * @param $options - * A bit mask of STREAM_USE_PATH and STREAM_REPORT_ERRORS. - * @param $opened_path - * A string containing the path actually opened. - * - * @return - * Returns TRUE if file was opened successfully. - * - * @see http://php.net/manual/en/streamwrapper.stream-open.php - */ - public function stream_open($uri, $mode, $options, &$opened_path) { - $this->uri = $uri; - $path = $this->getLocalPath(); - $this->handle = ($options & STREAM_REPORT_ERRORS) ? fopen($path, $mode) : @fopen($path, $mode); - - if ((bool) $this->handle && $options & STREAM_USE_PATH) { - $opened_url = $path; - } - - return (bool) $this->handle; - } - - /** - * Support for flock(). - * - * @param $operation - * One of the following: - * - LOCK_SH to acquire a shared lock (reader). - * - LOCK_EX to acquire an exclusive lock (writer). - * - LOCK_UN to release a lock (shared or exclusive). - * - LOCK_NB if you don't want flock() to block while locking (not - * supported on Windows). - * - * @return - * Always returns TRUE at the present time. - * - * @see http://php.net/manual/en/streamwrapper.stream-lock.php - */ - public function stream_lock($operation) { - if (in_array($operation, array(LOCK_SH, LOCK_EX, LOCK_UN, LOCK_NB))) { - return flock($this->handle, $operation); - } - - return TRUE; - } - - /** - * Support for fread(), file_get_contents() etc. - * - * @param $count - * Maximum number of bytes to be read. - * - * @return - * The string that was read, or FALSE in case of an error. - * - * @see http://php.net/manual/en/streamwrapper.stream-read.php - */ - public function stream_read($count) { - return fread($this->handle, $count); - } - - /** - * Support for fwrite(), file_put_contents() etc. - * - * @param $data - * The string to be written. - * - * @return - * The number of bytes written (integer). - * - * @see http://php.net/manual/en/streamwrapper.stream-write.php - */ - public function stream_write($data) { - return fwrite($this->handle, $data); - } - - /** - * Support for feof(). - * - * @return - * TRUE if end-of-file has been reached. - * - * @see http://php.net/manual/en/streamwrapper.stream-eof.php - */ - public function stream_eof() { - return feof($this->handle); - } - - /** - * Support for fseek(). - * - * @param $offset - * The byte offset to got to. - * @param $whence - * SEEK_SET, SEEK_CUR, or SEEK_END. - * - * @return - * TRUE on success. - * - * @see http://php.net/manual/en/streamwrapper.stream-seek.php - */ - public function stream_seek($offset, $whence) { - // fseek returns 0 on success and -1 on a failure. - // stream_seek 1 on success and 0 on a failure. - return !fseek($this->handle, $offset, $whence); - } - - /** - * Support for fflush(). - * - * @return - * TRUE if data was successfully stored (or there was no data to store). - * - * @see http://php.net/manual/en/streamwrapper.stream-flush.php - */ - public function stream_flush() { - return fflush($this->handle); - } - - /** - * Support for ftell(). - * - * @return - * The current offset in bytes from the beginning of file. - * - * @see http://php.net/manual/en/streamwrapper.stream-tell.php - */ - public function stream_tell() { - return ftell($this->handle); - } - - /** - * Support for fstat(). - * - * @return - * An array with file status, or FALSE in case of an error - see fstat() - * for a description of this array. - * - * @see http://php.net/manual/en/streamwrapper.stream-stat.php - */ - public function stream_stat() { - return fstat($this->handle); - } - - /** - * Support for fclose(). - * - * @return - * TRUE if stream was successfully closed. - * - * @see http://php.net/manual/en/streamwrapper.stream-close.php - */ - public function stream_close() { - return fclose($this->handle); - } - - /** - * Support for unlink(). - * - * @param $uri - * A string containing the uri to the resource to delete. - * - * @return - * TRUE if resource was successfully deleted. - * - * @see http://php.net/manual/en/streamwrapper.unlink.php - */ - public function unlink($uri) { - $this->uri = $uri; - return drupal_unlink($this->getLocalPath()); - } - - /** - * Support for rename(). - * - * @param $from_uri, - * The uri to the file to rename. - * @param $to_uri - * The new uri for file. - * - * @return - * TRUE if file was successfully renamed. - * - * @see http://php.net/manual/en/streamwrapper.rename.php - */ - public function rename($from_uri, $to_uri) { - return rename($this->getLocalPath($from_uri), $this->getLocalPath($to_uri)); - } - - /** - * Gets the name of the directory from a given path. - * - * This method is usually accessed through drupal_dirname(), which wraps - * around the PHP dirname() function because it does not support stream - * wrappers. - * - * @param $uri - * A URI or path. - * - * @return - * A string containing the directory name. - * - * @see drupal_dirname() - */ - public function dirname($uri = NULL) { - list($scheme, $target) = explode('://', $uri, 2); - $target = $this->getTarget($uri); - $dirname = dirname($target); - - if ($dirname == '.') { - $dirname = ''; - } - - return $scheme . '://' . $dirname; - } - - /** - * Support for mkdir(). - * - * @param $uri - * A string containing the URI to the directory to create. - * @param $mode - * Permission flags - see mkdir(). - * @param $options - * A bit mask of STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE. - * - * @return - * TRUE if directory was successfully created. - * - * @see http://php.net/manual/en/streamwrapper.mkdir.php - */ - public function mkdir($uri, $mode, $options) { - $this->uri = $uri; - $recursive = (bool) ($options & STREAM_MKDIR_RECURSIVE); - if ($recursive) { - // $this->getLocalPath() fails if $uri has multiple levels of directories - // that do not yet exist. - $localpath = $this->getDirectoryPath() . '/' . $this->getTarget($uri); - } - else { - $localpath = $this->getLocalPath($uri); - } - if ($options & STREAM_REPORT_ERRORS) { - return mkdir($localpath, $mode, $recursive); - } - else { - return @mkdir($localpath, $mode, $recursive); - } - } - - /** - * Support for rmdir(). - * - * @param $uri - * A string containing the URI to the directory to delete. - * @param $options - * A bit mask of STREAM_REPORT_ERRORS. - * - * @return - * TRUE if directory was successfully removed. - * - * @see http://php.net/manual/en/streamwrapper.rmdir.php - */ - public function rmdir($uri, $options) { - $this->uri = $uri; - if ($options & STREAM_REPORT_ERRORS) { - return drupal_rmdir($this->getLocalPath()); - } - else { - return @drupal_rmdir($this->getLocalPath()); - } - } - - /** - * Support for stat(). - * - * @param $uri - * A string containing the URI to get information about. - * @param $flags - * A bit mask of STREAM_URL_STAT_LINK and STREAM_URL_STAT_QUIET. - * - * @return - * An array with file status, or FALSE in case of an error - see fstat() - * for a description of this array. - * - * @see http://php.net/manual/en/streamwrapper.url-stat.php - */ - public function url_stat($uri, $flags) { - $this->uri = $uri; - $path = $this->getLocalPath(); - // Suppress warnings if requested or if the file or directory does not - // exist. This is consistent with PHP's plain filesystem stream wrapper. - if ($flags & STREAM_URL_STAT_QUIET || !file_exists($path)) { - return @stat($path); - } - else { - return stat($path); - } - } - - /** - * Support for opendir(). - * - * @param $uri - * A string containing the URI to the directory to open. - * @param $options - * Unknown (parameter is not documented in PHP Manual). - * - * @return - * TRUE on success. - * - * @see http://php.net/manual/en/streamwrapper.dir-opendir.php - */ - public function dir_opendir($uri, $options) { - $this->uri = $uri; - $this->handle = opendir($this->getLocalPath()); - - return (bool) $this->handle; - } - - /** - * Support for readdir(). - * - * @return - * The next filename, or FALSE if there are no more files in the directory. - * - * @see http://php.net/manual/en/streamwrapper.dir-readdir.php - */ - public function dir_readdir() { - return readdir($this->handle); - } - - /** - * Support for rewinddir(). - * - * @return - * TRUE on success. - * - * @see http://php.net/manual/en/streamwrapper.dir-rewinddir.php - */ - public function dir_rewinddir() { - rewinddir($this->handle); - // We do not really have a way to signal a failure as rewinddir() does not - // have a return value and there is no way to read a directory handler - // without advancing to the next file. - return TRUE; - } - - /** - * Support for closedir(). - * - * @return - * TRUE on success. - * - * @see http://php.net/manual/en/streamwrapper.dir-closedir.php - */ - public function dir_closedir() { - closedir($this->handle); - // We do not really have a way to signal a failure as closedir() does not - // have a return value. - return TRUE; - } -} - -/** - * Drupal public (public://) stream wrapper class. - * - * Provides support for storing publicly accessible files with the Drupal file - * interface. - */ -class DrupalPublicStreamWrapper extends DrupalLocalStreamWrapper { - /** - * Implements abstract public function getDirectoryPath() - */ - public function getDirectoryPath() { - return variable_get('file_public_path', conf_path() . '/files'); - } - - /** - * Overrides getExternalUrl(). - * - * Return the HTML URI of a public file. - */ - function getExternalUrl() { - $path = str_replace('\\', '/', $this->getTarget()); - return $GLOBALS['base_url'] . '/' . self::getDirectoryPath() . '/' . drupal_encode_path($path); - } -} - - -/** - * Drupal private (private://) stream wrapper class. - * - * Provides support for storing privately accessible files with the Drupal file - * interface. - * - * Extends DrupalPublicStreamWrapper. - */ -class DrupalPrivateStreamWrapper extends DrupalLocalStreamWrapper { - /** - * Implements abstract public function getDirectoryPath() - */ - public function getDirectoryPath() { - return variable_get('file_private_path', ''); - } - - /** - * Overrides getExternalUrl(). - * - * Return the HTML URI of a private file. - */ - function getExternalUrl() { - $path = str_replace('\\', '/', $this->getTarget()); - return url('system/files/' . $path, array('absolute' => TRUE)); - } -} - -/** - * Drupal temporary (temporary://) stream wrapper class. - * - * Provides support for storing temporarily accessible files with the Drupal - * file interface. - * - * Extends DrupalPublicStreamWrapper. - */ -class DrupalTemporaryStreamWrapper extends DrupalLocalStreamWrapper { - /** - * Implements abstract public function getDirectoryPath() - */ - public function getDirectoryPath() { - return variable_get('file_temporary_path', file_directory_temp()); - } - - /** - * Overrides getExternalUrl(). - */ - public function getExternalUrl() { - $path = str_replace('\\', '/', $this->getTarget()); - return url('system/temporary/' . $path, array('absolute' => TRUE)); - } -} diff --git a/includes/theme.inc b/includes/theme.inc deleted file mode 100644 index c211248..0000000 --- a/includes/theme.inc +++ /dev/null @@ -1,2616 +0,0 @@ -status) || ($admin_theme && $theme->name == $admin_theme); -} - -/** - * Initialize the theme system by loading the theme. - */ -function drupal_theme_initialize() { - global $theme, $user, $theme_key; - - // If $theme is already set, assume the others are set, too, and do nothing - if (isset($theme)) { - return; - } - - drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE); - $themes = list_themes(); - - // Only select the user selected theme if it is available in the - // list of themes that can be accessed. - $theme = !empty($user->theme) && drupal_theme_access($user->theme) ? $user->theme : variable_get('theme_default', 'bartik'); - - // Allow modules to override the theme. Validation has already been performed - // inside menu_get_custom_theme(), so we do not need to check it again here. - $custom_theme = menu_get_custom_theme(); - $theme = !empty($custom_theme) ? $custom_theme : $theme; - - // Store the identifier for retrieving theme settings with. - $theme_key = $theme; - - // Find all our ancestor themes and put them in an array. - $base_theme = array(); - $ancestor = $theme; - while ($ancestor && isset($themes[$ancestor]->base_theme)) { - $ancestor = $themes[$ancestor]->base_theme; - $base_theme[] = $themes[$ancestor]; - } - _drupal_theme_initialize($themes[$theme], array_reverse($base_theme)); - - // Themes can have alter functions, so reset the drupal_alter() cache. - drupal_static_reset('drupal_alter'); - - // Provide the page with information about the theme that's used, so that a - // later Ajax request can be rendered using the same theme. - // @see ajax_base_page_theme() - $setting['ajaxPageState'] = array( - 'theme' => $theme_key, - 'theme_token' => drupal_get_token($theme_key), - ); - drupal_add_js($setting, 'setting'); -} - -/** - * Initialize the theme system given already loaded information. This - * function is useful to initialize a theme when no database is present. - * - * @param $theme - * An object with the following information: - * filename - * The .info file for this theme. The 'path' to - * the theme will be in this file's directory. (Required) - * owner - * The path to the .theme file or the .engine file to load for - * the theme. (Required) - * stylesheet - * The primary stylesheet for the theme. (Optional) - * engine - * The name of theme engine to use. (Optional) - * @param $base_theme - * An optional array of objects that represent the 'base theme' if the - * theme is meant to be derivative of another theme. It requires - * the same information as the $theme object. It should be in - * 'oldest first' order, meaning the top level of the chain will - * be first. - * @param $registry_callback - * The callback to invoke to set the theme registry. - */ -function _drupal_theme_initialize($theme, $base_theme = array(), $registry_callback = '_theme_load_registry') { - global $theme_info, $base_theme_info, $theme_engine, $theme_path; - $theme_info = $theme; - $base_theme_info = $base_theme; - - $theme_path = dirname($theme->filename); - - // Prepare stylesheets from this theme as well as all ancestor themes. - // We work it this way so that we can have child themes override parent - // theme stylesheets easily. - $final_stylesheets = array(); - - // Grab stylesheets from base theme - foreach ($base_theme as $base) { - if (!empty($base->stylesheets)) { - foreach ($base->stylesheets as $media => $stylesheets) { - foreach ($stylesheets as $name => $stylesheet) { - $final_stylesheets[$media][$name] = $stylesheet; - } - } - } - } - - // Add stylesheets used by this theme. - if (!empty($theme->stylesheets)) { - foreach ($theme->stylesheets as $media => $stylesheets) { - foreach ($stylesheets as $name => $stylesheet) { - $final_stylesheets[$media][$name] = $stylesheet; - } - } - } - - // And now add the stylesheets properly - foreach ($final_stylesheets as $media => $stylesheets) { - foreach ($stylesheets as $stylesheet) { - drupal_add_css($stylesheet, array('group' => CSS_THEME, 'every_page' => TRUE, 'media' => $media)); - } - } - - // Do basically the same as the above for scripts - $final_scripts = array(); - - // Grab scripts from base theme - foreach ($base_theme as $base) { - if (!empty($base->scripts)) { - foreach ($base->scripts as $name => $script) { - $final_scripts[$name] = $script; - } - } - } - - // Add scripts used by this theme. - if (!empty($theme->scripts)) { - foreach ($theme->scripts as $name => $script) { - $final_scripts[$name] = $script; - } - } - - // Add scripts used by this theme. - foreach ($final_scripts as $script) { - drupal_add_js($script, array('group' => JS_THEME, 'every_page' => TRUE)); - } - - $theme_engine = NULL; - - // Initialize the theme. - if (isset($theme->engine)) { - // Include the engine. - include_once DRUPAL_ROOT . '/' . $theme->owner; - - $theme_engine = $theme->engine; - if (function_exists($theme_engine . '_init')) { - foreach ($base_theme as $base) { - call_user_func($theme_engine . '_init', $base); - } - call_user_func($theme_engine . '_init', $theme); - } - } - else { - // include non-engine theme files - foreach ($base_theme as $base) { - // Include the theme file or the engine. - if (!empty($base->owner)) { - include_once DRUPAL_ROOT . '/' . $base->owner; - } - } - // and our theme gets one too. - if (!empty($theme->owner)) { - include_once DRUPAL_ROOT . '/' . $theme->owner; - } - } - - if (isset($registry_callback)) { - _theme_registry_callback($registry_callback, array($theme, $base_theme, $theme_engine)); - } -} - -/** - * Get the theme registry. - * - * @return - * The theme registry array if it has been stored in memory, NULL otherwise. - */ -function theme_get_registry() { - static $theme_registry = NULL; - - if (!isset($theme_registry)) { - list($callback, $arguments) = _theme_registry_callback(); - $theme_registry = call_user_func_array($callback, $arguments); - } - - return $theme_registry; -} - -/** - * Set the callback that will be used by theme_get_registry() to fetch the registry. - * - * @param $callback - * The name of the callback function. - * @param $arguments - * The arguments to pass to the function. - */ -function _theme_registry_callback($callback = NULL, array $arguments = array()) { - static $stored; - if (isset($callback)) { - $stored = array($callback, $arguments); - } - return $stored; -} - -/** - * Get the theme_registry cache from the database; if it doesn't exist, build it. - * - * @param $theme - * The loaded $theme object as returned by list_themes(). - * @param $base_theme - * An array of loaded $theme objects representing the ancestor themes in - * oldest first order. - * @param theme_engine - * The name of the theme engine. - */ -function _theme_load_registry($theme, $base_theme = NULL, $theme_engine = NULL) { - // Check the theme registry cache; if it exists, use it. - $cache = cache_get("theme_registry:$theme->name", 'cache'); - if (isset($cache->data)) { - $registry = $cache->data; - } - else { - // If not, build one and cache it. - $registry = _theme_build_registry($theme, $base_theme, $theme_engine); - // Only persist this registry if all modules are loaded. This assures a - // complete set of theme hooks. - if (module_load_all(NULL)) { - _theme_save_registry($theme, $registry); - } - } - return $registry; -} - -/** - * Write the theme_registry cache into the database. - */ -function _theme_save_registry($theme, $registry) { - cache_set("theme_registry:$theme->name", $registry); -} - -/** - * Force the system to rebuild the theme registry; this should be called - * when modules are added to the system, or when a dynamic system needs - * to add more theme hooks. - */ -function drupal_theme_rebuild() { - cache_clear_all('theme_registry', 'cache', TRUE); -} - -/** - * Process a single implementation of hook_theme(). - * - * @param $cache - * The theme registry that will eventually be cached; It is an associative - * array keyed by theme hooks, whose values are associative arrays describing - * the hook: - * - 'type': The passed-in $type. - * - 'theme path': The passed-in $path. - * - 'function': The name of the function generating output for this theme - * hook. Either defined explicitly in hook_theme() or, if neither 'function' - * nor 'template' is defined, then the default theme function name is used. - * The default theme function name is the theme hook prefixed by either - * 'theme_' for modules or '$name_' for everything else. If 'function' is - * defined, 'template' is not used. - * - 'template': The filename of the template generating output for this - * theme hook. The template is in the directory defined by the 'path' key of - * hook_theme() or defaults to $path. - * - 'variables': The variables for this theme hook as defined in - * hook_theme(). If there is more than one implementation and 'variables' is - * not specified in a later one, then the previous definition is kept. - * - 'render element': The renderable element for this theme hook as defined - * in hook_theme(). If there is more than one implementation and - * 'render element' is not specified in a later one, then the previous - * definition is kept. - * - 'preprocess functions': See theme() for detailed documentation. - * - 'process functions': See theme() for detailed documentation. - * @param $name - * The name of the module, theme engine, base theme engine, theme or base - * theme implementing hook_theme(). - * @param $type - * One of 'module', 'theme_engine', 'base_theme_engine', 'theme', or - * 'base_theme'. Unlike regular hooks that can only be implemented by modules, - * each of these can implement hook_theme(). _theme_process_registry() is - * called in aforementioned order and new entries override older ones. For - * example, if a theme hook is both defined by a module and a theme, then the - * definition in the theme will be used. - * @param $theme - * The loaded $theme object as returned from list_themes(). - * @param $path - * The directory where $name is. For example, modules/system or - * themes/bartik. - * - * @see theme() - * @see _theme_process_registry() - * @see hook_theme() - * @see list_themes() - */ -function _theme_process_registry(&$cache, $name, $type, $theme, $path) { - $result = array(); - - // Processor functions work in two distinct phases with the process - // functions always being executed after the preprocess functions. - $variable_process_phases = array( - 'preprocess functions' => 'preprocess', - 'process functions' => 'process', - ); - - $hook_defaults = array( - 'variables' => TRUE, - 'render element' => TRUE, - 'pattern' => TRUE, - 'base hook' => TRUE, - ); - - // Invoke the hook_theme() implementation, process what is returned, and - // merge it into $cache. - $function = $name . '_theme'; - if (function_exists($function)) { - $result = $function($cache, $type, $theme, $path); - foreach ($result as $hook => $info) { - // When a theme or engine overrides a module's theme function - // $result[$hook] will only contain key/value pairs for information being - // overridden. Pull the rest of the information from what was defined by - // an earlier hook. - - // Fill in the type and path of the module, theme, or engine that - // implements this theme function. - $result[$hook]['type'] = $type; - $result[$hook]['theme path'] = $path; - - // If function and file are omitted, default to standard naming - // conventions. - if (!isset($info['template']) && !isset($info['function'])) { - $result[$hook]['function'] = ($type == 'module' ? 'theme_' : $name . '_') . $hook; - } - - if (isset($cache[$hook]['includes'])) { - $result[$hook]['includes'] = $cache[$hook]['includes']; - } - - // If the theme implementation defines a file, then also use the path - // that it defined. Otherwise use the default path. This allows - // system.module to declare theme functions on behalf of core .include - // files. - if (isset($info['file'])) { - $include_file = isset($info['path']) ? $info['path'] : $path; - $include_file .= '/' . $info['file']; - include_once DRUPAL_ROOT . '/' . $include_file; - $result[$hook]['includes'][] = $include_file; - } - - // If the default keys are not set, use the default values registered - // by the module. - if (isset($cache[$hook])) { - $result[$hook] += array_intersect_key($cache[$hook], $hook_defaults); - } - - // The following apply only to theming hooks implemented as templates. - if (isset($info['template'])) { - // Prepend the current theming path when none is set. - if (!isset($info['path'])) { - $result[$hook]['template'] = $path . '/' . $info['template']; - } - } - - // Allow variable processors for all theming hooks, whether the hook is - // implemented as a template or as a function. - foreach ($variable_process_phases as $phase_key => $phase) { - // Check for existing variable processors. Ensure arrayness. - if (!isset($info[$phase_key]) || !is_array($info[$phase_key])) { - $info[$phase_key] = array(); - $prefixes = array(); - if ($type == 'module') { - // Default variable processor prefix. - $prefixes[] = 'template'; - // Add all modules so they can intervene with their own variable - // processors. This allows them to provide variable processors even - // if they are not the owner of the current hook. - $prefixes += module_list(); - } - elseif ($type == 'theme_engine' || $type == 'base_theme_engine') { - // Theme engines get an extra set that come before the normally - // named variable processors. - $prefixes[] = $name . '_engine'; - // The theme engine registers on behalf of the theme using the - // theme's name. - $prefixes[] = $theme; - } - else { - // This applies when the theme manually registers their own variable - // processors. - $prefixes[] = $name; - } - foreach ($prefixes as $prefix) { - // Only use non-hook-specific variable processors for theming hooks - // implemented as templates. See theme(). - if (isset($info['template']) && function_exists($prefix . '_' . $phase)) { - $info[$phase_key][] = $prefix . '_' . $phase; - } - if (function_exists($prefix . '_' . $phase . '_' . $hook)) { - $info[$phase_key][] = $prefix . '_' . $phase . '_' . $hook; - } - } - } - // Check for the override flag and prevent the cached variable - // processors from being used. This allows themes or theme engines to - // remove variable processors set earlier in the registry build. - if (!empty($info['override ' . $phase_key])) { - // Flag not needed inside the registry. - unset($result[$hook]['override ' . $phase_key]); - } - elseif (isset($cache[$hook][$phase_key]) && is_array($cache[$hook][$phase_key])) { - $info[$phase_key] = array_merge($cache[$hook][$phase_key], $info[$phase_key]); - } - $result[$hook][$phase_key] = $info[$phase_key]; - } - } - - // Merge the newly created theme hooks into the existing cache. - $cache = $result + $cache; - } - - // Let themes have variable processors even if they didn't register a template. - if ($type == 'theme' || $type == 'base_theme') { - foreach ($cache as $hook => $info) { - // Check only if not registered by the theme or engine. - if (empty($result[$hook])) { - foreach ($variable_process_phases as $phase_key => $phase) { - if (!isset($info[$phase_key])) { - $cache[$hook][$phase_key] = array(); - } - // Only use non-hook-specific variable processors for theming hooks - // implemented as templates. See theme(). - if (isset($info['template']) && function_exists($name . '_' . $phase)) { - $cache[$hook][$phase_key][] = $name . '_' . $phase; - } - if (function_exists($name . '_' . $phase . '_' . $hook)) { - $cache[$hook][$phase_key][] = $name . '_' . $phase . '_' . $hook; - $cache[$hook]['theme path'] = $path; - } - // Ensure uniqueness. - $cache[$hook][$phase_key] = array_unique($cache[$hook][$phase_key]); - } - } - } - } -} - -/** - * Rebuild the theme registry cache. - * - * @param $theme - * The loaded $theme object as returned by list_themes(). - * @param $base_theme - * An array of loaded $theme objects representing the ancestor themes in - * oldest first order. - * @param theme_engine - * The name of the theme engine. - */ -function _theme_build_registry($theme, $base_theme, $theme_engine) { - $cache = array(); - // First, process the theme hooks advertised by modules. This will - // serve as the basic registry. - foreach (module_implements('theme') as $module) { - _theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module)); - } - - // Process each base theme. - foreach ($base_theme as $base) { - // If the base theme uses a theme engine, process its hooks. - $base_path = dirname($base->filename); - if ($theme_engine) { - _theme_process_registry($cache, $theme_engine, 'base_theme_engine', $base->name, $base_path); - } - _theme_process_registry($cache, $base->name, 'base_theme', $base->name, $base_path); - } - - // And then the same thing, but for the theme. - if ($theme_engine) { - _theme_process_registry($cache, $theme_engine, 'theme_engine', $theme->name, dirname($theme->filename)); - } - - // Finally, hooks provided by the theme itself. - _theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename)); - - // Let modules alter the registry. - drupal_alter('theme_registry', $cache); - - // Optimize the registry to not have empty arrays for functions. - foreach ($cache as $hook => $info) { - foreach (array('preprocess functions', 'process functions') as $phase) { - if (empty($info[$phase])) { - unset($cache[$hook][$phase]); - } - } - } - return $cache; -} - -/** - * Return a list of all currently available themes. - * - * Retrieved from the database, if available and the site is not in maintenance - * mode; otherwise compiled freshly from the filesystem. - * - * @param $refresh - * Whether to reload the list of themes from the database. Defaults to FALSE. - * - * @return - * An associative array of the currently available themes. The keys are the - * names of the themes and the values are objects having the following - * properties: - * - 'filename': The name of the .info file. - * - 'name': The name of the theme. - * - 'status': 1 for enabled, 0 for disabled themes. - * - 'info': The contents of the .info file. - * - 'stylesheets': A two dimensional array, using the first key for the - * 'media' attribute (e.g. 'all'), the second for the name of the file - * (e.g. style.css). The value is a complete filepath - * (e.g. themes/bartik/style.css). - * - 'scripts': An associative array of JavaScripts, using the filename as key - * and the complete filepath as value. - * - 'engine': The name of the theme engine. - * - 'base theme': The name of the base theme. - */ -function list_themes($refresh = FALSE) { - $list = &drupal_static(__FUNCTION__, array()); - - if ($refresh) { - $list = array(); - system_list_reset(); - } - - if (empty($list)) { - $list = array(); - $themes = array(); - // Extract from the database only when it is available. - // Also check that the site is not in the middle of an install or update. - if (!defined('MAINTENANCE_MODE')) { - try { - $themes = system_list('theme'); - } - catch (Exception $e) { - // If the database is not available, rebuild the theme data. - $themes = _system_rebuild_theme_data(); - } - } - else { - // Scan the installation when the database should not be read. - $themes = _system_rebuild_theme_data(); - } - - foreach ($themes as $theme) { - foreach ($theme->info['stylesheets'] as $media => $stylesheets) { - foreach ($stylesheets as $stylesheet => $path) { - $theme->stylesheets[$media][$stylesheet] = $path; - } - } - foreach ($theme->info['scripts'] as $script => $path) { - $theme->scripts[$script] = $path; - } - if (isset($theme->info['engine'])) { - $theme->engine = $theme->info['engine']; - } - if (isset($theme->info['base theme'])) { - $theme->base_theme = $theme->info['base theme']; - } - // Status is normally retrieved from the database. Add zero values when - // read from the installation directory to prevent notices. - if (!isset($theme->status)) { - $theme->status = 0; - } - $list[$theme->name] = $theme; - } - } - - return $list; -} - -/** - * Generates themed output. - * - * All requests for themed output must go through this function. It examines - * the request and routes it to the appropriate theme function or template, by - * checking the theme registry. - * - * The first argument to this function is the name of the theme hook. For - * instance, to theme a table, the theme hook name is 'table'. By default, this - * theme hook could be implemented by a function called 'theme_table' or a - * template file called 'table.tpl.php', but hook_theme() can override the - * default function or template name. - * - * If the implementation is a template file, several functions are called - * before the template file is invoked, to modify the $variables array. These - * fall into the "preprocessing" phase and the "processing" phase, and are - * executed (if they exist), in the following order (note that in the following - * list, HOOK indicates the theme hook name, MODULE indicates a module name, - * THEME indicates a theme name, and ENGINE indicates a theme engine name): - * - template_preprocess(&$variables, $hook): Creates a default set of variables - * for all theme hooks. - * - template_preprocess_HOOK(&$variables): Should be implemented by - * the module that registers the theme hook, to set up default variables. - * - MODULE_preprocess(&$variables, $hook): hook_preprocess() is invoked on all - * implementing modules. - * - MODULE_preprocess_HOOK(&$variables): hook_preprocess_HOOK() is invoked on - * all implementing modules, so that modules that didn't define the theme hook - * can alter the variables. - * - ENGINE_engine_preprocess(&$variables, $hook): Allows the theme engine to - * set necessary variables for all theme hooks. - * - ENGINE_engine_preprocess_HOOK(&$variables): Allows the theme engine to set - * necessary variables for the particular theme hook. - * - THEME_preprocess(&$variables, $hook): Allows the theme to set necessary - * variables for all theme hooks. - * - THEME_preprocess_HOOK(&$variables): Allows the theme to set necessary - * variables specific to the particular theme hook. - * - template_process(&$variables, $hook): Creates a default set of variables - * for all theme hooks. - * - template_process_HOOK(&$variables): This is the first processor specific - * to the theme hook; it should be implemented by the module that registers - * it. - * - MODULE_process(&$variables, $hook): hook_process() is invoked on all - * implementing modules. - * - MODULE_process_HOOK(&$variables): hook_process_HOOK() is invoked on - * on all implementing modules, so that modules that didn't define the theme - * hook can alter the variables. - * - ENGINE_engine_process(&$variables, $hook): Allows the theme engine to set - * necessary variables for all theme hooks. - * - ENGINE_engine_process_HOOK(&$variables): Allows the theme engine to set - * necessary variables for the particular theme hook. - * - ENGINE_process(&$variables, $hook): Allows the theme engine to process the - * variables. - * - ENGINE_process_HOOK(&$variables): Allows the theme engine to process the - * variables specific to the theme hook. - * - THEME_process(&$variables, $hook): Allows the theme to process the - * variables. - * - THEME_process_HOOK(&$variables): Allows the theme to process the - * variables specific to the theme hook. - * - * If the implementation is a function, only the theme-hook-specific preprocess - * and process functions (the ones ending in _HOOK) are called from the - * list above. This is because theme hooks with function implementations - * need to be fast, and calling the non-theme-hook-specific preprocess and - * process functions for them would incur a noticeable performance penalty. - * - * There are two special variables that these preprocess and process functions - * can set: 'theme_hook_suggestion' and 'theme_hook_suggestions'. These will be - * merged together to form a list of 'suggested' alternate theme hooks to use, - * in reverse order of priority. theme_hook_suggestion will always be a higher - * priority than items in theme_hook_suggestions. theme() will use the - * highest priority implementation that exists. If none exists, theme() will - * use the implementation for the theme hook it was called with. These - * suggestions are similar to and are used for similar reasons as calling - * theme() with an array as the $hook parameter (see below). The difference - * is whether the suggestions are determined by the code that calls theme() or - * by a preprocess or process function. - * - * @param $hook - * The name of the theme hook to call. If the name contains a - * double-underscore ('__') and there isn't an implementation for the full - * name, the part before the '__' is checked. This allows a fallback to a more - * generic implementation. For example, if theme('links__node', ...) is - * called, but there is no implementation of that theme hook, then the 'links' - * implementation is used. This process is iterative, so if - * theme('links__contextual__node', ...) is called, theme() checks for the - * following implementations, and uses the first one that exists: - * - links__contextual__node - * - links__contextual - * - links - * This allows themes to create specific theme implementations for named - * objects and contexts of otherwise generic theme hooks. The $hook parameter - * may also be an array, in which case the first theme hook that has an - * implementation is used. This allows for the code that calls theme() to - * explicitly specify the fallback order in a situation where using the '__' - * convention is not desired or is insufficient. - * @param $variables - * An associative array of variables to merge with defaults from the theme - * registry, pass to preprocess and process functions for modification, and - * finally, pass to the function or template implementing the theme hook. - * Alternatively, this can be a renderable array, in which case, its - * properties are mapped to variables expected by the theme hook - * implementations. - * - * @return - * An HTML string representing the themed output. - */ -function theme($hook, $variables = array()) { - static $hooks = NULL; - - // If called before all modules are loaded, we do not necessarily have a full - // theme registry to work with, and therefore cannot process the theme - // request properly. See also _theme_load_registry(). - if (!module_load_all(NULL) && !defined('MAINTENANCE_MODE')) { - throw new Exception(t('theme() may not be called until all modules are loaded.')); - } - - if (!isset($hooks)) { - drupal_theme_initialize(); - $hooks = theme_get_registry(); - } - - // If an array of hook candidates were passed, use the first one that has an - // implementation. - if (is_array($hook)) { - foreach ($hook as $candidate) { - if (isset($hooks[$candidate])) { - break; - } - } - $hook = $candidate; - } - - // If there's no implementation, check for more generic fallbacks. If there's - // still no implementation, log an error and return an empty string. - if (!isset($hooks[$hook])) { - // Iteratively strip everything after the last '__' delimiter, until an - // implementation is found. - while ($pos = strrpos($hook, '__')) { - $hook = substr($hook, 0, $pos); - if (isset($hooks[$hook])) { - break; - } - } - if (!isset($hooks[$hook])) { - // Only log a message when not trying theme suggestions ($hook being an - // array). - if (!isset($candidate)) { - watchdog('theme', 'Theme key "@key" not found.', array('@key' => $hook), LOG_WARNING); - } - return ''; - } - } - - $info = $hooks[$hook]; - global $theme_path; - $temp = $theme_path; - // point path_to_theme() to the currently used theme path: - $theme_path = $info['theme path']; - - // Include a file if the theme function or variable processor is held elsewhere. - if (!empty($info['includes'])) { - foreach ($info['includes'] as $include_file) { - include_once DRUPAL_ROOT . '/' . $include_file; - } - } - - // If a renderable array is passed as $variables, then set $variables to - // the arguments expected by the theme function. - if (isset($variables['#theme']) || isset($variables['#theme_wrappers'])) { - $element = $variables; - $variables = array(); - if (isset($info['variables'])) { - foreach (array_keys($info['variables']) as $name) { - if (isset($element["#$name"])) { - $variables[$name] = $element["#$name"]; - } - } - } - else { - $variables[$info['render element']] = $element; - } - } - - // Merge in argument defaults. - if (!empty($info['variables'])) { - $variables += $info['variables']; - } - elseif (!empty($info['render element'])) { - $variables += array($info['render element'] => array()); - } - - // Invoke the variable processors, if any. The processors may specify - // alternate suggestions for which hook's template/function to use. If the - // hook is a suggestion of a base hook, invoke the variable processors of - // the base hook, but retain the suggestion as a high priority suggestion to - // be used unless overridden by a variable processor function. - if (isset($info['base hook'])) { - $base_hook = $info['base hook']; - $base_hook_info = $hooks[$base_hook]; - if (isset($base_hook_info['preprocess functions']) || isset($base_hook_info['process functions'])) { - $variables['theme_hook_suggestion'] = $hook; - $hook = $base_hook; - $info = $base_hook_info; - } - } - if (isset($info['preprocess functions']) || isset($info['process functions'])) { - $variables['theme_hook_suggestions'] = array(); - foreach (array('preprocess functions', 'process functions') as $phase) { - if (!empty($info[$phase])) { - foreach ($info[$phase] as $processor_function) { - if (function_exists($processor_function)) { - // We don't want a poorly behaved process function changing $hook. - $hook_clone = $hook; - $processor_function($variables, $hook_clone); - } - } - } - } - // If the preprocess/process functions specified hook suggestions, and the - // suggestion exists in the theme registry, use it instead of the hook that - // theme() was called with. This allows the preprocess/process step to - // route to a more specific theme hook. For example, a function may call - // theme('node', ...), but a preprocess function can add 'node__article' as - // a suggestion, enabling a theme to have an alternate template file for - // article nodes. Suggestions are checked in the following order: - // - The 'theme_hook_suggestion' variable is checked first. It overrides - // all others. - // - The 'theme_hook_suggestions' variable is checked in FILO order, so the - // last suggestion added to the array takes precedence over suggestions - // added earlier. - $suggestions = array(); - if (!empty($variables['theme_hook_suggestions'])) { - $suggestions = $variables['theme_hook_suggestions']; - } - if (!empty($variables['theme_hook_suggestion'])) { - $suggestions[] = $variables['theme_hook_suggestion']; - } - foreach (array_reverse($suggestions) as $suggestion) { - if (isset($hooks[$suggestion])) { - $info = $hooks[$suggestion]; - break; - } - } - } - - // Generate the output using either a function or a template. - $output = ''; - if (isset($info['function'])) { - if (function_exists($info['function'])) { - $output = $info['function']($variables); - } - } - else { - // Default render function and extension. - $render_function = 'theme_render_template'; - $extension = '.tpl.php'; - - // The theme engine may use a different extension and a different renderer. - global $theme_engine; - if (isset($theme_engine)) { - if ($info['type'] != 'module') { - if (function_exists($theme_engine . '_render_template')) { - $render_function = $theme_engine . '_render_template'; - } - $extension_function = $theme_engine . '_extension'; - if (function_exists($extension_function)) { - $extension = $extension_function(); - } - } - } - - // In some cases, a template implementation may not have had - // template_preprocess() run (for example, if the default implementation is - // a function, but a template overrides that default implementation). In - // these cases, a template should still be able to expect to have access to - // the variables provided by template_preprocess(), so we add them here if - // they don't already exist. We don't want to run template_preprocess() - // twice (it would be inefficient and mess up zebra striping), so we use the - // 'directory' variable to determine if it has already run, which while not - // completely intuitive, is reasonably safe, and allows us to save on the - // overhead of adding some new variable to track that. - if (!isset($variables['directory'])) { - $default_template_variables = array(); - template_preprocess($default_template_variables, $hook); - $variables += $default_template_variables; - } - - // Render the output using the template file. - $template_file = $info['template'] . $extension; - if (isset($info['path'])) { - $template_file = $info['path'] . '/' . $template_file; - } - $output = $render_function($template_file, $variables); - } - - // restore path_to_theme() - $theme_path = $temp; - return $output; -} - -/** - * Return the path to the current themed element. - * - * It can point to the active theme or the module handling a themed implementation. - * For example, when invoked within the scope of a theming call it will depend - * on where the theming function is handled. If implemented from a module, it - * will point to the module. If implemented from the active theme, it will point - * to the active theme. When called outside the scope of a theming call, it will - * always point to the active theme. - */ -function path_to_theme() { - global $theme_path; - - if (!isset($theme_path)) { - drupal_theme_initialize(); - } - - return $theme_path; -} - -/** - * Allow themes and/or theme engines to easily discover overridden theme functions. - * - * @param $cache - * The existing cache of theme hooks to test against. - * @param $prefixes - * An array of prefixes to test, in reverse order of importance. - * - * @return $implementations - * The functions found, suitable for returning from hook_theme; - */ -function drupal_find_theme_functions($cache, $prefixes) { - $implementations = array(); - $functions = get_defined_functions(); - - foreach ($cache as $hook => $info) { - foreach ($prefixes as $prefix) { - // Find theme functions that implement possible "suggestion" variants of - // registered theme hooks and add those as new registered theme hooks. - // The 'pattern' key defines a common prefix that all suggestions must - // start with. The default is the name of the hook followed by '__'. An - // 'base hook' key is added to each entry made for a found suggestion, - // so that common functionality can be implemented for all suggestions of - // the same base hook. To keep things simple, deep hierarchy of - // suggestions is not supported: each suggestion's 'base hook' key - // refers to a base hook, not to another suggestion, and all suggestions - // are found using the base hook's pattern, not a pattern from an - // intermediary suggestion. - $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__'); - if (!isset($info['base hook']) && !empty($pattern)) { - $matches = preg_grep('/^' . $prefix . '_' . $pattern . '/', $functions['user']); - if ($matches) { - foreach ($matches as $match) { - $new_hook = substr($match, strlen($prefix) + 1); - $arg_name = isset($info['variables']) ? 'variables' : 'render element'; - $implementations[$new_hook] = array( - 'function' => $match, - $arg_name => $info[$arg_name], - 'base hook' => $hook, - ); - } - } - } - // Find theme functions that implement registered theme hooks and include - // that in what is returned so that the registry knows that the theme has - // this implementation. - if (function_exists($prefix . '_' . $hook)) { - $implementations[$hook] = array( - 'function' => $prefix . '_' . $hook, - ); - } - } - } - - return $implementations; -} - -/** - * Allow themes and/or theme engines to easily discover overridden templates. - * - * @param $cache - * The existing cache of theme hooks to test against. - * @param $extension - * The extension that these templates will have. - * @param $path - * The path to search. - */ -function drupal_find_theme_templates($cache, $extension, $path) { - $implementations = array(); - - // Collect paths to all sub-themes grouped by base themes. These will be - // used for filtering. This allows base themes to have sub-themes in its - // folder hierarchy without affecting the base themes template discovery. - $theme_paths = array(); - foreach (list_themes() as $theme_info) { - if (!empty($theme_info->base_theme)) { - $theme_paths[$theme_info->base_theme][$theme_info->name] = dirname($theme_info->filename); - } - } - foreach ($theme_paths as $basetheme => $subthemes) { - foreach ($subthemes as $subtheme => $subtheme_path) { - if (isset($theme_paths[$subtheme])) { - $theme_paths[$basetheme] = array_merge($theme_paths[$basetheme], $theme_paths[$subtheme]); - } - } - } - global $theme; - $subtheme_paths = isset($theme_paths[$theme]) ? $theme_paths[$theme] : array(); - - // Escape the periods in the extension. - $regex = '/' . str_replace('.', '\.', $extension) . '$/'; - // Get a listing of all template files in the path to search. - $files = drupal_system_listing($regex, $path, 'name', 0); - - // Find templates that implement registered theme hooks and include that in - // what is returned so that the registry knows that the theme has this - // implementation. - foreach ($files as $template => $file) { - // Ignore sub-theme templates for the current theme. - if (strpos($file->uri, str_replace($subtheme_paths, '', $file->uri)) !== 0) { - continue; - } - // Chop off the remaining extensions if there are any. $template already - // has the rightmost extension removed, but there might still be more, - // such as with .tpl.php, which still has .tpl in $template at this point. - if (($pos = strpos($template, '.')) !== FALSE) { - $template = substr($template, 0, $pos); - } - // Transform - in filenames to _ to match function naming scheme - // for the purposes of searching. - $hook = strtr($template, '-', '_'); - if (isset($cache[$hook])) { - $implementations[$hook] = array( - 'template' => $template, - 'path' => dirname($file->uri), - ); - } - } - - // Find templates that implement possible "suggestion" variants of registered - // theme hooks and add those as new registered theme hooks. See - // drupal_find_theme_functions() for more information about suggestions and - // the use of 'pattern' and 'base hook'. - $patterns = array_keys($files); - foreach ($cache as $hook => $info) { - $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__'); - if (!isset($info['base hook']) && !empty($pattern)) { - // Transform _ in pattern to - to match file naming scheme - // for the purposes of searching. - $pattern = strtr($pattern, '_', '-'); - - $matches = preg_grep('/^' . $pattern . '/', $patterns); - if ($matches) { - foreach ($matches as $match) { - $file = substr($match, 0, strpos($match, '.')); - // Put the underscores back in for the hook name and register this pattern. - $arg_name = isset($info['variables']) ? 'variables' : 'render element'; - $implementations[strtr($file, '-', '_')] = array( - 'template' => $file, - 'path' => dirname($files[$match]->uri), - $arg_name => $info[$arg_name], - 'base hook' => $hook, - ); - } - } - } - } - return $implementations; -} - -/** - * Retrieve a setting for the current theme or for a given theme. - * - * The final setting is obtained from the last value found in the following - * sources: - * - the default global settings specified in this function - * - the default theme-specific settings defined in any base theme's .info file - * - the default theme-specific settings defined in the theme's .info file - * - the saved values from the global theme settings form - * - the saved values from the theme's settings form - * To only retrieve the default global theme setting, an empty string should be - * given for $theme. - * - * @param $setting_name - * The name of the setting to be retrieved. - * @param $theme - * The name of a given theme; defaults to the current theme. - * - * @return - * The value of the requested setting, NULL if the setting does not exist. - */ -function theme_get_setting($setting_name, $theme = NULL) { - $cache = &drupal_static(__FUNCTION__, array()); - - // If no key is given, use the current theme if we can determine it. - if (!isset($theme)) { - $theme = !empty($GLOBALS['theme_key']) ? $GLOBALS['theme_key'] : ''; - } - - if (empty($cache[$theme])) { - // Set the default values for each global setting. - // To add new global settings, add their default values below, and then - // add form elements to system_theme_settings() in system.admin.inc. - $cache[$theme] = array( - 'default_logo' => 1, - 'logo_path' => '', - 'default_favicon' => 1, - 'favicon_path' => '', - // Use the IANA-registered MIME type for ICO files as default. - 'favicon_mimetype' => 'image/vnd.microsoft.icon', - ); - // Turn on all default features. - $features = _system_default_theme_features(); - foreach ($features as $feature) { - $cache[$theme]['toggle_' . $feature] = 1; - } - - // Get the values for the theme-specific settings from the .info files of - // the theme and all its base themes. - if ($theme) { - $themes = list_themes(); - $theme_object = $themes[$theme]; - - // Create a list which includes the current theme and all its base themes. - if (isset($theme_object->base_themes)) { - $theme_keys = array_keys($theme_object->base_themes); - $theme_keys[] = $theme; - } - else { - $theme_keys = array($theme); - } - foreach ($theme_keys as $theme_key) { - if (!empty($themes[$theme_key]->info['settings'])) { - $cache[$theme] = array_merge($cache[$theme], $themes[$theme_key]->info['settings']); - } - } - } - - // Get the saved global settings from the database. - $cache[$theme] = array_merge($cache[$theme], variable_get('theme_settings', array())); - - if ($theme) { - // Get the saved theme-specific settings from the database. - $cache[$theme] = array_merge($cache[$theme], variable_get('theme_' . $theme . '_settings', array())); - - // If the theme does not support a particular feature, override the global - // setting and set the value to NULL. - if (!empty($theme_object->info['features'])) { - foreach ($features as $feature) { - if (!in_array($feature, $theme_object->info['features'])) { - $cache[$theme]['toggle_' . $feature] = NULL; - } - } - } - - // Generate the path to the logo image. - if ($cache[$theme]['toggle_logo']) { - if ($cache[$theme]['default_logo']) { - $cache[$theme]['logo'] = file_create_url(dirname($theme_object->filename) . '/logo.png'); - } - elseif ($cache[$theme]['logo_path']) { - $cache[$theme]['logo'] = file_create_url($cache[$theme]['logo_path']); - } - } - - // Generate the path to the favicon. - if ($cache[$theme]['toggle_favicon']) { - if ($cache[$theme]['default_favicon']) { - if (file_exists($favicon = dirname($theme_object->filename) . '/favicon.ico')) { - $cache[$theme]['favicon'] = file_create_url($favicon); - } - else { - $cache[$theme]['favicon'] = file_create_url('misc/favicon.ico'); - } - } - elseif ($cache[$theme]['favicon_path']) { - $cache[$theme]['favicon'] = file_create_url($cache[$theme]['favicon_path']); - } - else { - $cache[$theme]['toggle_favicon'] = FALSE; - } - } - } - } - - return isset($cache[$theme][$setting_name]) ? $cache[$theme][$setting_name] : NULL; -} - -/** - * Render a system default template, which is essentially a PHP template. - * - * @param $template_file - * The filename of the template to render. - * @param $variables - * A keyed array of variables that will appear in the output. - * - * @return - * The output generated by the template. - */ -function theme_render_template($template_file, $variables) { - extract($variables, EXTR_SKIP); // Extract the variables to a local namespace - ob_start(); // Start output buffering - include DRUPAL_ROOT . '/' . $template_file; // Include the template file - return ob_get_clean(); // End buffering and return its contents -} - -/** - * Enable a given list of themes. - * - * @param $theme_list - * An array of theme names. - */ -function theme_enable($theme_list) { - drupal_clear_css_cache(); - - foreach ($theme_list as $key) { - db_update('system') - ->fields(array('status' => 1)) - ->condition('type', 'theme') - ->condition('name', $key) - ->execute(); - } - - list_themes(TRUE); - menu_rebuild(); - drupal_theme_rebuild(); - - // Invoke hook_themes_enabled() after the themes have been enabled. - module_invoke_all('themes_enabled', $theme_list); -} - -/** - * Disable a given list of themes. - * - * @param $theme_list - * An array of theme names. - */ -function theme_disable($theme_list) { - // Don't disable the default theme. - if ($pos = array_search(variable_get('theme_default', 'bartik'), $theme_list) !== FALSE) { - unset($theme_list[$pos]); - if (empty($theme_list)) { - return; - } - } - - drupal_clear_css_cache(); - - foreach ($theme_list as $key) { - db_update('system') - ->fields(array('status' => 0)) - ->condition('type', 'theme') - ->condition('name', $key) - ->execute(); - } - - list_themes(TRUE); - menu_rebuild(); - drupal_theme_rebuild(); - - // Invoke hook_themes_disabled after the themes have been disabled. - module_invoke_all('themes_disabled', $theme_list); -} - -/** - * @ingroup themeable - * @{ - */ - -/** - * Returns HTML for status and/or error messages, grouped by type. - * - * An invisible heading identifies the messages for assistive technology. - * Sighted users see a colored box. See http://www.w3.org/TR/WCAG-TECHS/H69.html - * for info. - * - * @param $variables - * An associative array containing: - * - display: (optional) Set to 'status' or 'error' to display only messages - * of that type. - */ -function theme_status_messages($variables) { - $display = $variables['display']; - $output = ''; - - $status_heading = array( - 'status' => t('Status message'), - 'error' => t('Error message'), - 'warning' => t('Warning message'), - ); - foreach (drupal_get_messages($display) as $type => $messages) { - $output .= "
    \n"; - if (!empty($status_heading[$type])) { - $output .= '

    ' . $status_heading[$type] . "

    \n"; - } - if (count($messages) > 1) { - $output .= "
      \n"; - foreach ($messages as $message) { - $output .= '
    • ' . $message . "
    • \n"; - } - $output .= "
    \n"; - } - else { - $output .= $messages[0]; - } - $output .= "
    \n"; - } - return $output; -} - -/** - * Returns HTML for a link. - * - * All Drupal code that outputs a link should call the l() function. That - * function performs some initial preprocessing, and then, if necessary, calls - * theme('link') for rendering the anchor tag. - * - * To optimize performance for sites that don't need custom theming of links, - * the l() function includes an inline copy of this function, and uses that copy - * if none of the enabled modules or the active theme implement any preprocess - * or process functions or override this theme implementation. - * - * @param $variables - * An associative array containing the keys 'text', 'path', and 'options'. See - * the l() function for information about these variables. - * - * @see l() - */ -function theme_link($variables) { - return '' . ($variables['options']['html'] ? $variables['text'] : check_plain($variables['text'])) . ''; -} - -/** - * Returns HTML for a set of links. - * - * @param $variables - * An associative array containing: - * - links: An associative array of links to be themed. The key for each link - * is used as its css class. Each link should be itself an array, with the - * following elements: - * - title: The link text. - * - href: The link URL. If omitted, the 'title' is shown as a plain text - * item in the links list. - * - html: (optional) Whether or not 'title' is HTML. If set, the title - * will not be passed through check_plain(). - * - attributes: (optional) Attributes for the anchor, or for the tag - * used in its place if no 'href' is supplied. - * If the 'href' element is supplied, the entire link array is passed to l() - * as its $options parameter. - * - attributes: A keyed array of attributes for the UL containing the - * list of links. - * - heading: (optional) A heading to precede the links. May be an associative - * array or a string. If it's an array, it can have the following elements: - * - text: The heading text. - * - level: The heading level (e.g. 'h2', 'h3'). - * - class: (optional) An array of the CSS classes for the heading. - * When using a string it will be used as the text of the heading and the - * level will default to 'h2'. Headings should be used on navigation menus - * and any list of links that consistently appears on multiple pages. To - * make the heading invisible use the 'element-invisible' CSS class. Do not - * use 'display:none', which removes it from screen-readers and assistive - * technology. Headings allow screen-reader and keyboard only users to - * navigate to or skip the links. See - * http://juicystudio.com/article/screen-readers-display-none.php and - * http://www.w3.org/TR/WCAG-TECHS/H42.html for more information. - */ -function theme_links($variables) { - $links = $variables['links']; - $attributes = $variables['attributes']; - $heading = $variables['heading']; - global $language_url; - $output = ''; - - if (count($links) > 0) { - $output = ''; - - // Treat the heading first if it is present to prepend it to the - // list of links. - if (!empty($heading)) { - if (is_string($heading)) { - // Prepare the array that will be used when the passed heading - // is a string. - $heading = array( - 'text' => $heading, - // Set the default level of the heading. - 'level' => 'h2', - ); - } - $output .= '<' . $heading['level']; - if (!empty($heading['class'])) { - $output .= drupal_attributes(array('class' => $heading['class'])); - } - $output .= '>' . check_plain($heading['text']) . ''; - } - - $output .= ''; - - $num_links = count($links); - $i = 1; - - foreach ($links as $key => $link) { - $class = array($key); - - // Add first, last and active classes to the list of links to help out themers. - if ($i == 1) { - $class[] = 'first'; - } - if ($i == $num_links) { - $class[] = 'last'; - } - if (isset($link['href']) && ($link['href'] == $_GET['q'] || ($link['href'] == '' && drupal_is_front_page())) - && (empty($link['language']) || $link['language']->language == $language_url->language)) { - $class[] = 'active'; - } - $output .= ' $class)) . '>'; - - if (isset($link['href'])) { - // Pass in $link as $options, they share the same keys. - $output .= l($link['title'], $link['href'], $link); - } - elseif (!empty($link['title'])) { - // Some links are actually not links, but we wrap these in for adding title and class attributes. - if (empty($link['html'])) { - $link['title'] = check_plain($link['title']); - } - $span_attributes = ''; - if (isset($link['attributes'])) { - $span_attributes = drupal_attributes($link['attributes']); - } - $output .= '' . $link['title'] . ''; - } - - $i++; - $output .= "\n"; - } - - $output .= ''; - } - - return $output; -} - -/** - * Returns HTML for an image. - * - * @param $variables - * An associative array containing: - * - path: Either the path of the image file (relative to base_path()) or a - * full URL. - * - width: The width of the image (if known). - * - height: The height of the image (if known). - * - alt: The alternative text for text-based browsers. HTML 4 and XHTML 1.0 - * always require an alt attribute. The HTML 5 draft allows the alt - * attribute to be omitted in some cases. Therefore, this variable defaults - * to an empty string, but can be set to NULL for the attribute to be - * omitted. Usually, neither omission nor an empty string satisfies - * accessibility requirements, so it is strongly encouraged for code calling - * theme('image') to pass a meaningful value for this variable. - * - http://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8 - * - http://www.w3.org/TR/xhtml1/dtds.html - * - http://dev.w3.org/html5/spec/Overview.html#alt - * - title: The title text is displayed when the image is hovered in some - * popular browsers. - * - attributes: Associative array of attributes to be placed in the img tag. - */ -function theme_image($variables) { - $attributes = $variables['attributes']; - $attributes['src'] = file_create_url($variables['path']); - - foreach (array('width', 'height', 'alt', 'title') as $key) { - - if (isset($variables[$key])) { - $attributes[$key] = $variables[$key]; - } - } - - return ''; -} - -/** - * Returns HTML for a breadcrumb trail. - * - * @param $variables - * An associative array containing: - * - breadcrumb: An array containing the breadcrumb links. - */ -function theme_breadcrumb($variables) { - $breadcrumb = $variables['breadcrumb']; - - if (!empty($breadcrumb)) { - // Provide a navigational heading to give context for breadcrumb links to - // screen-reader users. Make the heading invisible with .element-invisible. - $output = '

    ' . t('You are here') . '

    '; - - $output .= ''; - return $output; - } -} - -/** - * Returns HTML for a table. - * - * @param $variables - * An associative array containing: - * - header: An array containing the table headers. Each element of the array - * can be either a localized string or an associative array with the - * following keys: - * - "data": The localized title of the table column. - * - "field": The database field represented in the table column (required - * if user is to be able to sort on this column). - * - "sort": A default sort order for this column ("asc" or "desc"). - * - Any HTML attributes, such as "colspan", to apply to the column header - * cell. - * - rows: An array of table rows. Every row is an array of cells, or an - * associative array with the following keys: - * - "data": an array of cells - * - Any HTML attributes, such as "class", to apply to the table row. - * - "no_striping": a boolean indicating that the row should receive no - * 'even / odd' styling. Defaults to FALSE. - * Each cell can be either a string or an associative array with the - * following keys: - * - "data": The string to display in the table cell. - * - "header": Indicates this cell is a header. - * - Any HTML attributes, such as "colspan", to apply to the table cell. - * Here's an example for $rows: - * @code - * $rows = array( - * // Simple row - * array( - * 'Cell 1', 'Cell 2', 'Cell 3' - * ), - * // Row with attributes on the row and some of its cells. - * array( - * 'data' => array('Cell 1', array('data' => 'Cell 2', 'colspan' => 2)), 'class' => array('funky') - * ) - * ); - * @endcode - * - attributes: An array of HTML attributes to apply to the table tag. - * - caption: A localized string to use for the tag. - * - colgroups: An array of column groups. Each element of the array can be - * either: - * - An array of columns, each of which is an associative array of HTML - * attributes applied to the COL element. - * - An array of attributes applied to the COLGROUP element, which must - * include a "data" attribute. To add attributes to COL elements, set the - * "data" attribute with an array of columns, each of which is an - * associative array of HTML attributes. - * Here's an example for $colgroup: - * @code - * $colgroup = array( - * // COLGROUP with one COL element. - * array( - * array( - * 'class' => array('funky'), // Attribute for the COL element. - * ), - * ), - * // Colgroup with attributes and inner COL elements. - * array( - * 'data' => array( - * array( - * 'class' => array('funky'), // Attribute for the COL element. - * ), - * ), - * 'class' => array('jazzy'), // Attribute for the COLGROUP element. - * ), - * ); - * @endcode - * These optional tags are used to group and set properties on columns - * within a table. For example, one may easily group three columns and - * apply same background style to all. - * - sticky: Use a "sticky" table header. - * - empty: The message to display in an extra row if table does not have any - * rows. - */ -function theme_table($variables) { - $header = $variables['header']; - $rows = $variables['rows']; - $attributes = $variables['attributes']; - $caption = $variables['caption']; - $colgroups = $variables['colgroups']; - $sticky = $variables['sticky']; - $empty = $variables['empty']; - - // Add sticky headers, if applicable. - if (count($header) && $sticky) { - drupal_add_js('misc/tableheader.js'); - // Add 'sticky-enabled' class to the table to identify it for JS. - // This is needed to target tables constructed by this function. - $attributes['class'][] = 'sticky-enabled'; - } - - $output = '\n"; - - if (isset($caption)) { - $output .= '' . $caption . "\n"; - } - - // Format the table columns: - if (count($colgroups)) { - foreach ($colgroups as $number => $colgroup) { - $attributes = array(); - - // Check if we're dealing with a simple or complex column - if (isset($colgroup['data'])) { - foreach ($colgroup as $key => $value) { - if ($key == 'data') { - $cols = $value; - } - else { - $attributes[$key] = $value; - } - } - } - else { - $cols = $colgroup; - } - - // Build colgroup - if (is_array($cols) && count($cols)) { - $output .= ' '; - $i = 0; - foreach ($cols as $col) { - $output .= ' '; - } - $output .= " \n"; - } - else { - $output .= ' \n"; - } - } - } - - // Add the 'empty' row message if available. - if (!count($rows) && $empty) { - $header_count = 0; - foreach ($header as $header_cell) { - if (is_array($header_cell)) { - $header_count += isset($header_cell['colspan']) ? $header_cell['colspan'] : 1; - } - else { - $header_count++; - } - } - $rows[] = array(array('data' => $empty, 'colspan' => $header_count, 'class' => array('empty', 'message'))); - } - - // Format the table header: - if (count($header)) { - $ts = tablesort_init($header); - // HTML requires that the thead tag has tr tags in it followed by tbody - // tags. Using ternary operator to check and see if we have any rows. - $output .= (count($rows) ? ' ' : ' '); - foreach ($header as $cell) { - $cell = tablesort_header($cell, $header, $ts); - $output .= _theme_table_cell($cell, TRUE); - } - // Using ternary operator to close the tags based on whether or not there are rows - $output .= (count($rows) ? " \n" : "\n"); - } - else { - $ts = array(); - } - - // Format the table rows: - if (count($rows)) { - $output .= "\n"; - $flip = array('even' => 'odd', 'odd' => 'even'); - $class = 'even'; - foreach ($rows as $number => $row) { - $attributes = array(); - - // Check if we're dealing with a simple or complex row - if (isset($row['data'])) { - foreach ($row as $key => $value) { - if ($key == 'data') { - $cells = $value; - } - else { - $attributes[$key] = $value; - } - } - } - else { - $cells = $row; - } - if (count($cells)) { - // Add odd/even class - if (empty($row['no_striping'])) { - $class = $flip[$class]; - $attributes['class'][] = $class; - } - - // Build row - $output .= ' '; - $i = 0; - foreach ($cells as $cell) { - $cell = tablesort_cell($cell, $header, $ts, $i++); - $output .= _theme_table_cell($cell); - } - $output .= " \n"; - } - } - $output .= "\n"; - } - - $output .= "\n"; - return $output; -} - -/** - * Returns HTML for a sort icon. - * - * @param $variables - * An associative array containing: - * - style: Set to either 'asc' or 'desc', this determines which icon to show. - */ -function theme_tablesort_indicator($variables) { - if ($variables['style'] == "asc") { - return theme('image', array('path' => 'misc/arrow-asc.png', 'width' => 13, 'height' => 13, 'alt' => t('sort ascending'), 'title' => t('sort ascending'))); - } - else { - return theme('image', array('path' => 'misc/arrow-desc.png', 'width' => 13, 'height' => 13, 'alt' => t('sort descending'), 'title' => t('sort descending'))); - } -} - -/** - * Returns HTML for a marker for new or updated content. - * - * @param $variables - * An associative array containing: - * - type: Number representing the marker type to display. See MARK_NEW, - * MARK_UPDATED, MARK_READ. - */ -function theme_mark($variables) { - $type = $variables['type']; - global $user; - if ($user->uid) { - if ($type == MARK_NEW) { - return ' ' . t('new') . ''; - } - elseif ($type == MARK_UPDATED) { - return ' ' . t('updated') . ''; - } - } -} - -/** - * Returns HTML for a list or nested list of items. - * - * @param $variables - * An associative array containing: - * - items: An array of items to be displayed in the list. If an item is a - * string, then it is used as is. If an item is an array, then the "data" - * element of the array is used as the contents of the list item. If an item - * is an array with a "children" element, those children are displayed in a - * nested list. All other elements are treated as attributes of the list - * item element. - * - title: The title of the list. - * - type: The type of list to return (e.g. "ul", "ol"). - * - attributes: The attributes applied to the list element. - */ -function theme_item_list($variables) { - $items = $variables['items']; - $title = $variables['title']; - $type = $variables['type']; - $attributes = $variables['attributes']; - - $output = '
    '; - if (isset($title)) { - $output .= '

    ' . $title . '

    '; - } - - if (!empty($items)) { - $output .= "<$type" . drupal_attributes($attributes) . '>'; - $num_items = count($items); - foreach ($items as $i => $item) { - $attributes = array(); - $children = array(); - if (is_array($item)) { - foreach ($item as $key => $value) { - if ($key == 'data') { - $data = $value; - } - elseif ($key == 'children') { - $children = $value; - } - else { - $attributes[$key] = $value; - } - } - } - else { - $data = $item; - } - if (count($children) > 0) { - // Render nested list. - $data .= theme_item_list(array('items' => $children, 'title' => NULL, 'type' => $type, 'attributes' => $attributes)); - } - if ($i == 0) { - $attributes['class'][] = 'first'; - } - if ($i == $num_items - 1) { - $attributes['class'][] = 'last'; - } - $output .= '' . $data . "\n"; - } - $output .= ""; - } - $output .= '
    '; - return $output; -} - -/** - * Returns HTML for a "more help" link. - * - * @param $variables - * An associative array containing: - * - url: The url for the link. - */ -function theme_more_help_link($variables) { - return ''; -} - -/** - * Returns HTML for a feed icon. - * - * @param $variables - * An associative array containing: - * - url: An internal system path or a fully qualified external URL of the - * feed. - * - title: A descriptive title of the feed. - */ -function theme_feed_icon($variables) { - $text = t('Subscribe to @feed-title', array('@feed-title' => $variables['title'])); - if ($image = theme('image', array('path' => 'misc/feed.png', 'width' => 16, 'height' => 16, 'alt' => $text))) { - return l($image, $variables['url'], array('html' => TRUE, 'attributes' => array('class' => array('feed-icon'), 'title' => $text))); - } -} - -/** - * Returns HTML for a generic HTML tag with attributes. - * - * @param $variables - * An associative array containing: - * - element: An associative array describing the tag: - * - #tag: The tag name to output. Typical tags added to the HTML HEAD: - * - meta: To provide meta information, such as a page refresh. - * - link: To refer to stylesheets and other contextual information. - * - script: To load JavaScript. - * - #attributes: (optional) An array of HTML attributes to apply to the - * tag. - * - #value: (optional) A string containing tag content, such as inline CSS. - * - #value_prefix: (optional) A string to prepend to #value, e.g. a CDATA - * wrapper prefix. - * - #value_suffix: (optional) A string to append to #value, e.g. a CDATA - * wrapper suffix. - */ -function theme_html_tag($variables) { - $element = $variables['element']; - if (!isset($element['#value'])) { - return '<' . $element['#tag'] . drupal_attributes($element['#attributes']) . " />\n"; - } - else { - $output = '<' . $element['#tag'] . drupal_attributes($element['#attributes']) . '>'; - if (isset($element['#value_prefix'])) { - $output .= $element['#value_prefix']; - } - $output .= $element['#value']; - if (isset($element['#value_suffix'])) { - $output .= $element['#value_suffix']; - } - $output .= '\n"; - return $output; - } -} - -/** - * Returns HTML for a "more" link, like those used in blocks. - * - * @param $variables - * An associative array containing: - * - url: The url of the main page. - * - title: A descriptive verb for the link, like 'Read more'. - */ -function theme_more_link($variables) { - return ''; -} - -/** - * Returns HTML for a username, potentially linked to the user's page. - * - * @param $variables - * An associative array containing: - * - account: The user object to format. - * - name: The user's name, sanitized. - * - extra: Additional text to append to the user's name, sanitized. - * - link_path: The path or URL of the user's profile page, home page, or - * other desired page to link to for more information about the user. - * - link_options: An array of options to pass to the l() function's $options - * parameter if linking the user's name to the user's page. - * - attributes_array: An array of attributes to pass to the - * drupal_attributes() function if not linking to the user's page. - * - * @see template_preprocess_username() - * @see template_process_username() - */ -function theme_username($variables) { - if (isset($variables['link_path'])) { - // We have a link path, so we should generate a link using l(). - // Additional classes may be added as array elements like - // $variables['link_options']['attributes']['class'][] = 'myclass'; - $output = l($variables['name'] . $variables['extra'], $variables['link_path'], $variables['link_options']); - } - else { - // Modules may have added important attributes so they must be included - // in the output. Additional classes may be added as array elements like - // $variables['attributes_array']['class'][] = 'myclass'; - $output = '' . $variables['name'] . $variables['extra'] . '
    '; - } - return $output; -} - -/** - * Returns HTML for a progress bar. - * - * @param $variables - * An associative array containing: - * - percent: The percentage of the progress. - * - message: A string containing information to be displayed. - */ -function theme_progress_bar($variables) { - $output = '
    '; - $output .= '
    '; - $output .= '
    ' . $variables['percent'] . '%
    '; - $output .= '
    ' . $variables['message'] . '
    '; - $output .= '
    '; - - return $output; -} - -/** - * Returns HTML for an indentation div; used for drag and drop tables. - * - * @param $variables - * An associative array containing: - * - size: Optional. The number of indentations to create. - */ -function theme_indentation($variables) { - $output = ''; - for ($n = 0; $n < $variables['size']; $n++) { - $output .= '
     
    '; - } - return $output; -} - -/** - * @} End of "ingroup themeable". - */ - -/** - * Returns HTML output for a single table cell for theme_table(). - * - * @param $cell - * Array of cell information, or string to display in cell. - * @param bool $header - * TRUE if this cell is a table header cell, FALSE if it is an ordinary - * table cell. If $cell is an array with element 'header' set to TRUE, that - * will override the $header parameter. - * - * @return - * HTML for the cell. - */ -function _theme_table_cell($cell, $header = FALSE) { - $attributes = ''; - - if (is_array($cell)) { - $data = isset($cell['data']) ? $cell['data'] : ''; - // Cell's data property can be a string or a renderable array. - if (is_array($data)) { - $data = drupal_render($data); - } - $header |= isset($cell['header']); - unset($cell['data']); - unset($cell['header']); - $attributes = drupal_attributes($cell); - } - else { - $data = $cell; - } - - if ($header) { - $output = "$data"; - } - else { - $output = "$data"; - } - - return $output; -} - -/** - * Adds a default set of helper variables for variable processors and templates. - * This comes in before any other preprocess function which makes it possible to - * be used in default theme implementations (non-overridden theme functions). - * - * For more detailed information, see theme(). - * - */ -function template_preprocess(&$variables, $hook) { - global $user; - static $count = array(); - - // Track run count for each hook to provide zebra striping. - // See "template_preprocess_block()" which provides the same feature specific to blocks. - $count[$hook] = isset($count[$hook]) && is_int($count[$hook]) ? $count[$hook] : 1; - $variables['zebra'] = ($count[$hook] % 2) ? 'odd' : 'even'; - $variables['id'] = $count[$hook]++; - - // Tell all templates where they are located. - $variables['directory'] = path_to_theme(); - - // Initialize html class attribute for the current hook. - $variables['classes_array'] = array(drupal_html_class($hook)); - - // Merge in variables that don't depend on hook and don't change during a - // single page request. - // Use the advanced drupal_static() pattern, since this is called very often. - static $drupal_static_fast; - if (!isset($drupal_static_fast)) { - $drupal_static_fast['default_variables'] = &drupal_static(__FUNCTION__); - } - $default_variables = &$drupal_static_fast['default_variables']; - // Global $user object shouldn't change during a page request once rendering - // has started, but if there's an edge case where it does, re-fetch the - // variables appropriate for the new user. - if (!isset($default_variables) || ($user !== $default_variables['user'])) { - $default_variables = _template_preprocess_default_variables(); - } - $variables += $default_variables; -} - -/** - * Returns hook-independant variables to template_preprocess(). - */ -function _template_preprocess_default_variables() { - global $user; - - // Variables that don't depend on a database connection. - $variables = array( - 'attributes_array' => array(), - 'title_attributes_array' => array(), - 'content_attributes_array' => array(), - 'title_prefix' => array(), - 'title_suffix' => array(), - 'user' => $user, - 'db_is_active' => !defined('MAINTENANCE_MODE'), - 'is_admin' => FALSE, - 'logged_in' => FALSE, - ); - - // The user object has no uid property when the database does not exist during - // install. The user_access() check deals with issues when in maintenance mode - // as uid is set but the user.module has not been included. - if (isset($user->uid) && function_exists('user_access')) { - $variables['is_admin'] = user_access('access administration pages'); - $variables['logged_in'] = ($user->uid > 0); - } - - // drupal_is_front_page() might throw an exception. - try { - $variables['is_front'] = drupal_is_front_page(); - } - catch (Exception $e) { - // If the database is not yet available, set default values for these - // variables. - $variables['is_front'] = FALSE; - $variables['db_is_active'] = FALSE; - } - - return $variables; -} - -/** - * A default process function used to alter variables as late as possible. - * - * For more detailed information, see theme(). - * - */ -function template_process(&$variables, $hook) { - // Flatten out classes. - $variables['classes'] = implode(' ', $variables['classes_array']); - - // Flatten out attributes, title_attributes, and content_attributes. - // Because this function can be called very often, and often with empty - // attributes, optimize performance by only calling drupal_attributes() if - // necessary. - $variables['attributes'] = $variables['attributes_array'] ? drupal_attributes($variables['attributes_array']) : ''; - $variables['title_attributes'] = $variables['title_attributes_array'] ? drupal_attributes($variables['title_attributes_array']) : ''; - $variables['content_attributes'] = $variables['content_attributes_array'] ? drupal_attributes($variables['content_attributes_array']) : ''; -} - -/** - * Preprocess variables for html.tpl.php - * - * @see system_elements() - * @see html.tpl.php - */ -function template_preprocess_html(&$variables) { - // Compile a list of classes that are going to be applied to the body element. - // This allows advanced theming based on context (home page, node of certain type, etc.). - // Add a class that tells us whether we're on the front page or not. - $variables['classes_array'][] = $variables['is_front'] ? 'front' : 'not-front'; - // Add a class that tells us whether the page is viewed by an authenticated user or not. - $variables['classes_array'][] = $variables['logged_in'] ? 'logged-in' : 'not-logged-in'; - - // Add information about the number of sidebars. - if (!empty($variables['page']['sidebar_first']) && !empty($variables['page']['sidebar_second'])) { - $variables['classes_array'][] = 'two-sidebars'; - } - elseif (!empty($variables['page']['sidebar_first'])) { - $variables['classes_array'][] = 'one-sidebar sidebar-first'; - } - elseif (!empty($variables['page']['sidebar_second'])) { - $variables['classes_array'][] = 'one-sidebar sidebar-second'; - } - else { - $variables['classes_array'][] = 'no-sidebars'; - } - - // Populate the body classes. - if ($suggestions = theme_get_suggestions(arg(), 'page', '-')) { - foreach ($suggestions as $suggestion) { - if ($suggestion != 'page-front') { - // Add current suggestion to page classes to make it possible to theme - // the page depending on the current page type (e.g. node, admin, user, - // etc.) as well as more specific data like node-12 or node-edit. - $variables['classes_array'][] = drupal_html_class($suggestion); - } - } - } - - // If on an individual node page, add the node type to body classes. - if ($node = menu_get_object()) { - $variables['classes_array'][] = drupal_html_class('node-type-' . $node->type); - } - - // RDFa allows annotation of XHTML pages with RDF data, while GRDDL provides - // mechanisms for extraction of this RDF content via XSLT transformation - // using an associated GRDDL profile. - $variables['rdf_namespaces'] = drupal_get_rdf_namespaces(); - $variables['grddl_profile'] = 'http://www.w3.org/1999/xhtml/vocab'; - $variables['language'] = $GLOBALS['language']; - $variables['language']->dir = $GLOBALS['language']->direction ? 'rtl' : 'ltr'; - - // Add favicon. - if (theme_get_setting('toggle_favicon')) { - $favicon = theme_get_setting('favicon'); - $type = theme_get_setting('favicon_mimetype'); - drupal_add_html_head_link(array('rel' => 'shortcut icon', 'href' => drupal_strip_dangerous_protocols($favicon), 'type' => $type)); - } - - // Construct page title. - if (drupal_get_title()) { - $head_title = array( - 'title' => strip_tags(drupal_get_title()), - 'name' => check_plain(variable_get('site_name', 'Drupal')), - ); - } - else { - $head_title = array('name' => check_plain(variable_get('site_name', 'Drupal'))); - if (variable_get('site_slogan', '')) { - $head_title['slogan'] = filter_xss_admin(variable_get('site_slogan', '')); - } - } - $variables['head_title_array'] = $head_title; - $variables['head_title'] = implode(' | ', $head_title); - - // Populate the page template suggestions. - if ($suggestions = theme_get_suggestions(arg(), 'html')) { - $variables['theme_hook_suggestions'] = $suggestions; - } -} - -/** - * Preprocess variables for page.tpl.php - * - * Most themes utilize their own copy of page.tpl.php. The default is located - * inside "modules/system/page.tpl.php". Look in there for the full list of - * variables. - * - * Uses the arg() function to generate a series of page template suggestions - * based on the current path. - * - * Any changes to variables in this preprocessor should also be changed inside - * template_preprocess_maintenance_page() to keep all of them consistent. - * - * @see drupal_render_page() - * @see template_process_page() - * @see page.tpl.php - */ -function template_preprocess_page(&$variables) { - // Move some variables to the top level for themer convenience and template cleanliness. - $variables['show_messages'] = $variables['page']['#show_messages']; - - foreach (system_region_list($GLOBALS['theme']) as $region_key => $region_name) { - if (!isset($variables['page'][$region_key])) { - $variables['page'][$region_key] = array(); - } - } - - // Set up layout variable. - $variables['layout'] = 'none'; - if (!empty($variables['page']['sidebar_first'])) { - $variables['layout'] = 'first'; - } - if (!empty($variables['page']['sidebar_second'])) { - $variables['layout'] = ($variables['layout'] == 'first') ? 'both' : 'second'; - } - - $variables['base_path'] = base_path(); - $variables['front_page'] = url(); - $variables['feed_icons'] = drupal_get_feeds(); - $variables['language'] = $GLOBALS['language']; - $variables['language']->dir = $GLOBALS['language']->direction ? 'rtl' : 'ltr'; - $variables['logo'] = theme_get_setting('logo'); - $variables['main_menu'] = theme_get_setting('toggle_main_menu') ? menu_main_menu() : array(); - $variables['secondary_menu'] = theme_get_setting('toggle_secondary_menu') ? menu_secondary_menu() : array(); - $variables['action_links'] = menu_local_actions(); - $variables['site_name'] = (theme_get_setting('toggle_name') ? filter_xss_admin(variable_get('site_name', 'Drupal')) : ''); - $variables['site_slogan'] = (theme_get_setting('toggle_slogan') ? filter_xss_admin(variable_get('site_slogan', '')) : ''); - $variables['tabs'] = menu_local_tabs(); - - if ($node = menu_get_object()) { - $variables['node'] = $node; - } - - // Populate the page template suggestions. - if ($suggestions = theme_get_suggestions(arg(), 'page')) { - $variables['theme_hook_suggestions'] = $suggestions; - } -} - -/** - * Process variables for page.tpl.php - * - * Perform final addition of variables before passing them into the template. - * To customize these variables, simply set them in an earlier step. - * - * @see template_preprocess_page() - * @see page.tpl.php - */ -function template_process_page(&$variables) { - if (!isset($variables['breadcrumb'])) { - // Build the breadcrumb last, so as to increase the chance of being able to - // re-use the cache of an already rendered menu containing the active link - // for the current page. - // @see menu_tree_page_data() - $variables['breadcrumb'] = theme('breadcrumb', array('breadcrumb' => drupal_get_breadcrumb())); - } - if (!isset($variables['title'])) { - $variables['title'] = drupal_get_title(); - } - - // Generate messages last in order to capture as many as possible for the - // current page. - if (!isset($variables['messages'])) { - $variables['messages'] = $variables['show_messages'] ? theme('status_messages') : ''; - } -} - -/** - * Process variables for html.tpl.php - * - * Perform final addition and modification of variables before passing into - * the template. To customize these variables, call drupal_render() on elements - * in $variables['page'] during THEME_preprocess_page(). - * - * @see template_preprocess_html() - * @see html.tpl.php - */ -function template_process_html(&$variables) { - // Render page_top and page_bottom into top level variables. - $variables['page_top'] = drupal_render($variables['page']['page_top']); - $variables['page_bottom'] = drupal_render($variables['page']['page_bottom']); - // Place the rendered HTML for the page body into a top level variable. - $variables['page'] = $variables['page']['#children']; - $variables['page_bottom'] .= drupal_get_js('footer'); - - $variables['head'] = drupal_get_html_head(); - $variables['css'] = drupal_add_css(); - $variables['styles'] = drupal_get_css(); - $variables['scripts'] = drupal_get_js(); -} - -/** - * Generate an array of suggestions from path arguments. - * - * This is typically called for adding to the 'theme_hook_suggestions' or - * 'classes_array' variables from within preprocess functions, when wanting to - * base the additional suggestions on the path of the current page. - * - * @param $args - * An array of path arguments, such as from function arg(). - * @param $base - * A string identifying the base 'thing' from which more specific suggestions - * are derived. For example, 'page' or 'html'. - * @param $delimiter - * The string used to delimit increasingly specific information. The default - * of '__' is appropriate for theme hook suggestions. '-' is appropriate for - * extra classes. - * - * @return - * An array of suggestions, suitable for adding to - * $variables['theme_hook_suggestions'] within a preprocess function or to - * $variables['classes_array'] if the suggestions represent extra CSS classes. - */ -function theme_get_suggestions($args, $base, $delimiter = '__') { - - // Build a list of suggested theme hooks or body classes in order of - // specificity. One suggestion is made for every element of the current path, - // though numeric elements are not carried to subsequent suggestions. For - // example, for $base='page', http://www.example.com/node/1/edit would result - // in the following suggestions and body classes: - // - // page__node page-node - // page__node__% page-node-% - // page__node__1 page-node-1 - // page__node__edit page-node-edit - - $suggestions = array(); - $prefix = $base; - foreach ($args as $arg) { - // Remove slashes or null per SA-CORE-2009-003 and change - (hyphen) to _ - // (underscore). - // - // When we discover templates in @see drupal_find_theme_templates, - // hyphens (-) are converted to underscores (_) before the theme hook - // is registered. We do this because the hyphens used for delimiters - // in hook suggestions cannot be used in the function names of the - // associated preprocess functions. Any page templates designed to be used - // on paths that contain a hyphen are also registered with these hyphens - // converted to underscores so here we must convert any hyphens in path - // arguments to underscores here before fetching theme hook suggestions - // to ensure the templates are appropriately recognized. - $arg = str_replace(array("/", "\\", "\0", '-'), array('', '', '', '_'), $arg); - // The percent acts as a wildcard for numeric arguments since - // asterisks are not valid filename characters on many filesystems. - if (is_numeric($arg)) { - $suggestions[] = $prefix . $delimiter . '%'; - } - $suggestions[] = $prefix . $delimiter . $arg; - if (!is_numeric($arg)) { - $prefix .= $delimiter . $arg; - } - } - if (drupal_is_front_page()) { - // Front templates should be based on root only, not prefixed arguments. - $suggestions[] = $base . $delimiter . 'front'; - } - - return $suggestions; -} - -/** - * The variables array generated here is a mirror of template_preprocess_page(). - * This preprocessor will run its course when theme_maintenance_page() is - * invoked. - * - * An alternate template file of "maintenance-page--offline.tpl.php" can be - * used when the database is offline to hide errors and completely replace the - * content. - * - * The $variables array contains the following arguments: - * - $content - * - * @see maintenance-page.tpl.php - */ -function template_preprocess_maintenance_page(&$variables) { - // Add favicon - if (theme_get_setting('toggle_favicon')) { - $favicon = theme_get_setting('favicon'); - $type = theme_get_setting('favicon_mimetype'); - drupal_add_html_head_link(array('rel' => 'shortcut icon', 'href' => drupal_strip_dangerous_protocols($favicon), 'type' => $type)); - } - - global $theme; - // Retrieve the theme data to list all available regions. - $theme_data = list_themes(); - $regions = $theme_data[$theme]->info['regions']; - - // Get all region content set with drupal_add_region_content(). - foreach (array_keys($regions) as $region) { - // Assign region to a region variable. - $region_content = drupal_get_region_content($region); - isset($variables[$region]) ? $variables[$region] .= $region_content : $variables[$region] = $region_content; - } - - // Setup layout variable. - $variables['layout'] = 'none'; - if (!empty($variables['sidebar_first'])) { - $variables['layout'] = 'first'; - } - if (!empty($variables['sidebar_second'])) { - $variables['layout'] = ($variables['layout'] == 'first') ? 'both' : 'second'; - } - - // Construct page title - if (drupal_get_title()) { - $head_title = array( - 'title' => strip_tags(drupal_get_title()), - 'name' => variable_get('site_name', 'Drupal'), - ); - } - else { - $head_title = array('name' => variable_get('site_name', 'Drupal')); - if (variable_get('site_slogan', '')) { - $head_title['slogan'] = variable_get('site_slogan', ''); - } - } - - // set the default language if necessary - $language = isset($GLOBALS['language']) ? $GLOBALS['language'] : language_default(); - - $variables['head_title_array'] = $head_title; - $variables['head_title'] = implode(' | ', $head_title); - $variables['base_path'] = base_path(); - $variables['front_page'] = url(); - $variables['breadcrumb'] = ''; - $variables['feed_icons'] = ''; - $variables['help'] = ''; - $variables['language'] = $language; - $variables['language']->dir = $language->direction ? 'rtl' : 'ltr'; - $variables['logo'] = theme_get_setting('logo'); - $variables['messages'] = $variables['show_messages'] ? theme('status_messages') : ''; - $variables['main_menu'] = array(); - $variables['secondary_menu'] = array(); - $variables['site_name'] = (theme_get_setting('toggle_name') ? variable_get('site_name', 'Drupal') : ''); - $variables['site_slogan'] = (theme_get_setting('toggle_slogan') ? variable_get('site_slogan', '') : ''); - $variables['tabs'] = ''; - $variables['title'] = drupal_get_title(); - $variables['closure'] = ''; - - // Compile a list of classes that are going to be applied to the body element. - $variables['classes_array'][] = 'in-maintenance'; - if (isset($variables['db_is_active']) && !$variables['db_is_active']) { - $variables['classes_array'][] = 'db-offline'; - } - if ($variables['layout'] == 'both') { - $variables['classes_array'][] = 'two-sidebars'; - } - elseif ($variables['layout'] == 'none') { - $variables['classes_array'][] = 'no-sidebars'; - } - else { - $variables['classes_array'][] = 'one-sidebar sidebar-' . $variables['layout']; - } - - // Dead databases will show error messages so supplying this template will - // allow themers to override the page and the content completely. - if (isset($variables['db_is_active']) && !$variables['db_is_active']) { - $variables['theme_hook_suggestion'] = 'maintenance_page__offline'; - } -} - -/** - * The variables array generated here is a mirror of template_process_html(). - * This processor will run its course when theme_maintenance_page() is invoked. - * - * @see maintenance-page.tpl.php - */ -function template_process_maintenance_page(&$variables) { - $variables['head'] = drupal_get_html_head(); - $variables['css'] = drupal_add_css(); - $variables['styles'] = drupal_get_css(); - $variables['scripts'] = drupal_get_js(); -} - -/** - * Preprocess variables for region.tpl.php - * - * Prepare the values passed to the theme_region function to be passed into a - * pluggable template engine. Uses the region name to generate a template file - * suggestions. If none are found, the default region.tpl.php is used. - * - * @see drupal_region_class() - * @see region.tpl.php - */ -function template_preprocess_region(&$variables) { - // Create the $content variable that templates expect. - $variables['content'] = $variables['elements']['#children']; - $variables['region'] = $variables['elements']['#region']; - - $variables['classes_array'][] = drupal_region_class($variables['region']); - $variables['theme_hook_suggestions'][] = 'region__' . $variables['region']; -} - -/** - * Preprocesses variables for theme_username(). - * - * Modules that make any changes to variables like 'name' or 'extra' must insure - * that the final string is safe to include directly in the output by using - * check_plain() or filter_xss(). - * - * @see template_process_username() - */ -function template_preprocess_username(&$variables) { - $account = $variables['account']; - - $variables['extra'] = ''; - if (empty($account->uid)) { - $variables['uid'] = 0; - if (theme_get_setting('toggle_comment_user_verification')) { - $variables['extra'] = ' (' . t('not verified') . ')'; - } - } - else { - $variables['uid'] = (int) $account->uid; - } - - // Set the name to a formatted name that is safe for printing and - // that won't break tables by being too long. Keep an unshortened, - // unsanitized version, in case other preprocess functions want to implement - // their own shortening logic or add markup. If they do so, they must ensure - // that $variables['name'] is safe for printing. - $name = $variables['name_raw'] = format_username($account); - if (drupal_strlen($name) > 20) { - $name = drupal_substr($name, 0, 15) . '...'; - } - $variables['name'] = check_plain($name); - - $variables['profile_access'] = user_access('access user profiles'); - $variables['link_attributes'] = array(); - // Populate link path and attributes if appropriate. - if ($variables['uid'] && $variables['profile_access']) { - // We are linking to a local user. - $variables['link_attributes'] = array('title' => t('View user profile.')); - $variables['link_path'] = 'user/' . $variables['uid']; - } - elseif (!empty($account->homepage)) { - // Like the 'class' attribute, the 'rel' attribute can hold a - // space-separated set of values, so initialize it as an array to make it - // easier for other preprocess functions to append to it. - $variables['link_attributes'] = array('rel' => array('nofollow')); - $variables['link_path'] = $account->homepage; - $variables['homepage'] = $account->homepage; - } - // We do not want the l() function to check_plain() a second time. - $variables['link_options']['html'] = TRUE; - // Set a default class. - $variables['attributes_array'] = array('class' => array('username')); -} - -/** - * Processes variables for theme_username(). - * - * @see template_preprocess_username() - */ -function template_process_username(&$variables) { - // Finalize the link_options array for passing to the l() function. - // This is done in the process phase so that attributes may be added by - // modules or the theme during the preprocess phase. - if (isset($variables['link_path'])) { - // $variables['attributes_array'] contains attributes that should be applied - // regardless of whether a link is being rendered or not. - // $variables['link_attributes'] contains attributes that should only be - // applied if a link is being rendered. Preprocess functions are encouraged - // to use the former unless they want to add attributes on the link only. - // If a link is being rendered, these need to be merged. Some attributes are - // themselves arrays, so the merging needs to be recursive. - $variables['link_options']['attributes'] = array_merge_recursive($variables['link_attributes'], $variables['attributes_array']); - } -} diff --git a/includes/theme.maintenance.inc b/includes/theme.maintenance.inc deleted file mode 100644 index 218a8ad..0000000 --- a/includes/theme.maintenance.inc +++ /dev/null @@ -1,211 +0,0 @@ -base_theme)) { - $base_theme[] = $new_base_theme = $themes[$themes[$ancestor]->base_theme]; - $ancestor = $themes[$ancestor]->base_theme; - } - _drupal_theme_initialize($themes[$theme], array_reverse($base_theme), '_theme_load_offline_registry'); - - // These are usually added from system_init() -except maintenance.css. - // When the database is inactive it's not called so we add it here. - $path = drupal_get_path('module', 'system'); - drupal_add_css($path . '/system.base.css'); - drupal_add_css($path . '/system.admin.css'); - drupal_add_css($path . '/system.menus.css'); - drupal_add_css($path . '/system.messages.css'); - drupal_add_css($path . '/system.theme.css'); - drupal_add_css($path . '/system.maintenance.css'); -} - -/** - * This builds the registry when the site needs to bypass any database calls. - */ -function _theme_load_offline_registry($theme, $base_theme = NULL, $theme_engine = NULL) { - return _theme_build_registry($theme, $base_theme, $theme_engine); -} - -/** - * Returns HTML for a list of maintenance tasks to perform. - * - * @param $variables - * An associative array containing: - * - items: An associative array of maintenance tasks. - * - active: The key for the currently active maintenance task. - * - * @ingroup themeable - */ -function theme_task_list($variables) { - $items = $variables['items']; - $active = $variables['active']; - - $done = isset($items[$active]) || $active == NULL; - $output = '

    Installation tasks

    '; - $output .= '
      '; - - foreach ($items as $k => $item) { - if ($active == $k) { - $class = 'active'; - $status = '(' . t('active') . ')'; - $done = FALSE; - } - else { - $class = $done ? 'done' : ''; - $status = $done ? '(' . t('done') . ')' : ''; - } - $output .= ''; - $output .= $item; - $output .= ($status ? '' . $status . '' : ''); - $output .= ''; - } - $output .= '
    '; - return $output; -} - -/** - * Returns HTML for the installation page. - * - * Note: this function is not themeable. - * - * @param $variables - * An associative array containing: - * - content: The page content to show. - */ -function theme_install_page($variables) { - drupal_add_http_header('Content-Type', 'text/html; charset=utf-8'); - return theme('maintenance_page', $variables); -} - -/** - * Returns HTML for the update page. - * - * Note: this function is not themeable. - * - * @param $variables - * An associative array containing: - * - content: The page content to show. - * - show_messages: Whether to output status and error messages. - * FALSE can be useful to postpone the messages to a subsequent page. - */ -function theme_update_page($variables) { - drupal_add_http_header('Content-Type', 'text/html; charset=utf-8'); - return theme('maintenance_page', $variables); -} - -/** - * Returns HTML for a report of the results from an operation run via authorize.php. - * - * @param $variables - * An associative array containing: - * - messages: An array of result messages. - * - * @ingroup themeable - */ -function theme_authorize_report($variables) { - $messages = $variables['messages']; - $output = ''; - if (!empty($messages)) { - $output .= '
    '; - foreach ($messages as $heading => $logs) { - $items = array(); - foreach ($logs as $number => $log_message) { - if ($number === '#abort') { - continue; - } - $items[] = theme('authorize_message', array('message' => $log_message['message'], 'success' => $log_message['success'])); - } - $output .= theme('item_list', array('items' => $items, 'title' => $heading)); - } - $output .= '
    '; - } - return $output; -} - -/** - * Returns HTML for a single log message from the authorize.php batch operation. - * - * @param $variables - * An associative array containing: - * - message: The log message. - * - success: A boolean indicating failure or success. - * - * @ingroup themeable - */ -function theme_authorize_message($variables) { - $message = $variables['message']; - $success = $variables['success']; - if ($success) { - $item = array('data' => $message, 'class' => array('success')); - } - else { - $item = array('data' => '' . $message . '', 'class' => array('failure')); - } - return $item; -} diff --git a/includes/update.inc b/includes/update.inc deleted file mode 100644 index f7a8fd6..0000000 --- a/includes/update.inc +++ /dev/null @@ -1,700 +0,0 @@ -name, $row->type)) { - $incompatible[] = $row->name; - } - } - if (!empty($incompatible)) { - db_update('system') - ->fields(array('status' => 0)) - ->condition('name', $incompatible, 'IN') - ->execute(); - } -} - -/** - * Helper function to test compatibility of a module or theme. - */ -function update_check_incompatibility($name, $type = 'module') { - static $themes, $modules; - - // Store values of expensive functions for future use. - if (empty($themes) || empty($modules)) { - // We need to do a full rebuild here to make sure the database reflects any - // code changes that were made in the filesystem before the update script - // was initiated. - $themes = system_rebuild_theme_data(); - $modules = system_rebuild_module_data(); - } - - if ($type == 'module' && isset($modules[$name])) { - $file = $modules[$name]; - } - elseif ($type == 'theme' && isset($themes[$name])) { - $file = $themes[$name]; - } - if (!isset($file) - || !isset($file->info['core']) - || $file->info['core'] != DRUPAL_CORE_COMPATIBILITY - || version_compare(phpversion(), $file->info['php']) < 0) { - return TRUE; - } - return FALSE; -} - -/** - * Performs extra steps required to bootstrap when using a Drupal 7 database. - * - * Users who still have a Drupal 7 database (and are in the process of - * updating to Drupal 8) need extra help before a full bootstrap can be - * achieved. This function does the necessary preliminary work that allows - * the bootstrap to be successful. - * - * No access check has been performed when this function is called, so no - * irreversible changes to the database are made here. - */ -function update_prepare_d8_bootstrap() { - // Allow the database system to work even if the registry has not been - // created yet. - include_once DRUPAL_ROOT . '/includes/install.inc'; - drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE); - - // If the site has not updated to Drupal 8 yet, check to make sure that it is - // running an up-to-date version of Drupal 7 before proceeding. Note this has - // to happen AFTER the database bootstraps because of - // drupal_get_installed_schema_version(). - $system_schema = drupal_get_installed_schema_version('system'); - if ($system_schema < 8000) { - $has_required_schema = $system_schema >= REQUIRED_D7_SCHEMA_VERSION; - $requirements = array( - 'drupal 7 version' => array( - 'title' => 'Drupal 7 version', - 'value' => $has_required_schema ? 'You are running a current version of Drupal 7.' : 'You are not running a current version of Drupal 7', - 'severity' => $has_required_schema ? REQUIREMENT_OK : REQUIREMENT_ERROR, - 'description' => $has_required_schema ? '' : 'Please update your Drupal 7 installation to the most recent version before attempting to upgrade to Drupal 8', - ), - ); - } -} - -/** - * Perform Drupal 7.x to 8.x updates that are required for update.php - * to function properly. - * - * This function runs when update.php is run the first time for 8.x, - * even before updates are selected or performed. It is important - * that if updates are not ultimately performed that no changes are - * made which make it impossible to continue using the prior version. - */ -function update_fix_d8_requirements() { - global $conf; - - if (drupal_get_installed_schema_version('system') < 8000 && !variable_get('update_d8_requirements', FALSE)) { - // @todo: Make critical, first-run changes to the database here. - variable_set('update_d8_requirements', TRUE); - } -} - -/** - * Perform one update and store the results for display on finished page. - * - * If an update function completes successfully, it should return a message - * as a string indicating success, for example: - * @code - * return t('New index added successfully.'); - * @endcode - * - * Alternatively, it may return nothing. In that case, no message - * will be displayed at all. - * - * If it fails for whatever reason, it should throw an instance of - * DrupalUpdateException with an appropriate error message, for example: - * @code - * throw new DrupalUpdateException(t('Description of what went wrong')); - * @endcode - * - * If an exception is thrown, the current update and all updates that depend on - * it will be aborted. The schema version will not be updated in this case, and - * all the aborted updates will continue to appear on update.php as updates - * that have not yet been run. - * - * If an update function needs to be re-run as part of a batch process, it - * should accept the $sandbox array by reference as its first parameter - * and set the #finished property to the percentage completed that it is, as a - * fraction of 1. - * - * @param $module - * The module whose update will be run. - * @param $number - * The update number to run. - * @param $dependency_map - * An array whose keys are the names of all update functions that will be - * performed during this batch process, and whose values are arrays of other - * update functions that each one depends on. - * @param $context - * The batch context array. - * - * @see update_resolve_dependencies() - */ -function update_do_one($module, $number, $dependency_map, &$context) { - $function = $module . '_update_' . $number; - - // If this update was aborted in a previous step, or has a dependency that - // was aborted in a previous step, go no further. - if (!empty($context['results']['#abort']) && array_intersect($context['results']['#abort'], array_merge($dependency_map, array($function)))) { - return; - } - - $ret = array(); - if (function_exists($function)) { - try { - $ret['results']['query'] = $function($context['sandbox']); - $ret['results']['success'] = TRUE; - } - // @TODO We may want to do different error handling for different - // exception types, but for now we'll just log the exception and - // return the message for printing. - catch (Exception $e) { - watchdog_exception('update', $e); - - require_once DRUPAL_ROOT . '/includes/errors.inc'; - $variables = _drupal_decode_exception($e); - // The exception message is run through check_plain() by _drupal_decode_exception(). - $ret['#abort'] = array('success' => FALSE, 'query' => t('%type: !message in %function (line %line of %file).', $variables)); - } - } - - if (isset($context['sandbox']['#finished'])) { - $context['finished'] = $context['sandbox']['#finished']; - unset($context['sandbox']['#finished']); - } - - if (!isset($context['results'][$module])) { - $context['results'][$module] = array(); - } - if (!isset($context['results'][$module][$number])) { - $context['results'][$module][$number] = array(); - } - $context['results'][$module][$number] = array_merge($context['results'][$module][$number], $ret); - - if (!empty($ret['#abort'])) { - // Record this function in the list of updates that were aborted. - $context['results']['#abort'][] = $function; - } - - // Record the schema update if it was completed successfully. - if ($context['finished'] == 1 && empty($ret['#abort'])) { - drupal_set_installed_schema_version($module, $number); - } - - $context['message'] = 'Updating ' . check_plain($module) . ' module'; -} - -/** - * @class Exception class used to throw error if a module update fails. - */ -class DrupalUpdateException extends Exception { } - -/** - * Start the database update batch process. - * - * @param $start - * An array whose keys contain the names of modules to be updated during the - * current batch process, and whose values contain the number of the first - * requested update for that module. The actual updates that are run (and the - * order they are run in) will depend on the results of passing this data - * through the update dependency system. - * @param $redirect - * Path to redirect to when the batch has finished processing. - * @param $url - * URL of the batch processing page (should only be used for separate - * scripts like update.php). - * @param $batch - * Optional parameters to pass into the batch API. - * @param $redirect_callback - * (optional) Specify a function to be called to redirect to the progressive - * processing page. - * - * @see update_resolve_dependencies() - */ -function update_batch($start, $redirect = NULL, $url = NULL, $batch = array(), $redirect_callback = 'drupal_goto') { - // During the update, bring the site offline so that schema changes do not - // affect visiting users. - $_SESSION['maintenance_mode'] = variable_get('maintenance_mode', FALSE); - if ($_SESSION['maintenance_mode'] == FALSE) { - variable_set('maintenance_mode', TRUE); - } - - // Resolve any update dependencies to determine the actual updates that will - // be run and the order they will be run in. - $updates = update_resolve_dependencies($start); - - // Store the dependencies for each update function in an array which the - // batch API can pass in to the batch operation each time it is called. (We - // do not store the entire update dependency array here because it is - // potentially very large.) - $dependency_map = array(); - foreach ($updates as $function => $update) { - $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : array(); - } - - $operations = array(); - foreach ($updates as $update) { - if ($update['allowed']) { - // Set the installed version of each module so updates will start at the - // correct place. (The updates are already sorted, so we can simply base - // this on the first one we come across in the above foreach loop.) - if (isset($start[$update['module']])) { - drupal_set_installed_schema_version($update['module'], $update['number'] - 1); - unset($start[$update['module']]); - } - // Add this update function to the batch. - $function = $update['module'] . '_update_' . $update['number']; - $operations[] = array('update_do_one', array($update['module'], $update['number'], $dependency_map[$function])); - } - } - $batch['operations'] = $operations; - $batch += array( - 'title' => 'Updating', - 'init_message' => 'Starting updates', - 'error_message' => 'An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.', - 'finished' => 'update_finished', - 'file' => 'includes/update.inc', - ); - batch_set($batch); - batch_process($redirect, $url, $redirect_callback); -} - -/** - * Finish the update process and store results for eventual display. - * - * After the updates run, all caches are flushed. The update results are - * stored into the session (for example, to be displayed on the update results - * page in update.php). Additionally, if the site was off-line, now that the - * update process is completed, the site is set back online. - * - * @param $success - * Indicate that the batch API tasks were all completed successfully. - * @param $results - * An array of all the results that were updated in update_do_one(). - * @param $operations - * A list of all the operations that had not been completed by the batch API. - * - * @see update_batch() - */ -function update_finished($success, $results, $operations) { - // Clear the caches in case the data has been updated. - drupal_flush_all_caches(); - - $_SESSION['update_results'] = $results; - $_SESSION['update_success'] = $success; - $_SESSION['updates_remaining'] = $operations; - - // Now that the update is done, we can put the site back online if it was - // previously in maintenance mode. - if (isset($_SESSION['maintenance_mode']) && $_SESSION['maintenance_mode'] == FALSE) { - variable_set('maintenance_mode', FALSE); - unset($_SESSION['maintenance_mode']); - } -} - -/** - * Return a list of all the pending database updates. - * - * @return - * An associative array keyed by module name which contains all information - * about database updates that need to be run, and any updates that are not - * going to proceed due to missing requirements. The system module will - * always be listed first. - * - * The subarray for each module can contain the following keys: - * - start: The starting update that is to be processed. If this does not - * exist then do not process any updates for this module as there are - * other requirements that need to be resolved. - * - warning: Any warnings about why this module can not be updated. - * - pending: An array of all the pending updates for the module including - * the update number and the description from source code comment for - * each update function. This array is keyed by the update number. - */ -function update_get_update_list() { - // Make sure that the system module is first in the list of updates. - $ret = array('system' => array()); - - $modules = drupal_get_installed_schema_version(NULL, FALSE, TRUE); - foreach ($modules as $module => $schema_version) { - // Skip uninstalled and incompatible modules. - if ($schema_version == SCHEMA_UNINSTALLED || update_check_incompatibility($module)) { - continue; - } - // Otherwise, get the list of updates defined by this module. - $updates = drupal_get_schema_versions($module); - if ($updates !== FALSE) { - // module_invoke returns NULL for nonexisting hooks, so if no updates - // are removed, it will == 0. - $last_removed = module_invoke($module, 'update_last_removed'); - if ($schema_version < $last_removed) { - $ret[$module]['warning'] = '' . $module . ' module can not be updated. Its schema version is ' . $schema_version . '. Updates up to and including ' . $last_removed . ' have been removed in this release. In order to update ' . $module . ' module, you will first need to upgrade to the last version in which these updates were available.'; - continue; - } - - $updates = drupal_map_assoc($updates); - foreach (array_keys($updates) as $update) { - if ($update > $schema_version) { - // The description for an update comes from its Doxygen. - $func = new ReflectionFunction($module . '_update_' . $update); - $description = str_replace(array("\n", '*', '/'), '', $func->getDocComment()); - $ret[$module]['pending'][$update] = "$update - $description"; - if (!isset($ret[$module]['start'])) { - $ret[$module]['start'] = $update; - } - } - } - if (!isset($ret[$module]['start']) && isset($ret[$module]['pending'])) { - $ret[$module]['start'] = $schema_version; - } - } - } - - if (empty($ret['system'])) { - unset($ret['system']); - } - return $ret; -} - -/** - * Resolves dependencies in a set of module updates, and orders them correctly. - * - * This function receives a list of requested module updates and determines an - * appropriate order to run them in such that all update dependencies are met. - * Any updates whose dependencies cannot be met are included in the returned - * array but have the key 'allowed' set to FALSE; the calling function should - * take responsibility for ensuring that these updates are ultimately not - * performed. - * - * In addition, the returned array also includes detailed information about the - * dependency chain for each update, as provided by the depth-first search - * algorithm in drupal_depth_first_search(). - * - * @param $starting_updates - * An array whose keys contain the names of modules with updates to be run - * and whose values contain the number of the first requested update for that - * module. - * - * @return - * An array whose keys are the names of all update functions within the - * provided modules that would need to be run in order to fulfill the - * request, arranged in the order in which the update functions should be - * run. (This includes the provided starting update for each module and all - * subsequent updates that are available.) The values are themselves arrays - * containing all the keys provided by the drupal_depth_first_search() - * algorithm, which encode detailed information about the dependency chain - * for this update function (for example: 'paths', 'reverse_paths', 'weight', - * and 'component'), as well as the following additional keys: - * - 'allowed': A boolean which is TRUE when the update function's - * dependencies are met, and FALSE otherwise. Calling functions should - * inspect this value before running the update. - * - 'missing_dependencies': An array containing the names of any other - * update functions that are required by this one but that are unavailable - * to be run. This array will be empty when 'allowed' is TRUE. - * - 'module': The name of the module that this update function belongs to. - * - 'number': The number of this update function within that module. - * - * @see drupal_depth_first_search() - */ -function update_resolve_dependencies($starting_updates) { - // Obtain a dependency graph for the requested update functions. - $update_functions = update_get_update_function_list($starting_updates); - $graph = update_build_dependency_graph($update_functions); - - // Perform the depth-first search and sort the results. - require_once DRUPAL_ROOT . '/includes/graph.inc'; - drupal_depth_first_search($graph); - uasort($graph, 'drupal_sort_weight'); - - foreach ($graph as $function => &$data) { - $module = $data['module']; - $number = $data['number']; - // If the update function is missing and has not yet been performed, mark - // it and everything that ultimately depends on it as disallowed. - if (update_is_missing($module, $number, $update_functions) && !update_already_performed($module, $number)) { - $data['allowed'] = FALSE; - foreach (array_keys($data['paths']) as $dependent) { - $graph[$dependent]['allowed'] = FALSE; - $graph[$dependent]['missing_dependencies'][] = $function; - } - } - elseif (!isset($data['allowed'])) { - $data['allowed'] = TRUE; - $data['missing_dependencies'] = array(); - } - // Now that we have finished processing this function, remove it from the - // graph if it was not part of the original list. This ensures that we - // never try to run any updates that were not specifically requested. - if (!isset($update_functions[$module][$number])) { - unset($graph[$function]); - } - } - - return $graph; -} - -/** - * Returns an organized list of update functions for a set of modules. - * - * @param $starting_updates - * An array whose keys contain the names of modules and whose values contain - * the number of the first requested update for that module. - * - * @return - * An array containing all the update functions that should be run for each - * module, including the provided starting update and all subsequent updates - * that are available. The keys of the array contain the module names, and - * each value is an ordered array of update functions, keyed by the update - * number. - * - * @see update_resolve_dependencies() - */ -function update_get_update_function_list($starting_updates) { - // Go through each module and find all updates that we need (including the - // first update that was requested and any updates that run after it). - $update_functions = array(); - foreach ($starting_updates as $module => $version) { - $update_functions[$module] = array(); - $updates = drupal_get_schema_versions($module); - if ($updates !== FALSE) { - $max_version = max($updates); - if ($version <= $max_version) { - foreach ($updates as $update) { - if ($update >= $version) { - $update_functions[$module][$update] = $module . '_update_' . $update; - } - } - } - } - } - return $update_functions; -} - -/** - * Constructs a graph which encodes the dependencies between module updates. - * - * This function returns an associative array which contains a "directed graph" - * representation of the dependencies between a provided list of update - * functions, as well as any outside update functions that they directly depend - * on but that were not in the provided list. The vertices of the graph - * represent the update functions themselves, and each edge represents a - * requirement that the first update function needs to run before the second. - * For example, consider this graph: - * - * system_update_8000 ---> system_update_8001 ---> system_update_8002 - * - * Visually, this indicates that system_update_8000() must run before - * system_update_8001(), which in turn must run before system_update_8002(). - * - * The function takes into account standard dependencies within each module, as - * shown above (i.e., the fact that each module's updates must run in numerical - * order), but also finds any cross-module dependencies that are defined by - * modules which implement hook_update_dependencies(), and builds them into the - * graph as well. - * - * @param $update_functions - * An organized array of update functions, in the format returned by - * update_get_update_function_list(). - * - * @return - * A multidimensional array representing the dependency graph, suitable for - * passing in to drupal_depth_first_search(), but with extra information - * about each update function also included. Each array key contains the name - * of an update function, including all update functions from the provided - * list as well as any outside update functions which they directly depend - * on. Each value is an associative array containing the following keys: - * - 'edges': A representation of any other update functions that immediately - * depend on this one. See drupal_depth_first_search() for more details on - * the format. - * - 'module': The name of the module that this update function belongs to. - * - 'number': The number of this update function within that module. - * - * @see drupal_depth_first_search() - * @see update_resolve_dependencies() - */ -function update_build_dependency_graph($update_functions) { - // Initialize an array that will define a directed graph representing the - // dependencies between update functions. - $graph = array(); - - // Go through each update function and build an initial list of dependencies. - foreach ($update_functions as $module => $functions) { - $previous_function = NULL; - foreach ($functions as $number => $function) { - // Add an edge to the directed graph representing the fact that each - // update function in a given module must run after the update that - // numerically precedes it. - if ($previous_function) { - $graph[$previous_function]['edges'][$function] = TRUE; - } - $previous_function = $function; - - // Define the module and update number associated with this function. - $graph[$function]['module'] = $module; - $graph[$function]['number'] = $number; - } - } - - // Now add any explicit update dependencies declared by modules. - $update_dependencies = update_retrieve_dependencies(); - foreach ($graph as $function => $data) { - if (!empty($update_dependencies[$data['module']][$data['number']])) { - foreach ($update_dependencies[$data['module']][$data['number']] as $module => $number) { - $dependency = $module . '_update_' . $number; - $graph[$dependency]['edges'][$function] = TRUE; - $graph[$dependency]['module'] = $module; - $graph[$dependency]['number'] = $number; - } - } - } - - return $graph; -} - -/** - * Determines if a module update is missing or unavailable. - * - * @param $module - * The name of the module. - * @param $number - * The number of the update within that module. - * @param $update_functions - * An organized array of update functions, in the format returned by - * update_get_update_function_list(). This should represent all module - * updates that are requested to run at the time this function is called. - * - * @return - * TRUE if the provided module update is not installed or is not in the - * provided list of updates to run; FALSE otherwise. - */ -function update_is_missing($module, $number, $update_functions) { - return !isset($update_functions[$module][$number]) || !function_exists($update_functions[$module][$number]); -} - -/** - * Determines if a module update has already been performed. - * - * @param $module - * The name of the module. - * @param $number - * The number of the update within that module. - * - * @return - * TRUE if the database schema indicates that the update has already been - * performed; FALSE otherwise. - */ -function update_already_performed($module, $number) { - return $number <= drupal_get_installed_schema_version($module); -} - -/** - * Invoke hook_update_dependencies() in all installed modules. - * - * This function is similar to module_invoke_all(), with the main difference - * that it does not require that a module be enabled to invoke its hook, only - * that it be installed. This allows the update system to properly perform - * updates even on modules that are currently disabled. - * - * @return - * An array of return values obtained by merging the results of the - * hook_update_dependencies() implementations in all installed modules. - * - * @see module_invoke_all() - * @see hook_update_dependencies() - */ -function update_retrieve_dependencies() { - $return = array(); - // Get a list of installed modules, arranged so that we invoke their hooks in - // the same order that module_invoke_all() does. - $modules = db_query("SELECT name FROM {system} WHERE type = 'module' AND schema_version != :schema ORDER BY weight ASC, name ASC", array(':schema' => SCHEMA_UNINSTALLED))->fetchCol(); - foreach ($modules as $module) { - $function = $module . '_update_dependencies'; - if (function_exists($function)) { - $result = $function(); - // Each implementation of hook_update_dependencies() returns a - // multidimensional, associative array containing some keys that - // represent module names (which are strings) and other keys that - // represent update function numbers (which are integers). We cannot use - // array_merge_recursive() to properly merge these results, since it - // treats strings and integers differently. Therefore, we have to - // explicitly loop through the expected array structure here and perform - // the merge manually. - if (isset($result) && is_array($result)) { - foreach ($result as $module => $module_data) { - foreach ($module_data as $update => $update_data) { - foreach ($update_data as $module_dependency => $update_dependency) { - // If there are redundant dependencies declared for the same - // update function (so that it is declared to depend on more than - // one update from a particular module), record the dependency on - // the highest numbered update here, since that automatically - // implies the previous ones. For example, if one module's - // implementation of hook_update_dependencies() required this - // ordering: - // - // system_update_8001 ---> user_update_8000 - // - // but another module's implementation of the hook required this - // one: - // - // system_update_8002 ---> user_update_8000 - // - // we record the second one, since system_update_8001() is always - // guaranteed to run before system_update_8002() anyway (within - // an individual module, updates are always run in numerical - // order). - if (!isset($return[$module][$update][$module_dependency]) || $update_dependency > $return[$module][$update][$module_dependency]) { - $return[$module][$update][$module_dependency] = $update_dependency; - } - } - } - } - } - } - } - - return $return; -} - -/** - * @defgroup update-api-7.x-to-8.x Update versions of API functions - * @{ - * Functions similar to normal API function but not firing hooks. - * - * During update, it is impossible to judge the consequences of firing a hook - * as it might hit a module not yet updated. So simplified versions of some - * core APIs are provided. - */ - -/** - * @} End of "defgroup update-api-7.x-to-8.x" - */ diff --git a/index.php b/index.php index 8b83199..b91fb1e 100644 --- a/index.php +++ b/index.php @@ -8,7 +8,7 @@ * prints the appropriate page. * * All Drupal code is released under the GNU General Public License. - * See COPYRIGHT.txt and LICENSE.txt. + * See COPYRIGHT.txt and LICENSE.txt files in the "core" directory. */ /** @@ -16,6 +16,6 @@ */ define('DRUPAL_ROOT', getcwd()); -require_once DRUPAL_ROOT . '/includes/bootstrap.inc'; +require_once DRUPAL_ROOT . '/core/includes/bootstrap.inc'; drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); menu_execute_active_handler(); diff --git a/install.php b/install.php deleted file mode 100644 index 20576b2..0000000 --- a/install.php +++ /dev/null @@ -1,29 +0,0 @@ -system requirements page for more information.'; - exit; -} - -// Start the installer. -require_once DRUPAL_ROOT . '/includes/install.core.inc'; -install_drupal(); diff --git a/modules/aggregator/aggregator.test b/modules/aggregator/aggregator.test deleted file mode 100644 index 1ab12dc..0000000 --- a/modules/aggregator/aggregator.test +++ /dev/null @@ -1,859 +0,0 @@ -drupalCreateUser(array('administer news feeds', 'access news feeds', 'create article content')); - $this->drupalLogin($web_user); - } - - /** - * Create an aggregator feed (simulate form submission on admin/config/services/aggregator/add/feed). - * - * @param $feed_url - * If given, feed will be created with this URL, otherwise /rss.xml will be used. - * @return $feed - * Full feed object if possible. - * - * @see getFeedEditArray() - */ - function createFeed($feed_url = NULL) { - $edit = $this->getFeedEditArray($feed_url); - $this->drupalPost('admin/config/services/aggregator/add/feed', $edit, t('Save')); - $this->assertRaw(t('The feed %name has been added.', array('%name' => $edit['title'])), t('The feed !name has been added.', array('!name' => $edit['title']))); - - $feed = db_query("SELECT * FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $edit['title'], ':url' => $edit['url']))->fetch(); - $this->assertTrue(!empty($feed), t('The feed found in database.')); - return $feed; - } - - /** - * Delete an aggregator feed. - * - * @param $feed - * Feed object representing the feed. - */ - function deleteFeed($feed) { - $this->drupalPost('admin/config/services/aggregator/edit/feed/' . $feed->fid, array(), t('Delete')); - $this->assertRaw(t('The feed %title has been deleted.', array('%title' => $feed->title)), t('Feed deleted successfully.')); - } - - /** - * Return a randomly generated feed edit array. - * - * @param $feed_url - * If given, feed will be created with this URL, otherwise /rss.xml will be used. - * @return - * A feed array. - */ - function getFeedEditArray($feed_url = NULL) { - $feed_name = $this->randomName(10); - if (!$feed_url) { - $feed_url = url('rss.xml', array( - 'query' => array('feed' => $feed_name), - 'absolute' => TRUE, - )); - } - $edit = array( - 'title' => $feed_name, - 'url' => $feed_url, - 'refresh' => '900', - ); - return $edit; - } - - /** - * Return the count of the randomly created feed array. - * - * @return - * Number of feed items on default feed created by createFeed(). - */ - function getDefaultFeedItemCount() { - // Our tests are based off of rss.xml, so let's find out how many elements should be related. - $feed_count = db_query_range('SELECT COUNT(*) FROM {node} n WHERE n.promote = 1 AND n.status = 1', 0, variable_get('feed_default_items', 10))->fetchField(); - return $feed_count > 10 ? 10 : $feed_count; - } - - /** - * Update feed items (simulate click to admin/config/services/aggregator/update/$fid). - * - * @param $feed - * Feed object representing the feed. - * @param $expected_count - * Expected number of feed items. - */ - function updateFeedItems(&$feed, $expected_count) { - // First, let's ensure we can get to the rss xml. - $this->drupalGet($feed->url); - $this->assertResponse(200, t('!url is reachable.', array('!url' => $feed->url))); - - // Refresh the feed (simulated link click). - $this->drupalGet('admin/config/services/aggregator/update/' . $feed->fid); - - // Ensure we have the right number of items. - $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid)); - $items = array(); - $feed->items = array(); - foreach ($result as $item) { - $feed->items[] = $item->iid; - } - $feed->item_count = count($feed->items); - $this->assertEqual($expected_count, $feed->item_count, t('Total items in feed equal to the total items in database (!val1 != !val2)', array('!val1' => $expected_count, '!val2' => $feed->item_count))); - } - - /** - * Confirm item removal from a feed. - * - * @param $feed - * Feed object representing the feed. - */ - function removeFeedItems($feed) { - $this->drupalPost('admin/config/services/aggregator/remove/' . $feed->fid, array(), t('Remove items')); - $this->assertRaw(t('The news items from %title have been removed.', array('%title' => $feed->title)), t('Feed items removed.')); - } - - /** - * Add and remove feed items and ensure that the count is zero. - * - * @param $feed - * Feed object representing the feed. - * @param $expected_count - * Expected number of feed items. - */ - function updateAndRemove($feed, $expected_count) { - $this->updateFeedItems($feed, $expected_count); - $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(); - $this->assertTrue($count); - $this->removeFeedItems($feed); - $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(); - $this->assertTrue($count == 0); - } - - /** - * Pull feed categories from aggregator_category_feed table. - * - * @param $feed - * Feed object representing the feed. - */ - function getFeedCategories($feed) { - // add the categories to the feed so we can use them - $result = db_query('SELECT cid FROM {aggregator_category_feed} WHERE fid = :fid', array(':fid' => $feed->fid)); - foreach ($result as $category) { - $feed->categories[] = $category->cid; - } - } - - /** - * Pull categories from aggregator_category table. - */ - function getCategories() { - $categories = array(); - $result = db_query('SELECT * FROM {aggregator_category}'); - foreach ($result as $category) { - $categories[$category->cid] = $category; - } - return $categories; - } - - - /** - * Check if the feed name and url is unique. - * - * @param $feed_name - * String containing the feed name to check. - * @param $feed_url - * String containing the feed url to check. - * @return - * TRUE if feed is unique. - */ - function uniqueFeed($feed_name, $feed_url) { - $result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $feed_name, ':url' => $feed_url))->fetchField(); - return (1 == $result); - } - - /** - * Create a valid OPML file from an array of feeds. - * - * @param $feeds - * An array of feeds. - * @return - * Path to valid OPML file. - */ - function getValidOpml($feeds) { - // Properly escape URLs so that XML parsers don't choke on them. - foreach ($feeds as &$feed) { - $feed['url'] = htmlspecialchars($feed['url']); - } - /** - * Does not have an XML declaration, must pass the parser. - */ - $opml = << - - - - - - - - - - - - - - - - - -EOF; - - $path = 'public://valid-opml.xml'; - return file_unmanaged_save_data($opml, $path); - } - - /** - * Create an invalid OPML file. - * - * @return - * Path to invalid OPML file. - */ - function getInvalidOpml() { - $opml = << - - -EOF; - - $path = 'public://invalid-opml.xml'; - return file_unmanaged_save_data($opml, $path); - } - - /** - * Create a valid but empty OPML file. - * - * @return - * Path to empty OPML file. - */ - function getEmptyOpml() { - $opml = << - - - - - - - -EOF; - - $path = 'public://empty-opml.xml'; - return file_unmanaged_save_data($opml, $path); - } - - function getRSS091Sample() { - return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_rss091.xml'; - } - - function getAtomSample() { - // The content of this sample ATOM feed is based directly off of the - // example provided in RFC 4287. - return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_atom.xml'; - } - - function createSampleNodes() { - $langcode = LANGUAGE_NONE; - // Post 5 articles. - for ($i = 0; $i < 5; $i++) { - $edit = array(); - $edit['title'] = $this->randomName(); - $edit["body[$langcode][0][value]"] = $this->randomName(); - $this->drupalPost('node/add/article', $edit, t('Save')); - } - } -} - -class AddFeedTestCase extends AggregatorTestCase { - public static function getInfo() { - return array( - 'name' => 'Add feed functionality', - 'description' => 'Add feed test.', - 'group' => 'Aggregator' - ); - } - - /** - * Create a feed, ensure that it is unique, check the source, and delete the feed. - */ - function testAddFeed() { - $feed = $this->createFeed(); - - // Check feed data. - $this->assertEqual($this->getUrl(), url('admin/config/services/aggregator/add/feed', array('absolute' => TRUE)), t('Directed to correct url.')); - $this->assertTrue($this->uniqueFeed($feed->title, $feed->url), t('The feed is unique.')); - - // Check feed source. - $this->drupalGet('aggregator/sources/' . $feed->fid); - $this->assertResponse(200, t('Feed source exists.')); - $this->assertText($feed->title, t('Page title')); - $this->drupalGet('aggregator/sources/' . $feed->fid . '/categorize'); - $this->assertResponse(200, t('Feed categorization page exists.')); - - // Delete feed. - $this->deleteFeed($feed); - } -} - -class CategorizeFeedTestCase extends AggregatorTestCase { - public static function getInfo() { - return array( - 'name' => 'Categorize feed functionality', - 'description' => 'Categorize feed test.', - 'group' => 'Aggregator' - ); - } - - /** - * Create a feed and make sure you can add more than one category to it. - */ - function testCategorizeFeed() { - - // Create 2 categories. - $category_1 = array('title' => $this->randomName(10), 'description' => ''); - $this->drupalPost('admin/config/services/aggregator/add/category', $category_1, t('Save')); - $this->assertRaw(t('The category %title has been added.', array('%title' => $category_1['title'])), t('The category %title has been added.', array('%title' => $category_1['title']))); - - $category_2 = array('title' => $this->randomName(10), 'description' => ''); - $this->drupalPost('admin/config/services/aggregator/add/category', $category_2, t('Save')); - $this->assertRaw(t('The category %title has been added.', array('%title' => $category_2['title'])), t('The category %title has been added.', array('%title' => $category_2['title']))); - - // Get categories from database. - $categories = $this->getCategories(); - - // Create a feed and assign 2 categories to it. - $feed = $this->getFeedEditArray(); - $feed['block'] = 5; - foreach ($categories as $cid => $category) { - $feed['category'][$cid] = $cid; - } - - // Use aggregator_save_feed() function to save the feed. - aggregator_save_feed($feed); - $db_feed = db_query("SELECT * FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $feed['title'], ':url' => $feed['url']))->fetch(); - - // Assert the feed has two categories. - $this->getFeedCategories($db_feed); - $this->assertEqual(count($db_feed->categories), 2, t('Feed has 2 categories')); - } -} - -class UpdateFeedTestCase extends AggregatorTestCase { - public static function getInfo() { - return array( - 'name' => 'Update feed functionality', - 'description' => 'Update feed test.', - 'group' => 'Aggregator' - ); - } - - /** - * Create a feed and attempt to update it. - */ - function testUpdateFeed() { - $remamining_fields = array('title', 'url', ''); - foreach ($remamining_fields as $same_field) { - $feed = $this->createFeed(); - - // Get new feed data array and modify newly created feed. - $edit = $this->getFeedEditArray(); - $edit['refresh'] = 1800; // Change refresh value. - if (isset($feed->{$same_field})) { - $edit[$same_field] = $feed->{$same_field}; - } - $this->drupalPost('admin/config/services/aggregator/edit/feed/' . $feed->fid, $edit, t('Save')); - $this->assertRaw(t('The feed %name has been updated.', array('%name' => $edit['title'])), t('The feed %name has been updated.', array('%name' => $edit['title']))); - - // Check feed data. - $this->assertEqual($this->getUrl(), url('admin/config/services/aggregator/', array('absolute' => TRUE))); - $this->assertTrue($this->uniqueFeed($edit['title'], $edit['url']), t('The feed is unique.')); - - // Check feed source. - $this->drupalGet('aggregator/sources/' . $feed->fid); - $this->assertResponse(200, t('Feed source exists.')); - $this->assertText($edit['title'], t('Page title')); - - // Delete feed. - $feed->title = $edit['title']; // Set correct title so deleteFeed() will work. - $this->deleteFeed($feed); - } - } -} - -class RemoveFeedTestCase extends AggregatorTestCase { - public static function getInfo() { - return array( - 'name' => 'Remove feed functionality', - 'description' => 'Remove feed test.', - 'group' => 'Aggregator' - ); - } - - /** - * Remove a feed and ensure that all it services are removed. - */ - function testRemoveFeed() { - $feed = $this->createFeed(); - - // Delete feed. - $this->deleteFeed($feed); - - // Check feed source. - $this->drupalGet('aggregator/sources/' . $feed->fid); - $this->assertResponse(404, t('Deleted feed source does not exists.')); - - // Check database for feed. - $result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $feed->title, ':url' => $feed->url))->fetchField(); - $this->assertFalse($result, t('Feed not found in database')); - } -} - -class UpdateFeedItemTestCase extends AggregatorTestCase { - public static function getInfo() { - return array( - 'name' => 'Update feed item functionality', - 'description' => 'Update feed items from a feed.', - 'group' => 'Aggregator' - ); - } - - /** - * Test running "update items" from the 'admin/config/services/aggregator' page. - */ - function testUpdateFeedItem() { - $this->createSampleNodes(); - - // Create a feed and test updating feed items if possible. - $feed = $this->createFeed(); - if (!empty($feed)) { - $this->updateFeedItems($feed, $this->getDefaultFeedItemCount()); - $this->removeFeedItems($feed); - } - - // Delete feed. - $this->deleteFeed($feed); - - // Test updating feed items without valid timestamp information. - $edit = array( - 'title' => "Feed without publish timestamp", - 'url' => $this->getRSS091Sample(), - ); - - $this->drupalGet($edit['url']); - $this->assertResponse(array(200), t('URL !url is accessible', array('!url' => $edit['url']))); - - $this->drupalPost('admin/config/services/aggregator/add/feed', $edit, t('Save')); - $this->assertRaw(t('The feed %name has been added.', array('%name' => $edit['title'])), t('The feed !name has been added.', array('!name' => $edit['title']))); - - $feed = db_query("SELECT * FROM {aggregator_feed} WHERE url = :url", array(':url' => $edit['url']))->fetchObject(); - $this->drupalGet('admin/config/services/aggregator/update/' . $feed->fid); - - $before = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(); - - // Sleep for 3 second. - sleep(3); - db_update('aggregator_feed') - ->condition('fid', $feed->fid) - ->fields(array( - 'checked' => 0, - 'hash' => '', - 'etag' => '', - 'modified' => 0, - )) - ->execute(); - $this->drupalGet('admin/config/services/aggregator/update/' . $feed->fid); - - $after = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(); - - $this->assertTrue($before === $after, t('Publish timestamp of feed item was not updated (!before === !after)', array('!before' => $before, '!after' => $after))); - } -} - -class RemoveFeedItemTestCase extends AggregatorTestCase { - public static function getInfo() { - return array( - 'name' => 'Remove feed item functionality', - 'description' => 'Remove feed items from a feed.', - 'group' => 'Aggregator' - ); - } - - /** - * Test running "remove items" from the 'admin/config/services/aggregator' page. - */ - function testRemoveFeedItem() { - // Create a bunch of test feeds. - $feed_urls = array(); - // No last-modified, no etag. - $feed_urls[] = url('aggregator/test-feed', array('absolute' => TRUE)); - // Last-modified, but no etag. - $feed_urls[] = url('aggregator/test-feed/1', array('absolute' => TRUE)); - // No Last-modified, but etag. - $feed_urls[] = url('aggregator/test-feed/0/1', array('absolute' => TRUE)); - // Last-modified and etag. - $feed_urls[] = url('aggregator/test-feed/1/1', array('absolute' => TRUE)); - - foreach ($feed_urls as $feed_url) { - $feed = $this->createFeed($feed_url); - // Update and remove items two times in a row to make sure that removal - // resets all 'modified' information (modified, etag, hash) and allows for - // immediate update. - $this->updateAndRemove($feed, 2); - $this->updateAndRemove($feed, 2); - $this->updateAndRemove($feed, 2); - // Delete feed. - $this->deleteFeed($feed); - } - } -} - -class CategorizeFeedItemTestCase extends AggregatorTestCase { - public static function getInfo() { - return array( - 'name' => 'Categorize feed item functionality', - 'description' => 'Test feed item categorization.', - 'group' => 'Aggregator' - ); - } - - /** - * If a feed has a category, make sure that the children inherit that - * categorization. - */ - function testCategorizeFeedItem() { - $this->createSampleNodes(); - - // Simulate form submission on "admin/config/services/aggregator/add/category". - $edit = array('title' => $this->randomName(10), 'description' => ''); - $this->drupalPost('admin/config/services/aggregator/add/category', $edit, t('Save')); - $this->assertRaw(t('The category %title has been added.', array('%title' => $edit['title'])), t('The category %title has been added.', array('%title' => $edit['title']))); - - $category = db_query("SELECT * FROM {aggregator_category} WHERE title = :title", array(':title' => $edit['title']))->fetch(); - $this->assertTrue(!empty($category), t('The category found in database.')); - - $link_path = 'aggregator/categories/' . $category->cid; - $menu_link = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path", array(':link_path' => $link_path))->fetch(); - $this->assertTrue(!empty($menu_link), t('The menu link associated with the category found in database.')); - - $feed = $this->createFeed(); - db_insert('aggregator_category_feed') - ->fields(array( - 'cid' => $category->cid, - 'fid' => $feed->fid, - )) - ->execute(); - $this->updateFeedItems($feed, $this->getDefaultFeedItemCount()); - $this->getFeedCategories($feed); - $this->assertTrue(!empty($feed->categories), t('The category found in the feed.')); - - // For each category of a feed, ensure feed items have that category, too. - if (!empty($feed->categories) && !empty($feed->items)) { - foreach ($feed->categories as $category) { - $categorized_count = db_select('aggregator_category_item') - ->condition('iid', $feed->items, 'IN') - ->countQuery() - ->execute() - ->fetchField(); - - $this->assertEqual($feed->item_count, $categorized_count, t('Total items in feed equal to the total categorized feed items in database')); - } - } - - // Delete feed. - $this->deleteFeed($feed); - } -} - -class ImportOPMLTestCase extends AggregatorTestCase { - public static function getInfo() { - return array( - 'name' => 'Import feeds from OPML functionality', - 'description' => 'Test OPML import.', - 'group' => 'Aggregator', - ); - } - - /** - * Open OPML import form. - */ - function openImportForm() { - db_delete('aggregator_category')->execute(); - - $category = $this->randomName(10); - $cid = db_insert('aggregator_category') - ->fields(array( - 'title' => $category, - 'description' => '', - )) - ->execute(); - - $this->drupalGet('admin/config/services/aggregator/add/opml'); - $this->assertText('A single OPML document may contain a collection of many feeds.', t('Found OPML help text.')); - $this->assertField('files[upload]', t('Found file upload field.')); - $this->assertField('remote', t('Found Remote URL field.')); - $this->assertField('refresh', '', t('Found Refresh field.')); - $this->assertFieldByName("category[$cid]", $cid, t('Found category field.')); - } - - /** - * Submit form filled with invalid fields. - */ - function validateImportFormFields() { - $before = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField(); - - $edit = array(); - $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import')); - $this->assertRaw(t('You must either upload a file or enter a URL.'), t('Error if no fields are filled.')); - - $path = $this->getEmptyOpml(); - $edit = array( - 'files[upload]' => $path, - 'remote' => file_create_url($path), - ); - $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import')); - $this->assertRaw(t('You must either upload a file or enter a URL.'), t('Error if both fields are filled.')); - - $edit = array('remote' => 'invalidUrl://empty'); - $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import')); - $this->assertText(t('This URL is not valid.'), t('Error if the URL is invalid.')); - - $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField(); - $this->assertEqual($before, $after, t('No feeds were added during the three last form submissions.')); - } - - /** - * Submit form with invalid, empty and valid OPML files. - */ - function submitImportForm() { - $before = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField(); - - $form['files[upload]'] = $this->getInvalidOpml(); - $this->drupalPost('admin/config/services/aggregator/add/opml', $form, t('Import')); - $this->assertText(t('No new feed has been added.'), t('Attempting to upload invalid XML.')); - - $edit = array('remote' => file_create_url($this->getEmptyOpml())); - $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import')); - $this->assertText(t('No new feed has been added.'), t('Attempting to load empty OPML from remote URL.')); - - $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField(); - $this->assertEqual($before, $after, t('No feeds were added during the two last form submissions.')); - - db_delete('aggregator_feed')->execute(); - db_delete('aggregator_category')->execute(); - db_delete('aggregator_category_feed')->execute(); - - $category = $this->randomName(10); - db_insert('aggregator_category') - ->fields(array( - 'cid' => 1, - 'title' => $category, - 'description' => '', - )) - ->execute(); - - $feeds[0] = $this->getFeedEditArray(); - $feeds[1] = $this->getFeedEditArray(); - $feeds[2] = $this->getFeedEditArray(); - $edit = array( - 'files[upload]' => $this->getValidOpml($feeds), - 'refresh' => '900', - 'category[1]' => $category, - ); - $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import')); - $this->assertRaw(t('A feed with the URL %url already exists.', array('%url' => $feeds[0]['url'])), t('Verifying that a duplicate URL was identified')); - $this->assertRaw(t('A feed named %title already exists.', array('%title' => $feeds[1]['title'])), t('Verifying that a duplicate title was identified')); - - $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField(); - $this->assertEqual($after, 2, t('Verifying that two distinct feeds were added.')); - - $feeds_from_db = db_query("SELECT f.title, f.url, f.refresh, cf.cid FROM {aggregator_feed} f LEFT JOIN {aggregator_category_feed} cf ON f.fid = cf.fid"); - $refresh = $category = TRUE; - foreach ($feeds_from_db as $feed) { - $title[$feed->url] = $feed->title; - $url[$feed->title] = $feed->url; - $category = $category && $feed->cid == 1; - $refresh = $refresh && $feed->refresh == 900; - } - - $this->assertEqual($title[$feeds[0]['url']], $feeds[0]['title'], t('First feed was added correctly.')); - $this->assertEqual($url[$feeds[1]['title']], $feeds[1]['url'], t('Second feed was added correctly.')); - $this->assertTrue($refresh, t('Refresh times are correct.')); - $this->assertTrue($category, t('Categories are correct.')); - } - - function testOPMLImport() { - $this->openImportForm(); - $this->validateImportFormFields(); - $this->submitImportForm(); - } -} - -class AggregatorCronTestCase extends AggregatorTestCase { - public static function getInfo() { - return array( - 'name' => 'Update on cron functionality', - 'description' => 'Update feeds on cron.', - 'group' => 'Aggregator' - ); - } - - /** - * Add feeds update them on cron. - */ - public function testCron() { - // Create feed and test basic updating on cron. - global $base_url; - $key = variable_get('cron_key', 'drupal'); - $this->createSampleNodes(); - $feed = $this->createFeed(); - $this->drupalGet($base_url . '/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key))); - $this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.'); - $this->removeFeedItems($feed); - $this->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.'); - $this->drupalGet($base_url . '/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key))); - $this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.'); - - // Test feed locking when queued for update. - $this->removeFeedItems($feed); - db_update('aggregator_feed') - ->condition('fid', $feed->fid) - ->fields(array( - 'queued' => REQUEST_TIME, - )) - ->execute(); - $this->drupalGet($base_url . '/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key))); - $this->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.'); - db_update('aggregator_feed') - ->condition('fid', $feed->fid) - ->fields(array( - 'queued' => 0, - )) - ->execute(); - $this->drupalGet($base_url . '/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key))); - $this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.'); - } -} - -class AggregatorRenderingTestCase extends AggregatorTestCase { - public static function getInfo() { - return array( - 'name' => 'Checks display of aggregator items', - 'description' => 'Checks display of aggregator items on the page.', - 'group' => 'Aggregator' - ); - } - - /** - * Add a feed block to the page and checks its links. - * - * TODO: Test the category block as well. - */ - public function testBlockLinks() { - // Create feed. - $this->createSampleNodes(); - $feed = $this->createFeed(); - $this->updateFeedItems($feed, $this->getDefaultFeedItemCount()); - - // Place block on page (@see block.test:moveBlockToRegion()) - // Need admin user to be able to access block admin. - $this->admin_user = $this->drupalCreateUser(array( - 'administer blocks', - 'access administration pages', - 'administer news feeds', - 'access news feeds', - )); - $this->drupalLogin($this->admin_user); - - // Prepare to use the block admin form. - $block = array( - 'module' => 'aggregator', - 'delta' => 'feed-' . $feed->fid, - 'title' => $feed->title, - ); - $region = 'footer'; - $edit = array(); - $edit['blocks[' . $block['module'] . '_' . $block['delta'] . '][region]'] = $region; - // Check the feed block is available in the block list form. - $this->drupalGet('admin/structure/block'); - $this->assertFieldByName('blocks[' . $block['module'] . '_' . $block['delta'] . '][region]', '', 'Aggregator feed block is available for positioning.'); - // Position it. - $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); - $this->assertText(t('The block settings have been updated.'), t('Block successfully moved to %region_name region.', array( '%region_name' => $region))); - // Confirm that the block is now being displayed on pages. - $this->drupalGet('node'); - $this->assertText(t($block['title']), t('Feed block is displayed on the page.')); - - // Find the expected read_more link. - $href = 'aggregator/sources/' . $feed->fid; - $links = $this->xpath('//a[@href = :href]', array(':href' => url($href))); - $this->assert(isset($links[0]), t('Link to href %href found.', array('%href' => $href))); - - // Visit that page. - $this->drupalGet($href); - $correct_titles = $this->xpath('//h1[normalize-space(text())=:title]', array(':title' => $feed->title)); - $this->assertFalse(empty($correct_titles), t('Aggregator feed page is available and has the correct title.')); - } -} - -/** - * Tests for feed parsing. - */ -class FeedParserTestCase extends AggregatorTestCase { - public static function getInfo() { - return array( - 'name' => 'Feed parser functionality', - 'description' => 'Test the built-in feed parser with valid feed samples.', - 'group' => 'Aggregator', - ); - } - - function setUp() { - parent::setUp(); - // Do not remove old aggregator items during these tests, since our sample - // feeds have hardcoded dates in them (which may be expired when this test - // is run). - variable_set('aggregator_clear', AGGREGATOR_CLEAR_NEVER); - } - - /** - * Test a feed that uses the RSS 0.91 format. - */ - function testRSS091Sample() { - $feed = $this->createFeed($this->getRSS091Sample()); - aggregator_refresh($feed); - $this->drupalGet('aggregator/sources/' . $feed->fid); - $this->assertResponse(200, t('Feed %name exists.', array('%name' => $feed->title))); - $this->assertText('First example feed item title'); - $this->assertLinkByHref('http://example.com/example-turns-one'); - $this->assertText('First example feed item description.'); - } - - /** - * Test a feed that uses the Atom format. - */ - function testAtomSample() { - $feed = $this->createFeed($this->getAtomSample()); - aggregator_refresh($feed); - $this->drupalGet('aggregator/sources/' . $feed->fid); - $this->assertResponse(200, t('Feed %name exists.', array('%name' => $feed->title))); - $this->assertText('Atom-Powered Robots Run Amok'); - $this->assertLinkByHref('http://example.org/2003/12/13/atom03'); - $this->assertText('Some text.'); - $this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a', db_query('SELECT guid FROM {aggregator_item} WHERE link = :link', array(':link' => 'http://example.org/2003/12/13/atom03'))->fetchField(), 'Atom entry id element is parsed correctly.'); - } -} - diff --git a/modules/block/block-admin-display-form.tpl.php b/modules/block/block-admin-display-form.tpl.php deleted file mode 100644 index 94b6104..0000000 --- a/modules/block/block-admin-display-form.tpl.php +++ /dev/null @@ -1,67 +0,0 @@ -region_title: Region title for the listed block. - * - $data->block_title: Block title. - * - $data->region_select: Drop-down menu for assigning a region. - * - $data->weight_select: Drop-down menu for setting weights. - * - $data->configure_link: Block configuration link. - * - $data->delete_link: For deleting user added blocks. - * - * @see template_preprocess_block_admin_display_form() - * @see theme_block_admin_display() - */ -?> - $title) { - drupal_add_tabledrag('blocks', 'match', 'sibling', 'block-region-select', 'block-region-' . $region, NULL, FALSE); - drupal_add_tabledrag('blocks', 'order', 'sibling', 'block-weight', 'block-weight-' . $region); - } -?> - - - - - - - - - - - - $title): ?> - - - - - - - $data): ?> - - - - - - - - - - - -
    block_title; ?>region_select; ?>weight_select; ?>configure_link; ?>delete_link; ?>
    - - diff --git a/modules/field/field.module b/modules/field/field.module deleted file mode 100644 index b808e59..0000000 --- a/modules/field/field.module +++ /dev/null @@ -1,1206 +0,0 @@ -$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 - * - ... - * - * 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_flush_caches(). - */ -function field_flush_caches() { - return array('cache_field'); -} - -/** - * 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/handbook/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_theme(). - */ -function field_theme() { - return array( - 'field' => array( - 'render element' => 'element', - ), - 'field_multiple_value_form' => array( - 'render element' => 'element', - ), - ); -} - -/** - * Implements hook_cron(). - * - * Purges some deleted Field API data, if any exists. - */ -function field_cron() { - $limit = variable_get('field_purge_batch_size', 10); - field_purge_batch($limit); -} - -/** - * Implements hook_modules_uninstalled(). - */ -function field_modules_uninstalled($modules) { - module_load_include('inc', 'field', 'field.crud'); - foreach ($modules as $module) { - // TODO D7: field_module_delete is not yet implemented - // field_module_delete($module); - } -} - -/** - * Implements hook_modules_enabled(). - */ -function field_modules_enabled($modules) { - foreach ($modules as $module) { - field_associate_fields($module); - } - field_cache_clear(); -} - -/** - * Implements hook_modules_disabled(). - */ -function field_modules_disabled($modules) { - // Track fields whose field type is being disabled. - db_update('field_config') - ->fields(array('active' => 0)) - ->condition('module', $modules, 'IN') - ->execute(); - - // Track fields whose storage backend is being disabled. - db_update('field_config') - ->fields(array('storage_active' => 0)) - ->condition('storage_module', $modules, 'IN') - ->execute(); - - field_cache_clear(); -} - -/** - * Allows a module to update the database for fields and columns it controls. - * - * @param string $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'); - foreach ($field_types as $name => $field_info) { - watchdog('field', 'Updating field type %type with module %module.', array('%type' => $name, '%module' => $module)); - db_update('field_config') - ->fields(array('module' => $module, 'active' => 1)) - ->condition('type', $name) - ->execute(); - } - // Associate storage backends. - $storage_types = (array) module_invoke($module, 'field_storage_info'); - foreach ($storage_types as $name => $storage_info) { - watchdog('field', 'Updating field storage %type with module %module.', array('%type' => $name, '%module' => $module)); - db_update('field_config') - ->fields(array('storage_module' => $module, 'storage_active' => 1)) - ->condition('storage_type', $name) - ->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'; - function_exists($function); - 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. - * - * For each bundle, settings are provided as a nested array with the following - * structure: - * @code - * array( - * 'view_modes' => array( - * // One sub-array per view mode for the entity type: - * 'full' => array( - * 'custom_display' => Whether the view mode uses custom display - * settings or settings of the 'default' mode, - * ), - * 'teaser' => ... - * ), - * 'extra_fields' => array( - * 'form' => array( - * // One sub-array per pseudo-field in displayed entities: - * 'extra_field_1' => array( - * 'weight' => The weight of the pseudo-field, - * ), - * 'extra_field_2' => ... - * ), - * 'display' => array( - * // One sub-array per pseudo-field in displayed entities: - * 'extra_field_1' => array( - * // One sub-array per view mode for the entity type, including - * // the 'default' mode: - * 'default' => array( - * 'weight' => The weight of the pseudo-field, - * 'visible' => TRUE if the pseudo-field is visible, FALSE if hidden, - * ), - * 'full' => ... - * ), - * 'extra_field_2' => ... - * ), - * ), - * ); - * @endcode - * - * @param $entity_type - * The type of $entity; e.g. 'node' or 'user'. - * @param $bundle - * The bundle name. - * @param $settings - * (optional) The settings to store. - * - * @return - * If no $settings are passed, the current settings are returned. - */ -function field_bundle_settings($entity_type, $bundle, $settings = NULL) { - $stored_settings = variable_get('field_bundle_settings', array()); - - if (isset($settings)) { - $stored_settings[$entity_type][$bundle] = $settings; - - variable_set('field_bundle_settings', $stored_settings); - field_info_cache_clear(); - } - else { - $settings = isset($stored_settings[$entity_type][$bundle]) ? $stored_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 other entities) 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. - $display = _field_info_prepare_instance_display($field, $display); - } - - // 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, - ); - 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. - * @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(); - return (bool) $query - ->fieldCondition($field) - ->range(0, 1) - ->count() - ->execute(); -} - -/** - * Determine whether the user has access to a given field. - * - * @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; 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; -} - -/** - * Helper form element validator: integer. - */ -function _element_validate_integer($element, &$form_state) { - $value = $element['#value']; - if ($value !== '' && (!is_numeric($value) || intval($value) != $value)) { - form_error($element, t('%name must be an integer.', array('%name' => $element['#title']))); - } -} - -/** - * Helper form element validator: integer > 0. - */ -function _element_validate_integer_positive($element, &$form_state) { - $value = $element['#value']; - if ($value !== '' && (!is_numeric($value) || intval($value) != $value || $value <= 0)) { - form_error($element, t('%name must be a positive integer.', array('%name' => $element['#title']))); - } -} - -/** - * Helper form element validator: number. - */ -function _element_validate_number($element, &$form_state) { - $value = $element['#value']; - if ($value != '' && !is_numeric($value)) { - form_error($element, t('%name must be a number.', array('%name' => $element['#title']))); - } -} diff --git a/modules/field/tests/field.test b/modules/field/tests/field.test deleted file mode 100644 index 9281273..0000000 --- a/modules/field/tests/field.test +++ /dev/null @@ -1,3247 +0,0 @@ -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), t('Expected number of values were saved.')); - foreach ($expected_values as $key => $value) { - $this->assertEqual($values[$key][$column], $value, t('Value @value was saved correctly.', array('@value' => $value))); - } - } -} - -class FieldAttachTestCase extends FieldTestCase { - function setUp($modules = array()) { - // 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. - if (!is_array($modules)) { - $modules = func_get_args(); - } - if (!in_array('field_test', $modules)) { - $modules[] = 'field_test'; - } - parent::setUp($modules); - - $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); - $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'], t('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'], t('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', t('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'], t('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'], t('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', t('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], t('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', t('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]], t('Entity %index: expected value was found.', array('%index' => 1))); - $this->assertEqual($entity->{$field_names[1]}[$langcode][0]['additional_key'], 'additional_value', t('Entity %index: extra information was found', array('%index' => 1))); - $this->assert(!isset($entity->{$field_names[2]}), t('Entity %index: field %field_name is not loaded.', array('%index' => 2, '%field_name' => $field_names[2]))); - $this->assert(!isset($entity->{$field_names[3]}), t('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], t('%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']), t('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]), t('Moon is available in the instance array.')); - $this->assertTrue(array_key_exists('mars', $details[FIELD_LOAD_REVISION]), t('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, t('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, t('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}), t('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}), t('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, t('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, t('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}), t('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, t('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}), t('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]), t('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, t('Insert: missing field results in default value saved')); - } - - /** - * 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(), t('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 { - 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() { - $entity_type = 'test_entity'; - $entity_init = field_test_create_stub_entity(); - $langcode = LANGUAGE_NONE; - - // Populate values to be displayed. - $values = $this->_generateTestFieldValues($this->field['cardinality']); - $entity_init->{$this->field_name}[$langcode] = $values; - - // 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); - 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'], "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."); - } - - // 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($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, t('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'), t('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'), t('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'), t('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'), t('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'), t('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'), t('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, t('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'), t('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, t('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'), t('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, t('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'), t('Cached: no cache entry after delete')); - } - - /** - * Test field_attach_validate(). - * - * Verify that field_attach_validate() invokes the correct - * hook_field_validate. - */ - function testFieldAttachValidate() { - $entity_type = 'test_entity'; - $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); - $langcode = LANGUAGE_NONE; - - // Set up values 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; - - 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 value $delta"); - $this->assertEqual(count($errors[$this->field_name][$langcode][$delta]), 1, "Only one error set on value $delta"); - unset($errors[$this->field_name][$langcode][$delta]); - } - else { - $this->assertFalse(isset($errors[$this->field_name][$langcode][$delta]), "No error set on value $delta"); - } - } - $this->assertEqual(count($errors[$this->field_name][$langcode]), 0, 'No extraneous errors set'); - - // Check that cardinality is validated. - $entity->{$this->field_name}[$langcode] = $this->_generateTestFieldValues($this->field['cardinality'] + 1); - try { - field_attach_validate($entity_type, $entity); - } - catch (FieldValidationException $e) { - $errors = $e->errors; - } - $this->assertEqual($errors[$this->field_name][$langcode][0][0]['error'], 'field_cardinality', t('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() { - $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); - - $langcode = LANGUAGE_NONE; - $this->assertEqual($form[$this->field_name][$langcode]['#title'], $this->instance['label'], "Form title is {$this->instance['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', "Form delta $delta widget is textfield"); - } - } - - /** - * Test field_attach_submit(). - */ - function testFieldAttachSubmit() { - $entity_type = 'test_entity'; - $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); - - // Build the form. - $form = array(); - $form_state = form_state_defaults(); - field_attach_form($entity_type, $entity, $form, $form_state); - - // Simulate incoming values. - $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; - - $langcode = LANGUAGE_NONE; - // 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; - field_attach_submit($entity_type, $entity, $form, $form_state); - - asort($weights); - $expected_values = 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'); - } -} - -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, t("Field type $t_key key $key is $val")); - } - $this->assertEqual($info[$t_key]['module'], 'field_test', t("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, t("Formatter type $f_key key $key is $val")); - } - $this->assertEqual($info[$f_key]['module'], 'field_test', t("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, t("Widget type $w_key key $key is $val")); - } - $this->assertEqual($info[$w_key]['module'], 'field_test', t("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, t("Storage type $s_key key $key is $val")); - } - $this->assertEqual($info[$s_key]['module'], 'field_test', t("Storage type field_test module appears")); - } - - // Verify that no unexpected instances exist. - $core_fields = field_info_fields(); - $instances = field_info_instances('test_entity', 'test_bundle'); - $this->assertTrue(empty($instances), t('With no instances, info bundles is empty.')); - - // Create a field, verify it shows up. - $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, t('One new field exists')); - $this->assertEqual($fields[$field['field_name']]['field_name'], $field['field_name'], t('info fields contains field name')); - $this->assertEqual($fields[$field['field_name']]['type'], $field['type'], t('info fields contains field type')); - $this->assertEqual($fields[$field['field_name']]['module'], 'field_test', t('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, t("Field setting $key has correct default value $val")); - } - $this->assertEqual($fields[$field['field_name']]['cardinality'], 1, t('info fields contains cardinality 1')); - $this->assertEqual($fields[$field['field_name']]['active'], 1, t('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); - - $instances = field_info_instances('test_entity', $instance['bundle']); - $this->assertEqual(count($instances), 1, t('One instance shows up in info when attached to a bundle.')); - $this->assertTrue($instance < $instances[$instance['field_name']], t('Instance appears in info correctly')); - } - - /** - * 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'], t('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'] , t('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'], t('Unavailable widget replaced with default widget.')); - $widget_type = field_info_widget_types($instance['widget']['type']); - $this->assertIdentical($instance['widget']['settings'], $widget_type['settings'] , t('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'], t("Formatter is set for the 'default' view mode")); - $formatter_type = field_info_formatter_types($display['type']); - $this->assertIdentical($display['settings'], $formatter_type['settings'] , t("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'), t('No instances are returned on disabled entity types.')); - } - - /** - * 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'], "field_info_field_settings returns {$type}'s field settings"); - $this->assertIdentical(field_info_instance_settings($type), $data['instance_settings'], "field_info_field_settings returns {$type}'s field instance settings"); - } - - $info = field_test_field_widget_info(); - foreach ($info as $type => $data) { - $this->assertIdentical(field_info_widget_settings($type), $data['settings'], "field_info_widget_settings returns {$type}'s widget settings"); - } - - $info = field_test_field_formatter_info(); - foreach ($info as $type => $data) { - $this->assertIdentical(field_info_formatter_settings($type), $data['settings'], "field_info_formatter_settings returns {$type}'s formatter settings"); - } - } -} - -class FieldFormTestCase extends FieldTestCase { - 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 - } - - 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]", '', t('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', t('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', t('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; - - // Display creation form. - $this->drupalGet('test-entity/add/test-bundle'); - $this->assertNoFieldByName("{$field_name_no_access}[$langcode][0][value]", '', t('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, t('Default value was saved for the field with no edit access.')); - $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 1, t('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, t('New revision has the expected value for the field with no edit access.')); - $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 2, t('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, t('New revision has the expected value for the field with no edit access.')); - $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 2, t('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, t('Entity 1: field_single value appears correctly is the form.')); - $this->assertFieldByName('field_unlimited[und][0][value]', 1, t('Entity 1: field_unlimited value 0 appears correctly is the form.')); - $this->assertFieldByName('entity_2[field_single][und][0][value]', 10, t('Entity 2: field_single value appears correctly is the form.')); - $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 11, t('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')), t('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, t('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')), t('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, t('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, t('Entity 1: field_unlimited value 0 appears correctly is the form.')); - $this->assertFieldByName('field_unlimited[und][1][value]', 2, t('Entity 1: field_unlimited value 1 appears correctly is the form.')); - $this->assertFieldByName('field_unlimited[und][2][value]', '', t('Entity 1: field_unlimited value 2 appears correctly is the form.')); - $this->assertFieldByName('field_unlimited[und][3][value]', '', t('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, t('Entity 2: field_unlimited value 0 appears correctly is the form.')); - $this->assertFieldByName('entity_2[field_unlimited][und][1][value]', 14, t('Entity 2: field_unlimited value 1 appears correctly is the form.')); - $this->assertFieldByName('entity_2[field_unlimited][und][2][value]', 15, t('Entity 2: field_unlimited value 2 appears correctly is the form.')); - $this->assertFieldByName('entity_2[field_unlimited][und][3][value]', '', t('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 { - 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, t('Label was displayed.')); - foreach ($this->values as $delta => $value) { - $this->assertText($setting . '|' . $value['value'], t('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); - $this->drupalSetContent(drupal_render($output)); - $setting = $display['settings']['test_formatter_setting_multiple']; - $this->assertNoText($this->label, t('Label was not displayed.')); - $this->assertText('field_test_field_attach_view_alter', t('Alter fired, display passed.')); - $array = array(); - foreach ($this->values as $delta => $value) { - $array[] = $delta . ':' . $value['value']; - } - $this->assertText($setting . '|' . implode('|', $array), t('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, t('Label was not displayed.')); - $this->assertNoText('field_test_field_attach_view_alter', t('Alter not fired.')); - foreach ($this->values as $delta => $value) { - $this->assertText($setting . '|' . $value['value'] . '|' . ($value['value'] + 1), t('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, t('Label was displayed.')); - foreach ($this->values as $delta => $value) { - $this->assertText($setting . '|' . $value['value'], t('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, t('Label was displayed.')); - foreach ($this->values as $delta => $value) { - $this->assertText($setting . '|' . $value['value'], t('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'], t('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'], t('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), t('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'], t('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'], t('Value @delta was displayed with expected setting.', array('@delta' => $delta))); - } - } -} - -class FieldCrudTestCase extends FieldTestCase { - 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'], t('The field name is properly saved.')); - $this->assertEqual($record['type'], $field_definition['type'], t('The field type is properly saved.')); - - // Ensure that cardinality defaults to 1. - $this->assertEqual($record['cardinality'], 1, t('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'], t('Default field settings have been written.')); - - // Ensure that default storage was set. - $this->assertEqual($record['storage_type'], variable_get('field_storage_default'), t('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, t('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, t('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, t('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, t('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']), t('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']), t('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']), t('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), t('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), t('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']), t('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']), t('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']), t('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']), t('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, t('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, t('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), t('%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, t('The field was was marked active.')); - } -} - -class FieldInstanceCrudTestCase extends FieldTestCase { - protected $field; - - 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, t('Required defaults to false.')); - $this->assertIdentical($record['data']['label'], $this->instance_definition['field_name'], t('Label defaults to field name.')); - $this->assertIdentical($record['data']['description'], '', t('Description defaults to empty string.')); - $this->assertIdentical($record['data']['widget']['type'], $field_type['default_widget'], t('Default widget has been written.')); - $this->assertTrue(isset($record['data']['display']['default']), t('Display for "full" view_mode has been written.')); - $this->assertIdentical($record['data']['display']['default']['type'], $field_type['default_formatter'], t('Default formatter for "full" view_mode has been written.')); - - // Check that default settings are set. - $this->assertIdentical($record['data']['settings'], $field_type['instance_settings'] , t('Default instance settings have been written.')); - $this->assertIdentical($record['data']['widget']['settings'], $widget_type['settings'] , t('Default widget settings have been written.')); - $this->assertIdentical($record['data']['display']['default']['settings'], $formatter_type['settings'], t('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, t('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'], t('"required" change is saved')); - $this->assertEqual($instance['label'], $instance_new['label'], t('"label" change is saved')); - $this->assertEqual($instance['description'], $instance_new['description'], t('"description" change is saved')); - $this->assertEqual($instance['widget']['settings']['test_widget_setting'], $instance_new['widget']['settings']['test_widget_setting'], t('Widget setting change is saved')); - $this->assertEqual($instance['widget']['weight'], $instance_new['widget']['weight'], t('Widget weight change is saved')); - $this->assertEqual($instance['display']['default']['settings']['test_formatter_setting'], $instance_new['display']['default']['settings']['test_formatter_setting'], t('Formatter setting change is saved')); - $this->assertEqual($instance['display']['default']['weight'], $instance_new['display']['default']['weight'], t('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'] , t('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) , t('Widget type change updates default settings.')); - $this->assertEqual($instance['display']['default']['type'], $instance_new['display']['default']['type'] , t('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) , t('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']), t('Display for the new view_mode has been written.')); - $this->assertIdentical($instance_new['display']['teaser']['type'], $field_type['default_formatter'], t('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'] , t('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']), t('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']), t('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), t('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']), t('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']), t('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 { - 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), t('%language is an enabled language.', array('%language' => $langcode))); - } - } - $this->assertTrue(in_array('xx', $available_languages), t('%language was made available.', array('%language' => 'xx'))); - $this->assertFalse(in_array('en', $available_languages), t('%language was made unavailable.', array('%language' => 'en'))); - - // Test field_available_languages() behavior for untranslatable fields. - $this->field['translatable'] = FALSE; - $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = drupal_strtolower($this->randomName() . '_field_name'); - $available_languages = field_available_languages($this->entity_type, $this->field); - $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === LANGUAGE_NONE, t('For untranslatable fields only LANGUAGE_NONE is available.')); - } - - /** - * Test the multilanguage logic of _field_invoke(). - */ - function testFieldInvoke() { - $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, t('The result for %language is correctly stored.', array('%language' => $langcode))); - } - $this->assertEqual(count($results), count($available_languages), t('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 = mt_rand(2, 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() were correctly - // forwarded to the callback function. - $this->assertEqual($hash, $result, t('The result for entity %id/%language is correctly stored.', array('%id' => $id, '%language' => $langcode))); - } - } - $this->assertEqual(count($results), count($available_languages), t('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]), t('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']), t('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, t('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, t('%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, t('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, t('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, t('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, t('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, t('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; - - 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 $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[] = $stub; - } - return $stubs; - } - - function setUp() { - parent::setUp('field_test'); - - // Clean up data from previous test cases. - $this->fields = array(); - $this->instances = 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; - 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() { - 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 -= 2) { - // 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'); - } - - // hook_field_delete() was called on a pseudo-entity for each entity. Each - // pseudo entity has a $field property that matches the original entity, - // but no others. - $mem = field_test_memorize(); - $this->assertEqual(count($mem['field_test_field_delete']), 10, 'hook_field_delete was called for the right number of entities'); - $stubs = $this->_generateStubEntities($this->entity_type, $this->entities, $field['field_name']); - $count = count($stubs); - foreach ($mem['field_test_field_delete'] as $args) { - $entity = $args[1]; - $this->assertEqual($stubs[$entity->ftid], $entity, 'hook_field_delete() called with the correct stub'); - unset($stubs[$entity->ftid]); - } - $this->assertEqual(count($stubs), $count-10, 'hook_field_delete was called with each entity once'); - - // 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() { - $field = reset($this->fields); - - // Delete the first instance. - $instance = field_info_instance($this->entity_type, $field['field_name'], 'bb_1'); - field_delete_instance($instance); - - // Purge the data. - field_purge_batch(10); - - // 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. - $instance = field_info_instance($this->entity_type, $field['field_name'], 'bb_2'); - field_delete_instance($instance); - - // Purge the data. - field_purge_batch(10); - - // 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; - } - } - } -} diff --git a/modules/field/tests/field_test.module b/modules/field/tests/field_test.module deleted file mode 100644 index 7e34906..0000000 --- a/modules/field/tests/field_test.module +++ /dev/null @@ -1,249 +0,0 @@ - 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) { - if (isset($items[$id])) { - $result[$id] = array($langcode => hash('sha256', serialize(array($entity_type, $entity, $field['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); -} - -/** - * Memorize calls to hook_field_insert(). - */ -function field_test_field_insert($entity_type, $entity, $field, $instance, $items) { - $args = func_get_args(); - field_test_memorize(__FUNCTION__, $args); -} - -/** - * Memorize calls to hook_field_update(). - */ -function field_test_field_update($entity_type, $entity, $field, $instance, $items) { - $args = func_get_args(); - field_test_memorize(__FUNCTION__, $args); -} - -/** - * Memorize calls to 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_entity_query_alter(). - */ -function field_test_entity_query_alter(&$query) { - if (!empty($query->alterMyExecuteCallbackPlease)) { - $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)), - ), - ); -} - -/** - * Entity label callback. - * - * @param $entity_type - * The entity type. - * @param $entity - * The entity object. - * - * @return - * The label of the entity prefixed with "label callback". - */ -function field_test_entity_label_callback($entity_type, $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'); - } -} diff --git a/modules/field_ui/field_ui.admin.inc b/modules/field_ui/field_ui.admin.inc deleted file mode 100644 index 96beb13..0000000 --- a/modules/field_ui/field_ui.admin.inc +++ /dev/null @@ -1,2020 +0,0 @@ - $type_bundles) { - foreach ($type_bundles as $bundle => $bundle_instances) { - foreach ($bundle_instances as $field_name => $instance) { - $field = field_info_field($field_name); - $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle); - $rows[$field_name]['data'][0] = $field['locked'] ? t('@field_name (Locked)', array('@field_name' => $field_name)) : $field_name; - $rows[$field_name]['data'][1] = $field_types[$field['type']]['label']; - $rows[$field_name]['data'][2][] = $admin_path ? l($bundles[$entity_type][$bundle]['label'], $admin_path . '/fields') : $bundles[$entity_type][$bundle]['label']; - $rows[$field_name]['class'] = $field['locked'] ? array('menu-disabled') : array(''); - } - } - } - 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; -} - -/** - * Helper function to display a message about inactive fields. - */ -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'); - } -} - -/** - * Helper function: determines the rendering order of a tree array. - * - * This is intended as a callback for array_reduce(). - */ -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 #row_callback in field_ui_field_overview_form(), - * and 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 #row_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])) { - $cell = current(element_children($row)); - $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. - $colums_count = 0; - foreach ($table['header'] as $header) { - $colums_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' => $colums_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' => $colums_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']; - } - - foreach (element_children($element) as $cell_key) { - $cell = array('data' => drupal_render($element[$cell_key])); - if (isset($element[$cell_key]['#cell_attributes'])) { - $cell += $element[$cell_key]['#cell_attributes']; - } - $row['data'][] = $cell; - } - $table['rows'][] = $row; - } - } - - return theme('table', $table); -} - -/** - * Menu callback; listing of fields for a bundle. - * - * Allows fields and pseudo-fields to be re-ordered. - */ -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('Name'), - t('Field'), - 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' => '
    ' . t('Add new field') .'
    ', - '#suffix' => '
    ', - ), - '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' => 'textfield', - '#title' => t('New field name'), - '#title_display' => 'invisible', - // This field should stay LTR even for RTL languages. - '#field_prefix' => 'field_', - '#field_suffix' => '‎', - '#attributes' => array('dir'=>'ltr'), - '#size' => 10, - '#description' => t('Field name (a-z, 0-9, _)'), - '#prefix' => '
     
    ', - ), - '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' => '
     
    ', - ), - ); - } - - // Additional row: add existing field. - $existing_field_options = field_ui_existing_field_options($entity_type, $bundle); - if ($existing_field_options && $widget_type_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' => '
    ' . t('Add existing field') .'
    ', - '#suffix' => '
    ', - ), - '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_field_options as $field_name => $fields) { - $field = field_info_field($field_name); - $instance = field_info_instance($form['#entity_type'], $field_name, $form['#bundle']); - $js_fields[$field_name] = array('label' => $instance['label'], 'type' => $field['type'], 'widget' => $instance['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; -} - -/** - * Validate handler for the field overview form. - */ -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); -} - -/** - * Helper function for field_ui_field_overview_form_validate. - * - * Validate the 'add new field' row. - */ -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. - if (substr($field_name, 0, 6) != 'field_') { - $field_name = 'field_' . $field_name; - form_set_value($form['fields']['_add_new_field']['field_name'], $field_name, $form_state); - } - - // Invalid field name. - if (!preg_match('!^field_[a-z0-9_]+$!', $field_name)) { - form_set_error('fields][_add_new_field][field_name', t('Add new field: the field name %field_name is invalid. The name must include only lowercase unaccentuated letters, numbers, and underscores.', array('%field_name' => $field_name))); - } - if (strlen($field_name) > 32) { - form_set_error('fields][_add_new_field][field_name', t("Add new field: the field name %field_name is too long. The name is limited to 32 characters, including the 'field_' prefix.", array('%field_name' => $field_name))); - } - - // Field name already exists. We need to check inactive fields as well, so - // we can't use field_info_fields(). - $fields = field_read_fields(array('field_name' => $field_name), array('include_inactive' => TRUE)); - if ($fields) { - form_set_error('fields][_add_new_field][field_name', t('Add new field: the field name %field_name already exists.', array('%field_name' => $field_name))); - } - } - - // 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.')); - } - } - } -} - -/** - * Helper function for field_ui_field_overview_form_validate. - * - * Validate the 'add existing field' row. - */ -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.')); - } - } - } - } -} - -/** - * Submit handler for the field overview form. - */ -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(); - - // 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' => TRUE, - ); - $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()))); - } - } - - // 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']))); - } - 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()))); - } - } - } - - 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.')); - } -} - -/** - * Menu callback; presents field display settings for a given view mode. - */ -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(''), - ); - $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(''); - $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); - $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 submit handler for multistep buttons on the 'Manage display' screen. - */ -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 ro 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']; -} - -/** - * Submit handler for the 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.')); -} - -/** - * Helper function for field_ui_display_overview_form_submit(). - * - * 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(). - * - * @see 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(). - */ -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']; - } - } -} - -/** - * Return 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; -} - -/** - * Return 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; -} - -/** - * Return 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; -} - -/** - * Return an array of existing field to be added to a bundle. - */ -function field_ui_existing_field_options($entity_type, $bundle) { - $options = 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 that shoud 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'])) { - $text = t('@type: @field (@label)', array( - '@type' => $field_types[$field['type']]['label'], - '@label' => t($instance['label']), '@field' => $instance['field_name'], - )); - $options[$instance['field_name']] = (drupal_strlen($text) > 80 ? truncate_utf8($text, 77) . '...' : $text); - } - } - } - } - } - // Sort the list by field name. - asort($options); - return $options; -} - -/** - * Menu callback; presents the field settings edit page. - */ -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; -} - -/** - * Save a field's settings after editing. - */ -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 (FieldException $e) { - drupal_set_message(t('Attempt to update field %label failed: %message.', array('%label' => $instance['label'], '%message' => $e->getMessage())), 'error'); - // TODO: Where do we go from here? - $form_state['redirect'] = field_ui_next_destination($entity_type, $bundle); - } -} - -/** - * Menu callback; select a widget for the field. - */ -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; -} - -/** - * Submit the change in widget type. - */ -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 (FieldException $e) { - drupal_set_message(t('There was a problem changing the widget for field %label.', array('%label' => $instance['label']))); - } - - $form_state['redirect'] = field_ui_next_destination($entity_type, $bundle); -} - -/** - * Menu callback; present a form for removing a field instance from a bundle. - */ -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; -} - -/** - * 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))); - } - - $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle); - $form_state['redirect'] = field_ui_get_destinations(array($admin_path . '/fields')); -} - -/** - * Menu callback; presents the field instance edit page. - */ -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; -} - -/** - * Build default value fieldset. - */ -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(NULL, NULL, $field, $instance, LANGUAGE_NONE, $items, $element, $form_state, 0); - - return $element; -} - -/** - * Form validation handler for field instance settings form. - */ -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 submit handler for field instance settings form. - */ -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); - field_update_field($field); - - // 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']); -} - -/** - * Helper functions to handle multipage redirects. - */ -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); -} - -/** - * Return 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']); - return field_ui_get_destinations($destinations); - } - $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle); - return $admin_path . '/fields'; -} diff --git a/modules/file/file.module b/modules/file/file.module deleted file mode 100644 index 4002701..0000000 --- a/modules/file/file.module +++ /dev/null @@ -1,984 +0,0 @@ -' . 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/handbook/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', - 'delivery callback' => 'ajax_deliver', - '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 independently 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, - '#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), - ), - '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); - - // If there are no references, stop processing, to avoid returning headers - // for files controlled by other modules. - if (empty($references)) { - 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 = NULL; - if ($entity) { - // Load all fields 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 $field_item) { - if ($field_item['uri'] == $uri) { - $field = $field_item; - break; - } - } - } - - // Check that $entity and $field 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) || !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, $entity_type, $entity))); - } - // Allow other modules to alter the returned grants/denies. - drupal_alter('file_download_access', $grants, $field, $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); - - 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_replace(NULL, theme('status_messages')); - return array('#type' => 'ajax', '#commands' => $commands); - } - - list($form, $form_state) = 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_replace(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'] .= ''; - } - - $output = theme('status_messages') . drupal_render($form); - $js = drupal_add_js(); - $settings = call_user_func_array('array_merge_recursive', $js['settings']['data']); - - $commands = array(); - $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); -} - -/** - * Determine 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, - ); - - $ajax_settings['progress']['type'] ? $ajax_settings['progress']['type'] == 'bar' : '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')), - ); - } - elseif ($implementation == 'apc') { - $element['APC_UPLOAD_PROGRESS'] = array( - '#type' => 'hidden', - '#value' => $upload_progress_key, - '#attributes' => array('class' => array('file-progress')), - ); - } - - // 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', - '#title' => t('Choose a file'), - '#title_display' => 'invisible', - '#size' => 22, - '#theme_wrappers' => array(), - '#weight' => -10, - ); - - if ($fid && $element['#file']) { - $element['filename'] = array( - '#type' => 'markup', - '#markup' => theme('file_link', array('file' => $element['#file'])) . ' ', - '#weight' => -10, - ); - } - - // 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['#id'] . '-upload' => $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; - - // 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); - } - } - // Load file if the FID has changed to confirm it exists. - if (isset($input['fid']) && $file = file_load($input['fid'])) { - $fid = $file->fid; - } - } - } - - // If there is no input, set the default value. - else { - 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['clicked_button']['#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); - } -} - -/** - * Submit handler for upload and remove buttons of managed_file elements. - */ -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; -} - -/** - * Given a managed_file element, save any files that have been uploaded into it. - * - * @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']; - - // 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); - $icon = theme('file_icon', array('file' => $file, 'icon_directory' => $icon_directory)); - - // 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. - * - * @ingroup themeable - */ -function theme_file_icon($variables) { - $file = $variables['file']; - $icon_directory = $variables['icon_directory']; - - $mime = check_plain($file->filemime); - $icon_url = file_icon_url($file, $icon_directory); - return ''; -} - -/** - * Given a file object, create a URL to a matching icon. - * - * @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; -} - -/** - * Given a file object, create a path to a matching icon. - * - * @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; -} - -/** - * Determine 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. - */ - -/** - * Gets 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. - * - * @return - * An integer value. - */ -function file_get_file_references($file, $field = NULL, $age = FIELD_LOAD_REVISION, $field_type = 'file') { - $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); - $references[$field_name] = $query->execute(); - } - } - - return isset($field) ? $references[$field['field_name']] : $references; -} - -/** - * @} End of "defgroup file-module-api". - */ diff --git a/modules/image/image.module b/modules/image/image.module deleted file mode 100644 index 6ef43ad..0000000 --- a/modules/image/image.module +++ /dev/null @@ -1,1182 +0,0 @@ -' . t('About') . ''; - $output .= '

    ' . t('The Image module allows you to manipulate images on your website. It exposes a setting for using the Image toolkit, allows you to configure Image styles that can be used for resizing or adjusting images on display, and provides an Image field for attaching images to content. For more information, see the online handbook entry for Image module.', array('@image' => 'http://drupal.org/handbook/modules/image')) . '

    '; - $output .= '

    ' . t('Uses') . '

    '; - $output .= '
    '; - $output .= '
    ' . t('Manipulating images') . '
    '; - $output .= '
    ' . t('With the Image module you can scale, crop, resize, rotate and desaturate images without affecting the original image using image styles. When you change an image style, the module automatically refreshes all created images. Every image style must have a name, which will be used in the URL of the generated images. There are two common approaches to naming image styles (which you use will depend on how the image style is being applied):',array('@image' => url('admin/config/media/image-styles'))); - $output .= '
    • ' . t('Based on where it will be used: eg. profile-picture') . '
    • '; - $output .= '
    • ' . t('Describing its appearance: eg. square-85x85') . '
    '; - $output .= t('After you create an image style, you can add effects: crop, scale, resize, rotate, and desaturate (other contributed modules provide additional effects). For example, by combining effects as crop, scale, and desaturate, you can create square, grayscale thumbnails.') . '
    '; - $output .= '
    ' . t('Attaching images to content as fields') . '
    '; - $output .= '
    ' . t("Image module also allows you to attach images to content as fields. To add an image field to a content type, go to the content type's manage fields page, and add a new field of type Image. Attaching images to content this way allows image styles to be applied and maintained, and also allows you more flexibility when theming.", array('@content-type' => url('admin/structure/types'))) . '
    '; - $output .= '
    '; - return $output; - case 'admin/config/media/image-styles': - return '

    ' . t('Image styles commonly provide thumbnail sizes by scaling and cropping images, but can also add various effects before an image is displayed. When an image is displayed with a style, a new file is created and the original image is left unchanged.') . '

    '; - case 'admin/config/media/image-styles/edit/%/add/%': - $effect = image_effect_definition_load($arg[7]); - return isset($effect['help']) ? ('

    ' . $effect['help'] . '

    ') : NULL; - case 'admin/config/media/image-styles/edit/%/effects/%': - $effect = ($arg[5] == 'add') ? image_effect_definition_load($arg[6]) : image_effect_load($arg[6], $arg[4]); - return isset($effect['help']) ? ('

    ' . $effect['help'] . '

    ') : NULL; - } -} - -/** - * Implements hook_menu(). - */ -function image_menu() { - $items = array(); - - // Generate image derivatives of publicly available files. - // If clean URLs are disabled, image derivatives will always be served - // through the menu system. - // If clean URLs are enabled and the image derivative already exists, - // PHP will be bypassed. - $directory_path = file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath(); - $items[$directory_path . '/styles/%image_style'] = array( - 'title' => 'Generate image style', - 'page callback' => 'image_style_deliver', - 'page arguments' => array(count(explode('/', $directory_path)) + 1), - 'access callback' => TRUE, - 'type' => MENU_CALLBACK, - ); - // Generate and deliver image derivatives of private files. - // These image derivatives are always delivered through the menu system. - $items['system/files/styles/%image_style'] = array( - 'title' => 'Generate image style', - 'page callback' => 'image_style_deliver', - 'page arguments' => array(3), - 'access callback' => TRUE, - 'type' => MENU_CALLBACK, - ); - $items['admin/config/media/image-styles'] = array( - 'title' => 'Image styles', - 'description' => 'Configure styles that can be used for resizing or adjusting images on display.', - 'page callback' => 'image_style_list', - 'access arguments' => array('administer image styles'), - 'file' => 'image.admin.inc', - ); - $items['admin/config/media/image-styles/list'] = array( - 'title' => 'List', - 'description' => 'List the current image styles on the site.', - 'page callback' => 'image_style_list', - 'access arguments' => array('administer image styles'), - 'type' => MENU_DEFAULT_LOCAL_TASK, - 'weight' => 1, - 'file' => 'image.admin.inc', - ); - $items['admin/config/media/image-styles/add'] = array( - 'title' => 'Add style', - 'description' => 'Add a new image style.', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('image_style_add_form'), - 'access arguments' => array('administer image styles'), - 'type' => MENU_LOCAL_ACTION, - 'weight' => 2, - 'file' => 'image.admin.inc', - ); - $items['admin/config/media/image-styles/edit/%image_style'] = array( - 'title' => 'Edit style', - 'description' => 'Configure an image style.', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('image_style_form', 5), - 'access arguments' => array('administer image styles'), - 'file' => 'image.admin.inc', - ); - $items['admin/config/media/image-styles/delete/%image_style'] = array( - 'title' => 'Delete style', - 'description' => 'Delete an image style.', - 'load arguments' => array(NULL, (string) IMAGE_STORAGE_NORMAL), - 'page callback' => 'drupal_get_form', - 'page arguments' => array('image_style_delete_form', 5), - 'access arguments' => array('administer image styles'), - 'file' => 'image.admin.inc', - ); - $items['admin/config/media/image-styles/revert/%image_style'] = array( - 'title' => 'Revert style', - 'description' => 'Revert an image style.', - 'load arguments' => array(NULL, (string) IMAGE_STORAGE_OVERRIDE), - 'page callback' => 'drupal_get_form', - 'page arguments' => array('image_style_revert_form', 5), - 'access arguments' => array('administer image styles'), - 'file' => 'image.admin.inc', - ); - $items['admin/config/media/image-styles/edit/%image_style/effects/%image_effect'] = array( - 'title' => 'Edit image effect', - 'description' => 'Edit an existing effect within a style.', - 'load arguments' => array(5, (string) IMAGE_STORAGE_EDITABLE), - 'page callback' => 'drupal_get_form', - 'page arguments' => array('image_effect_form', 5, 7), - 'access arguments' => array('administer image styles'), - 'file' => 'image.admin.inc', - ); - $items['admin/config/media/image-styles/edit/%image_style/effects/%image_effect/delete'] = array( - 'title' => 'Delete image effect', - 'description' => 'Delete an existing effect from a style.', - 'load arguments' => array(5, (string) IMAGE_STORAGE_EDITABLE), - 'page callback' => 'drupal_get_form', - 'page arguments' => array('image_effect_delete_form', 5, 7), - 'access arguments' => array('administer image styles'), - 'file' => 'image.admin.inc', - ); - $items['admin/config/media/image-styles/edit/%image_style/add/%image_effect_definition'] = array( - 'title' => 'Add image effect', - 'description' => 'Add a new effect to a style.', - 'load arguments' => array(5), - 'page callback' => 'drupal_get_form', - 'page arguments' => array('image_effect_form', 5, 7), - 'access arguments' => array('administer image styles'), - 'file' => 'image.admin.inc', - ); - - return $items; -} - -/** - * Implements hook_theme(). - */ -function image_theme() { - return array( - // Theme functions in image.module. - 'image_style' => array( - 'variables' => array( - 'style_name' => NULL, - 'path' => NULL, - 'alt' => '', - 'title' => NULL, - 'attributes' => array(), - ), - ), - - // Theme functions in image.admin.inc. - 'image_style_list' => array( - 'variables' => array('styles' => NULL), - ), - 'image_style_effects' => array( - 'render element' => 'form', - ), - 'image_style_preview' => array( - 'variables' => array('style' => NULL), - ), - 'image_anchor' => array( - 'render element' => 'element', - ), - 'image_resize_summary' => array( - 'variables' => array('data' => NULL), - ), - 'image_scale_summary' => array( - 'variables' => array('data' => NULL), - ), - 'image_crop_summary' => array( - 'variables' => array('data' => NULL), - ), - 'image_rotate_summary' => array( - 'variables' => array('data' => NULL), - ), - - // Theme functions in image.field.inc. - 'image_widget' => array( - 'render element' => 'element', - ), - 'image_formatter' => array( - 'variables' => array('item' => NULL, 'path' => NULL, 'image_style' => NULL), - ), - ); -} - -/** - * Implements hook_permission(). - */ -function image_permission() { - return array( - 'administer image styles' => array( - 'title' => t('Administer image styles'), - 'description' => t('Create and modify styles for generating image modifications such as thumbnails.'), - ), - ); -} - -/** - * Implements hook_form_FORM_ID_alter(). - */ -function image_form_system_file_system_settings_alter(&$form, &$form_state) { - $form['#submit'][] = 'image_system_file_system_settings_submit'; -} - -/** - * Submit handler for the file system settings form. - * - * Adds a menu rebuild after the public file path has been changed, so that the - * menu router item depending on that file path will be regenerated. - */ -function image_system_file_system_settings_submit($form, &$form_state) { - if ($form['file_public_path']['#default_value'] !== $form_state['values']['file_public_path']) { - variable_set('menu_rebuild_needed', TRUE); - } -} - -/** - * Implements hook_flush_caches(). - */ -function image_flush_caches() { - return array('cache_image'); -} - -/** - * Implements hook_file_download(). - * - * Control the access to files underneath the styles directory. - */ -function image_file_download($uri) { - $path = file_uri_target($uri); - - // Private file access for image style derivatives. - if (strpos($path, 'styles/') === 0) { - $args = explode('/', $path); - // Discard the first part of the path (styles). - array_shift($args); - // Get the style name from the second part. - $style_name = array_shift($args); - // Remove the scheme from the path. - array_shift($args); - - // Then the remaining parts are the path to the image. - $original_uri = file_uri_scheme($uri) . '://' . implode('/', $args); - - // Check that the file exists and is an image. - if ($info = image_get_info($uri)) { - // Check the permissions of the original to grant access to this image. - $headers = module_invoke_all('file_download', $original_uri); - if (!in_array(-1, $headers)) { - return array( - // Send headers describing the image's size, and MIME-type... - 'Content-Type' => $info['mime_type'], - 'Content-Length' => $info['file_size'], - // ...and allow the file to be cached for two weeks (matching the - // value we/ use for the mod_expires settings in .htaccess) and - // ensure that caching proxies do not share the image with other - // users. - 'Expires' => gmdate(DATE_RFC1123, REQUEST_TIME + 1209600), - 'Cache-Control' => 'max-age=1209600, private, must-revalidate', - ); - } - } - return -1; - } - - // Private file access for the original files. Note that we only - // check access for non-temporary images, since file.module will - // grant access for all temporary files. - $files = file_load_multiple(array(), array('uri' => $uri)); - if (count($files)) { - $file = reset($files); - if ($file->status) { - return file_file_download($uri, 'image'); - } - } -} - -/** - * Implements hook_file_move(). - */ -function image_file_move($file, $source) { - // Delete any image derivatives at the original image path. - image_path_flush($file->uri); -} - -/** - * Implements hook_file_delete(). - */ -function image_file_delete($file) { - // Delete any image derivatives of this image. - image_path_flush($file->uri); -} - -/** - * Implements hook_image_default_styles(). - */ -function image_image_default_styles() { - $styles = array(); - - $styles['thumbnail'] = array( - 'effects' => array( - array( - 'name' => 'image_scale', - 'data' => array('width' => 100, 'height' => 100, 'upscale' => 1), - 'weight' => 0, - ), - ) - ); - - $styles['medium'] = array( - 'effects' => array( - array( - 'name' => 'image_scale', - 'data' => array('width' => 220, 'height' => 220, 'upscale' => 1), - 'weight' => 0, - ), - ) - ); - - $styles['large'] = array( - 'effects' => array( - array( - 'name' => 'image_scale', - 'data' => array('width' => 480, 'height' => 480, 'upscale' => 0), - 'weight' => 0, - ), - ) - ); - - return $styles; -} - -/** - * Implements hook_image_style_save(). - */ -function image_image_style_save($style) { - if (isset($style['old_name']) && $style['old_name'] != $style['name']) { - $instances = field_read_instances(); - // Loop through all fields searching for image fields. - foreach ($instances as $instance) { - if ($instance['widget']['module'] == 'image') { - $instance_changed = FALSE; - foreach ($instance['display'] as $view_mode => $display) { - // Check if the formatter involves an image style. - if ($display['type'] == 'image' && $display['settings']['image_style'] == $style['old_name']) { - // Update display information for any instance using the image - // style that was just deleted. - $instance['display'][$view_mode]['settings']['image_style'] = $style['name']; - $instance_changed = TRUE; - } - } - if ($instance['widget']['settings']['preview_image_style'] == $style['old_name']) { - $instance['widget']['settings']['preview_image_style'] = $style['name']; - $instance_changed = TRUE; - } - if ($instance_changed) { - field_update_instance($instance); - } - } - } - } -} - -/** - * Implements hook_image_style_delete(). - */ -function image_image_style_delete($style) { - image_image_style_save($style); -} - -/** - * Implements hook_field_delete_field(). - */ -function image_field_delete_field($field) { - if ($field['type'] != 'image') { - return; - } - - // The value of a managed_file element can be an array if #extended == TRUE. - $fid = (is_array($field['settings']['default_image']) ? $field['settings']['default_image']['fid'] : $field['settings']['default_image']); - if ($fid && ($file = file_load($fid))) { - file_usage_delete($file, 'image', 'default_image', $field['id']); - } -} - -/** - * Implements hook_field_update_field(). - */ -function image_field_update_field($field, $prior_field, $has_data) { - if ($field['type'] != 'image') { - return; - } - - // The value of a managed_file element can be an array if #extended == TRUE. - $fid_new = (is_array($field['settings']['default_image']) ? $field['settings']['default_image']['fid'] : $field['settings']['default_image']); - $fid_old = (is_array($prior_field['settings']['default_image']) ? $prior_field['settings']['default_image']['fid'] : $prior_field['settings']['default_image']); - - $file_new = $fid_new ? file_load($fid_new) : FALSE; - - if ($fid_new != $fid_old) { - - // Is there a new file? - if ($file_new) { - $file_new->status = FILE_STATUS_PERMANENT; - file_save($file_new); - file_usage_add($file_new, 'image', 'default_image', $field['id']); - } - - // Is there an old file? - if ($fid_old && ($file_old = file_load($fid_old))) { - file_usage_delete($file_old, 'image', 'default_image', $field['id']); - } - } - - // If the upload destination changed, then move the file. - if ($file_new && (file_uri_scheme($file_new->uri) != $field['settings']['uri_scheme'])) { - $directory = $field['settings']['uri_scheme'] . '://default_images/'; - file_prepare_directory($directory, FILE_CREATE_DIRECTORY); - file_move($file_new, $directory . $file_new->filename); - } -} - -/** - * Clear cached versions of a specific file in all styles. - * - * @param $path - * The Drupal file path to the original image. - */ -function image_path_flush($path) { - $styles = image_styles(); - foreach ($styles as $style) { - $image_path = image_style_path($style['name'], $path); - if (file_exists($image_path)) { - file_unmanaged_delete($image_path); - } - } -} - -/** - * Get an array of all styles and their settings. - * - * @return - * An array of styles keyed by the image style ID (isid). - * @see image_style_load() - */ -function image_styles() { - $styles = &drupal_static(__FUNCTION__); - - // Grab from cache or build the array. - if (!isset($styles)) { - if ($cache = cache_get('image_styles', 'cache')) { - $styles = $cache->data; - } - else { - $styles = array(); - - // Select the module-defined styles. - foreach (module_implements('image_default_styles') as $module) { - $module_styles = module_invoke($module, 'image_default_styles'); - foreach ($module_styles as $style_name => $style) { - $style['name'] = $style_name; - $style['module'] = $module; - $style['storage'] = IMAGE_STORAGE_DEFAULT; - foreach ($style['effects'] as $key => $effect) { - $definition = image_effect_definition_load($effect['name']); - $effect = array_merge($definition, $effect); - $style['effects'][$key] = $effect; - } - $styles[$style_name] = $style; - } - } - - // Select all the user-defined styles. - $user_styles = db_select('image_styles', NULL, array('fetch' => PDO::FETCH_ASSOC)) - ->fields('image_styles') - ->orderBy('name') - ->execute() - ->fetchAllAssoc('name', PDO::FETCH_ASSOC); - - // Allow the user styles to override the module styles. - foreach ($user_styles as $style_name => $style) { - $style['module'] = NULL; - $style['storage'] = IMAGE_STORAGE_NORMAL; - $style['effects'] = image_style_effects($style); - if (isset($styles[$style_name]['module'])) { - $style['module'] = $styles[$style_name]['module']; - $style['storage'] = IMAGE_STORAGE_OVERRIDE; - } - $styles[$style_name] = $style; - } - - drupal_alter('image_styles', $styles); - cache_set('image_styles', $styles); - } - } - - return $styles; -} - -/** - * Load a style by style name or ID. May be used as a loader for menu items. - * - * @param $name - * The name of the style. - * @param $isid - * Optional. The numeric id of a style if the name is not known. - * @param $include - * If set, this loader will restrict to a specific type of image style, may be - * one of the defined Image style storage constants. - * @return - * An image style array containing the following keys: - * - "isid": The unique image style ID. - * - "name": The unique image style name. - * - "effects": An array of image effects within this image style. - * If the image style name or ID is not valid, an empty array is returned. - * @see image_effect_load() - */ -function image_style_load($name = NULL, $isid = NULL, $include = NULL) { - $styles = image_styles(); - - // If retrieving by name. - if (isset($name) && isset($styles[$name])) { - $style = $styles[$name]; - } - - // If retrieving by image style id. - if (!isset($name) && isset($isid)) { - foreach ($styles as $name => $database_style) { - if (isset($database_style['isid']) && $database_style['isid'] == $isid) { - $style = $database_style; - break; - } - } - } - - // Restrict to the specific type of flag. This bitwise operation basically - // states "if the storage is X, then allow". - if (isset($style) && (!isset($include) || ($style['storage'] & (int) $include))) { - return $style; - } - - // Otherwise the style was not found. - return FALSE; -} - -/** - * Save an image style. - * - * @param style - * An image style array. - * @return - * An image style array. In the case of a new style, 'isid' will be populated. - */ -function image_style_save($style) { - if (isset($style['isid']) && is_numeric($style['isid'])) { - // Load the existing style to make sure we account for renamed styles. - $old_style = image_style_load(NULL, $style['isid']); - image_style_flush($old_style); - drupal_write_record('image_styles', $style, 'isid'); - if ($old_style['name'] != $style['name']) { - $style['old_name'] = $old_style['name']; - } - } - else { - drupal_write_record('image_styles', $style); - $style['is_new'] = TRUE; - } - - // Let other modules update as necessary on save. - module_invoke_all('image_style_save', $style); - - // Clear all caches and flush. - image_style_flush($style); - - return $style; -} - -/** - * Delete an image style. - * - * @param $style - * An image style array. - * @param $replacement_style_name - * (optional) When deleting a style, specify a replacement style name so - * that existing settings (if any) may be converted to a new style. - * @return - * TRUE on success. - */ -function image_style_delete($style, $replacement_style_name = '') { - image_style_flush($style); - - db_delete('image_effects')->condition('isid', $style['isid'])->execute(); - db_delete('image_styles')->condition('isid', $style['isid'])->execute(); - - // Let other modules update as necessary on save. - $style['old_name'] = $style['name']; - $style['name'] = $replacement_style_name; - module_invoke_all('image_style_delete', $style); - - return TRUE; -} - -/** - * Load all the effects for an image style. - * - * @param $style - * An image style array. - * @return - * An array of image effects associated with specified image style in the - * format array('isid' => array()), or an empty array if the specified style - * has no effects. - */ -function image_style_effects($style) { - $effects = image_effects(); - $style_effects = array(); - foreach ($effects as $effect) { - if ($style['isid'] == $effect['isid']) { - $style_effects[$effect['ieid']] = $effect; - } - } - - return $style_effects; -} - -/** - * Get an array of image styles suitable for using as select list options. - * - * @param $include_empty - * If TRUE a option will be inserted in the options array. - * @return - * Array of image styles both key and value are set to style name. - */ -function image_style_options($include_empty = TRUE) { - $styles = image_styles(); - $options = array(); - if ($include_empty && !empty($styles)) { - $options[''] = t(''); - } - $options = array_merge($options, drupal_map_assoc(array_keys($styles))); - if (empty($options)) { - $options[''] = t('No defined styles'); - } - return $options; -} - -/** - * Menu callback; Given a style and image path, generate a derivative. - * - * After generating an image, transfer it to the requesting agent. - * - * @param $style - * The image style - */ -function image_style_deliver($style, $scheme) { - // Check that the style is defined and the scheme is valid. - if (!$style || !file_stream_wrapper_valid_scheme($scheme)) { - drupal_exit(); - } - - $args = func_get_args(); - array_shift($args); - array_shift($args); - $target = implode('/', $args); - - $image_uri = $scheme . '://' . $target; - $derivative_uri = image_style_path($style['name'], $image_uri); - - // If using the private scheme, let other modules provide headers and - // control access to the file. - if ($scheme == 'private') { - if (file_exists($derivative_uri)) { - file_download($scheme, file_uri_target($derivative_uri)); - } - else { - $headers = module_invoke_all('file_download', $image_uri); - if (in_array(-1, $headers) || empty($headers)) { - return drupal_access_denied(); - } - if (count($headers)) { - foreach ($headers as $name => $value) { - drupal_add_http_header($name, $value); - } - } - } - } - - // Don't start generating the image if the derivative already exists or if - // generation is in progress in another thread. - $lock_name = 'image_style_deliver:' . $style['name'] . ':' . drupal_hash_base64($image_uri); - if (!file_exists($derivative_uri)) { - $lock_acquired = lock_acquire($lock_name); - if (!$lock_acquired) { - // Tell client to retry again in 3 seconds. Currently no browsers are known - // to support Retry-After. - drupal_add_http_header('Status', '503 Service Unavailable'); - drupal_add_http_header('Retry-After', 3); - print t('Image generation in progress. Try again shortly.'); - drupal_exit(); - } - } - - // Try to generate the image, unless another thread just did it while we were - // acquiring the lock. - $success = file_exists($derivative_uri) || image_style_create_derivative($style, $image_uri, $derivative_uri); - - if (!empty($lock_acquired)) { - lock_release($lock_name); - } - - if ($success) { - $image = image_load($derivative_uri); - file_transfer($image->source, array('Content-Type' => $image->info['mime_type'], 'Content-Length' => $image->info['file_size'])); - } - else { - watchdog('image', 'Unable to generate the derived image located at %path.', array('%path' => $derivative_uri)); - drupal_add_http_header('Status', '500 Internal Server Error'); - print t('Error generating image.'); - drupal_exit(); - } -} - -/** - * Creates a new image derivative based on an image style. - * - * Generates an image derivative by creating the destination folder (if it does - * not already exist), applying all image effects defined in $style['effects'], - * and saving a cached version of the resulting image. - * - * @param $style - * An image style array. - * @param $source - * Path of the source file. - * @param $destination - * Path or URI of the destination file. - * - * @return - * TRUE if an image derivative was generated, or FALSE if the image derivative - * could not be generated. - * - * @see image_style_load() - */ -function image_style_create_derivative($style, $source, $destination) { - // Get the folder for the final location of this style. - $directory = drupal_dirname($destination); - - // Build the destination folder tree if it doesn't already exist. - if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) { - watchdog('image', 'Failed to create style directory: %directory', array('%directory' => $directory), LOG_ERR); - return FALSE; - } - - if (!$image = image_load($source)) { - return FALSE; - } - - foreach ($style['effects'] as $effect) { - image_effect_apply($image, $effect); - } - - if (!image_save($image, $destination)) { - if (file_exists($destination)) { - watchdog('image', 'Cached image file %destination already exists. There may be an issue with your rewrite configuration.', array('%destination' => $destination), LOG_ERR); - } - return FALSE; - } - - return TRUE; -} - -/** - * Flush cached media for a style. - * - * @param $style - * An image style array. - */ -function image_style_flush($style) { - $style_directory = drupal_realpath(file_default_scheme() . '://styles/' . $style['name']); - if (is_dir($style_directory)) { - file_unmanaged_delete_recursive($style_directory); - } - - // Let other modules update as necessary on flush. - module_invoke_all('image_style_flush', $style); - - // Clear image style and effect caches. - cache_clear_all('image_styles', 'cache'); - cache_clear_all('image_effects:', 'cache', TRUE); - drupal_static_reset('image_styles'); - drupal_static_reset('image_effects'); - - // Clear field caches so that formatters may be added for this style. - field_info_cache_clear(); - drupal_theme_rebuild(); - - // Clear page caches when flushing. - if (module_exists('block')) { - cache_clear_all('*', 'cache_block', TRUE); - } - cache_clear_all('*', 'cache_page', TRUE); -} - -/** - * Return the URL for an image derivative given a style and image path. - * - * @param $style_name - * The name of the style to be used with this image. - * @param $path - * The path to the image. - * @return - * The absolute URL where a style image can be downloaded, suitable for use - * in an tag. Requesting the URL will cause the image to be created. - * @see image_style_deliver() - */ -function image_style_url($style_name, $path) { - $uri = image_style_path($style_name, $path); - - // If not using clean URLs, the image derivative callback is only available - // with the query string. If the file does not exist, use url() to ensure - // that it is included. Once the file exists it's fine to fall back to the - // actual file path, this avoids bootstrapping PHP once the files are built. - if (!variable_get('clean_url') && file_uri_scheme($uri) == 'public' && !file_exists($uri)) { - $directory_path = file_stream_wrapper_get_instance_by_uri($uri)->getDirectoryPath(); - return url($directory_path . '/' . file_uri_target($uri), array('absolute' => TRUE)); - } - - return file_create_url($uri); -} - -/** - * Return the URI of an image when using a style. - * - * The path returned by this function may not exist. The default generation - * method only creates images when they are requested by a user's browser. - * - * @param $style_name - * The name of the style to be used with this image. - * @param $uri - * The URI or path to the image. - * @return - * The URI to an image style image. - * @see image_style_url() - */ -function image_style_path($style_name, $uri) { - $scheme = file_uri_scheme($uri); - if ($scheme) { - $path = file_uri_target($uri); - } - else { - $path = $uri; - $scheme = file_default_scheme(); - } - return $scheme . '://styles/' . $style_name . '/' . $scheme . '/' . $path; -} - -/** - * Save a default image style to the database. - * - * @param style - * An image style array provided by a module. - * @return - * An image style array. The returned style array will include the new 'isid' - * assigned to the style. - */ -function image_default_style_save($style) { - $style = image_style_save($style); - $effects = array(); - foreach ($style['effects'] as $effect) { - $effect['isid'] = $style['isid']; - $effect = image_effect_save($effect); - $effects[$effect['ieid']] = $effect; - } - $style['effects'] = $effects; - return $style; -} - -/** - * Revert the changes made by users to a default image style. - * - * @param style - * An image style array. - * @return - * Boolean TRUE if the operation succeeded. - */ -function image_default_style_revert($style) { - image_style_flush($style); - - db_delete('image_effects')->condition('isid', $style['isid'])->execute(); - db_delete('image_styles')->condition('isid', $style['isid'])->execute(); - - return TRUE; -} - -/** - * Pull in image effects exposed by modules implementing hook_image_effect_info(). - * - * @return - * An array of image effects to be used when transforming images. - * @see hook_image_effect_info() - * @see image_effect_definition_load() - */ -function image_effect_definitions() { - global $language; - - // hook_image_effect_info() includes translated strings, so each language is - // cached separately. - $langcode = $language->language; - - $effects = &drupal_static(__FUNCTION__); - - if (!isset($effects)) { - if ($cache = cache_get("image_effects:$langcode") && !empty($cache->data)) { - $effects = $cache->data; - } - else { - $effects = array(); - include_once DRUPAL_ROOT . '/modules/image/image.effects.inc'; - foreach (module_implements('image_effect_info') as $module) { - foreach (module_invoke($module, 'image_effect_info') as $name => $effect) { - // Ensure the current toolkit supports the effect. - $effect['module'] = $module; - $effect['name'] = $name; - $effect['data'] = isset($effect['data']) ? $effect['data'] : array(); - $effects[$name] = $effect; - } - } - uasort($effects, '_image_effect_definitions_sort'); - drupal_alter('image_effect_info', $effects); - cache_set("image_effects:$langcode", $effects); - } - } - - return $effects; -} - -/** - * Load the definition for an image effect. - * - * The effect definition is a set of core properties for an image effect, not - * containing any user-settings. The definition defines various functions to - * call when configuring or executing an image effect. This loader is mostly for - * internal use within image.module. Use image_effect_load() or - * image_style_load() to get image effects that contain configuration. - * - * @param $effect - * The name of the effect definition to load. - * @param $style - * An image style array to which this effect will be added. - * @return - * An array containing the image effect definition with the following keys: - * - "effect": The unique name for the effect being performed. Usually prefixed - * with the name of the module providing the effect. - * - "module": The module providing the effect. - * - "help": A description of the effect. - * - "function": The name of the function that will execute the effect. - * - "form": (optional) The name of a function to configure the effect. - * - "summary": (optional) The name of a theme function that will display a - * one-line summary of the effect. Does not include the "theme_" prefix. - */ -function image_effect_definition_load($effect, $style_name = NULL) { - $definitions = image_effect_definitions(); - - // If a style is specified, do not allow loading of default style - // effects. - if (isset($style_name)) { - $style = image_style_load($style_name, NULL); - if ($style['storage'] == IMAGE_STORAGE_DEFAULT) { - return FALSE; - } - } - - return isset($definitions[$effect]) ? $definitions[$effect] : FALSE; -} - -/** - * Load all image effects from the database. - * - * @return - * An array of all image effects. - * @see image_effect_load() - */ -function image_effects() { - $effects = &drupal_static(__FUNCTION__); - - if (!isset($effects)) { - $effects = array(); - - // Add database image effects. - $result = db_select('image_effects', NULL, array('fetch' => PDO::FETCH_ASSOC)) - ->fields('image_effects') - ->orderBy('image_effects.weight', 'ASC') - ->execute(); - foreach ($result as $effect) { - $effect['data'] = unserialize($effect['data']); - $definition = image_effect_definition_load($effect['name']); - // Do not load image effects whose definition cannot be found. - if ($definition) { - $effect = array_merge($definition, $effect); - $effects[$effect['ieid']] = $effect; - } - } - } - - return $effects; -} - -/** - * Load a single image effect. - * - * @param $ieid - * The image effect ID. - * @param $style_name - * The image style name. - * @param $include - * If set, this loader will restrict to a specific type of image style, may be - * one of the defined Image style storage constants. - * @return - * An image effect array, consisting of the following keys: - * - "ieid": The unique image effect ID. - * - "isid": The unique image style ID that contains this image effect. - * - "weight": The weight of this image effect within the image style. - * - "name": The name of the effect definition that powers this image effect. - * - "data": An array of configuration options for this image effect. - * Besides these keys, the entirety of the image definition is merged into - * the image effect array. Returns FALSE if the specified effect cannot be - * found. - * @see image_style_load() - * @see image_effect_definition_load() - */ -function image_effect_load($ieid, $style_name, $include = NULL) { - if (($style = image_style_load($style_name, NULL, $include)) && isset($style['effects'][$ieid])) { - return $style['effects'][$ieid]; - } - return FALSE; -} - -/** - * Save an image effect. - * - * @param $effect - * An image effect array. - * @return - * An image effect array. In the case of a new effect, 'ieid' will be set. - */ -function image_effect_save($effect) { - if (!empty($effect['ieid'])) { - drupal_write_record('image_effects', $effect, 'ieid'); - } - else { - drupal_write_record('image_effects', $effect); - } - $style = image_style_load(NULL, $effect['isid']); - image_style_flush($style); - return $effect; -} - -/** - * Delete an image effect. - * - * @param $effect - * An image effect array. - */ -function image_effect_delete($effect) { - db_delete('image_effects')->condition('ieid', $effect['ieid'])->execute(); - $style = image_style_load(NULL, $effect['isid']); - image_style_flush($style); -} - -/** - * Given an image object and effect, perform the effect on the file. - * - * @param $image - * An image object returned by image_load(). - * @param $effect - * An image effect array. - * @return - * TRUE on success. FALSE if unable to perform the image effect on the image. - */ -function image_effect_apply($image, $effect) { - module_load_include('inc', 'image', 'image.effects'); - $function = $effect['effect callback']; - if (function_exists($function)) { - return $function($image, $effect['data']); - } - return FALSE; -} - -/** - * Returns HTML for an image using a specific image style. - * - * @param $variables - * An associative array containing: - * - style_name: The name of the style to be used to alter the original image. - * - path: The path of the image file relative to the Drupal files directory. - * This function does not work with images outside the files directory nor - * with remotely hosted images. - * - alt: The alternative text for text-based browsers. - * - title: The title text is displayed when the image is hovered in some - * popular browsers. - * - attributes: Associative array of attributes to be placed in the img tag. - * - * @ingroup themeable - */ -function theme_image_style($variables) { - $variables['path'] = image_style_url($variables['style_name'], $variables['path']); - return theme('image', $variables); -} - -/** - * Accept a keyword (center, top, left, etc) and return it as a pixel offset. - * - * @param $value - * @param $current_pixels - * @param $new_pixels - */ -function image_filter_keyword($value, $current_pixels, $new_pixels) { - switch ($value) { - case 'top': - case 'left': - return 0; - - case 'bottom': - case 'right': - return $current_pixels - $new_pixels; - - case 'center': - return $current_pixels / 2 - $new_pixels / 2; - } - return $value; -} - -/** - * Internal function for sorting image effect definitions through uasort(). - * - * @see image_effect_definitions() - */ -function _image_effect_definitions_sort($a, $b) { - return strcasecmp($a['name'], $b['name']); -} diff --git a/modules/locale/locale.admin.inc b/modules/locale/locale.admin.inc deleted file mode 100644 index 36efe01..0000000 --- a/modules/locale/locale.admin.inc +++ /dev/null @@ -1,1440 +0,0 @@ - TRUE); - foreach ($languages as $langcode => $language) { - - $options[$langcode] = ''; - if ($language->enabled) { - $enabled[] = $langcode; - } - $form['weight'][$langcode] = array( - '#type' => 'weight', - '#title' => t('Weight for @title', array('@title' => $language->name)), - '#title_display' => 'invisible', - '#default_value' => $language->weight, - '#attributes' => array('class' => array('language-order-weight')), - ); - $form['name'][$langcode] = array('#markup' => check_plain($language->name)); - $form['native'][$langcode] = array('#markup' => check_plain($language->native)); - $form['direction'][$langcode] = array('#markup' => ($language->direction == LANGUAGE_RTL ? t('Right to left') : t('Left to right'))); - } - $form['enabled'] = array( - '#type' => 'checkboxes', - '#title' => t('Enabled languages'), - '#title_display' => 'invisible', - '#options' => $options, - '#default_value' => $enabled, - ); - $form['site_default'] = array( - '#type' => 'radios', - '#title' => t('Default language'), - '#title_display' => 'invisible', - '#options' => $options, - '#default_value' => language_default('language'), - ); - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration')); - $form['#theme'] = 'locale_languages_overview_form'; - - return $form; -} - -/** - * Returns HTML for the language overview form. - * - * @param $variables - * An associative array containing: - * - form: A render element representing the form. - * - * @ingroup themeable - */ -function theme_locale_languages_overview_form($variables) { - $form = $variables['form']; - $default = language_default(); - foreach ($form['name'] as $key => $element) { - // Do not take form control structures. - if (is_array($element) && element_child($key)) { - // Disable checkbox for the default language, because it cannot be disabled. - if ($key == $default->language) { - $form['enabled'][$key]['#attributes']['disabled'] = 'disabled'; - } - - // Add invisible labels for the checkboxes and radio buttons in the table - // for accessibility. These changes are only required and valid when the - // form is themed as a table, so it would be wrong to perform them in the - // form constructor. - $title = drupal_render($form['name'][$key]); - $form['enabled'][$key]['#title'] = t('Enable !title', array('!title' => $title)); - $form['enabled'][$key]['#title_display'] = 'invisible'; - $form['site_default'][$key]['#title'] = t('Set !title as default', array('!title' => $title)); - $form['site_default'][$key]['#title_display'] = 'invisible'; - $rows[] = array( - 'data' => array( - '' . $title . '', - drupal_render($form['native'][$key]), - check_plain($key), - drupal_render($form['direction'][$key]), - array('data' => drupal_render($form['enabled'][$key]), 'align' => 'center'), - drupal_render($form['site_default'][$key]), - drupal_render($form['weight'][$key]), - l(t('edit'), 'admin/config/regional/language/edit/' . $key) . (($key != 'en' && $key != $default->language) ? ' ' . l(t('delete'), 'admin/config/regional/language/delete/' . $key) : '') - ), - 'class' => array('draggable'), - ); - } - } - $header = array(array('data' => t('English name')), array('data' => t('Native name')), array('data' => t('Code')), array('data' => t('Direction')), array('data' => t('Enabled')), array('data' => t('Default')), array('data' => t('Weight')), array('data' => t('Operations'))); - $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'language-order'))); - $output .= drupal_render_children($form); - - drupal_add_tabledrag('language-order', 'order', 'sibling', 'language-order-weight'); - - return $output; -} - -/** - * Process language overview form submissions, updating existing languages. - */ -function locale_languages_overview_form_submit($form, &$form_state) { - $languages = language_list(); - $default = language_default(); - $url_prefixes = variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX) == LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX; - $enabled_count = 0; - - foreach ($languages as $langcode => $language) { - if ($form_state['values']['site_default'] == $langcode || $default->language == $langcode) { - // Automatically enable the default language and the language - // which was default previously (because we will not get the - // value from that disabled checkbox). - $form_state['values']['enabled'][$langcode] = 1; - } - - // If language URL prefixes are enabled we must clear language domains and - // assign a valid prefix to each non-default language. - if ($url_prefixes) { - $language->domain = ''; - if (empty($language->prefix) && $form_state['values']['site_default'] != $langcode) { - $language->prefix = $langcode; - } - } - - if ($form_state['values']['enabled'][$langcode]) { - $enabled_count++; - $language->enabled = 1; - } - else { - $language->enabled = 0; - } - - $language->weight = $form_state['values']['weight'][$langcode]; - - db_update('languages') - ->fields(array( - 'enabled' => $language->enabled, - 'weight' => $language->weight, - 'prefix' => $language->prefix, - 'domain' => $language->domain, - )) - ->condition('language', $langcode) - ->execute(); - - $languages[$langcode] = $language; - } - - variable_set('language_default', $languages[$form_state['values']['site_default']]); - variable_set('language_count', $enabled_count); - drupal_set_message(t('Configuration saved.')); - - // Changing the language settings impacts the interface. - cache_clear_all('*', 'cache_page', TRUE); - module_invoke_all('multilingual_settings_changed'); - - $form_state['redirect'] = 'admin/config/regional/language'; - return; -} - -/** - * User interface for the language addition screen. - */ -function locale_languages_add_screen() { - $build['predefined'] = drupal_get_form('locale_languages_predefined_form'); - $build['custom'] = drupal_get_form('locale_languages_custom_form'); - return $build; -} - -/** - * Predefined language setup form. - */ -function locale_languages_predefined_form($form) { - $predefined = _locale_prepare_predefined_list(); - $form['language list'] = array('#type' => 'fieldset', - '#title' => t('Predefined language'), - '#collapsible' => TRUE, - ); - $form['language list']['langcode'] = array('#type' => 'select', - '#title' => t('Language name'), - '#default_value' => key($predefined), - '#options' => $predefined, - '#description' => t('Use the Custom language section below if your desired language does not appear in this list.'), - ); - $form['language list']['actions'] = array('#type' => 'actions'); - $form['language list']['actions']['submit'] = array('#type' => 'submit', '#value' => t('Add language')); - return $form; -} - -/** - * Custom language addition form. - */ -function locale_languages_custom_form($form) { - $form['custom language'] = array('#type' => 'fieldset', - '#title' => t('Custom language'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - ); - _locale_languages_common_controls($form['custom language']); - $form['custom language']['actions'] = array('#type' => 'actions'); - $form['custom language']['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Add custom language') - ); - // Reuse the validation and submit functions of the predefined language setup form. - $form['#submit'][] = 'locale_languages_predefined_form_submit'; - $form['#validate'][] = 'locale_languages_predefined_form_validate'; - return $form; -} - -/** - * Editing screen for a particular language. - * - * @param $langcode - * Language code of the language to edit. - */ -function locale_languages_edit_form($form, &$form_state, $langcode) { - if ($language = db_query("SELECT * FROM {languages} WHERE language = :language", array(':language' => $langcode))->fetchObject()) { - _locale_languages_common_controls($form, $language); - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Save language') - ); - $form['#submit'][] = 'locale_languages_edit_form_submit'; - $form['#validate'][] = 'locale_languages_edit_form_validate'; - return $form; - } - else { - drupal_not_found(); - drupal_exit(); - } -} - -/** - * Common elements of the language addition and editing form. - * - * @param $form - * A parent form item (or empty array) to add items below. - * @param $language - * Language object to edit. - */ -function _locale_languages_common_controls(&$form, $language = NULL) { - if (!is_object($language)) { - $language = new stdClass(); - } - if (isset($language->language)) { - $form['langcode_view'] = array( - '#type' => 'item', - '#title' => t('Language code'), - '#markup' => $language->language - ); - $form['langcode'] = array( - '#type' => 'value', - '#value' => $language->language - ); - } - else { - $form['langcode'] = array('#type' => 'textfield', - '#title' => t('Language code'), - '#size' => 12, - '#maxlength' => 60, - '#required' => TRUE, - '#default_value' => @$language->language, - '#disabled' => (isset($language->language)), - '#description' => t('RFC 4646 compliant language identifier. Language codes typically use a country code, and optionally, a script or regional variant name. Examples: "en", "en-US" and "zh-Hant".', array('@rfc4646' => 'http://www.ietf.org/rfc/rfc4646.txt')), - ); - } - $form['name'] = array('#type' => 'textfield', - '#title' => t('Language name in English'), - '#maxlength' => 64, - '#default_value' => @$language->name, - '#required' => TRUE, - '#description' => t('Name of the language in English. Will be available for translation in all languages.'), - ); - $form['native'] = array('#type' => 'textfield', - '#title' => t('Native language name'), - '#maxlength' => 64, - '#default_value' => @$language->native, - '#required' => TRUE, - '#description' => t('Name of the language in the language being added.'), - ); - $form['prefix'] = array('#type' => 'textfield', - '#title' => t('Path prefix language code'), - '#maxlength' => 64, - '#default_value' => @$language->prefix, - '#description' => t('Language code or other custom text to use as a path prefix for URL language detection, if your Detection and selection settings use URL path prefixes. For the default language, this value may be left blank. Modifying this value may break existing URLs. Use with caution in a production environment. Example: Specifying "deutsch" as the path prefix code for German results in URLs like "example.com/deutsch/contact".') - ); - $form['domain'] = array('#type' => 'textfield', - '#title' => t('Language domain'), - '#maxlength' => 128, - '#default_value' => @$language->domain, - '#description' => t('URL including protocol to use for this language, if your Detection and selection settings use URL domains. For the default language, this value may be left blank. Modifying this value may break existing URLs. Use with caution in a production environment. Example: Specifying "http://example.de" or "http://de.example.com" as language domains for German results in URLs like "http://example.de/contact" and "http://de.example.com/contact", respectively.'), - ); - $form['direction'] = array('#type' => 'radios', - '#title' => t('Direction'), - '#required' => TRUE, - '#description' => t('Direction that text in this language is presented.'), - '#default_value' => @$language->direction, - '#options' => array(LANGUAGE_LTR => t('Left to right'), LANGUAGE_RTL => t('Right to left')) - ); - return $form; -} - -/** - * Validate the language addition form. - */ -function locale_languages_predefined_form_validate($form, &$form_state) { - $langcode = $form_state['values']['langcode']; - - if (($duplicate = db_query("SELECT COUNT(*) FROM {languages} WHERE language = :language", array(':language' => $langcode))->fetchField()) != 0) { - form_set_error('langcode', t('The language %language (%code) already exists.', array('%language' => $form_state['values']['name'], '%code' => $langcode))); - } - - if (!isset($form_state['values']['name'])) { - // Predefined language selection. - include_once DRUPAL_ROOT . '/includes/iso.inc'; - $predefined = _locale_get_predefined_list(); - if (!isset($predefined[$langcode])) { - form_set_error('langcode', t('Invalid language code.')); - } - } - else { - // Reuse the editing form validation routine if we add a custom language. - locale_languages_edit_form_validate($form, $form_state); - } -} - -/** - * Process the language addition form submission. - */ -function locale_languages_predefined_form_submit($form, &$form_state) { - $langcode = $form_state['values']['langcode']; - if (isset($form_state['values']['name'])) { - // Custom language form. - locale_add_language($langcode, $form_state['values']['name'], $form_state['values']['native'], $form_state['values']['direction'], $form_state['values']['domain'], $form_state['values']['prefix']); - drupal_set_message(t('The language %language has been created and can now be used. More information is available on the help screen.', array('%language' => t($form_state['values']['name']), '@locale-help' => url('admin/help/locale')))); - } - else { - // Predefined language selection. - include_once DRUPAL_ROOT . '/includes/iso.inc'; - $predefined = _locale_get_predefined_list(); - locale_add_language($langcode); - drupal_set_message(t('The language %language has been created and can now be used. More information is available on the help screen.', array('%language' => t($predefined[$langcode][0]), '@locale-help' => url('admin/help/locale')))); - } - - // See if we have language files to import for the newly added - // language, collect and import them. - if ($batch = locale_batch_by_language($langcode, '_locale_batch_language_finished')) { - batch_set($batch); - } - - $form_state['redirect'] = 'admin/config/regional/language'; -} - -/** - * Validate the language editing form. Reused for custom language addition too. - */ -function locale_languages_edit_form_validate($form, &$form_state) { - // Ensure sane field values for langcode, name, and native. - if (!isset($form['langcode_view']) && preg_match('@[^a-zA-Z_-]@', $form_state['values']['langcode'])) { - form_set_error('langcode', t('%field may only contain characters a-z, underscores, or hyphens.', array('%field' => $form['langcode']['#title']))); - } - if ($form_state['values']['name'] != check_plain($form_state['values']['name'])) { - form_set_error('name', t('%field cannot contain any markup.', array('%field' => $form['name']['#title']))); - } - if ($form_state['values']['native'] != check_plain($form_state['values']['native'])) { - form_set_error('native', t('%field cannot contain any markup.', array('%field' => $form['native']['#title']))); - } - - if (!empty($form_state['values']['domain']) && !empty($form_state['values']['prefix'])) { - form_set_error('prefix', t('Domain and path prefix values should not be set at the same time.')); - } - if (!empty($form_state['values']['domain']) && $duplicate = db_query("SELECT language FROM {languages} WHERE domain = :domain AND language <> :language", array(':domain' => $form_state['values']['domain'], ':language' => $form_state['values']['langcode']))->fetchField()) { - form_set_error('domain', t('The domain (%domain) is already tied to a language (%language).', array('%domain' => $form_state['values']['domain'], '%language' => $duplicate->language))); - } - if (empty($form_state['values']['prefix']) && language_default('language') != $form_state['values']['langcode'] && empty($form_state['values']['domain'])) { - form_set_error('prefix', t('Only the default language can have both the domain and prefix empty.')); - } - if (!empty($form_state['values']['prefix']) && $duplicate = db_query("SELECT language FROM {languages} WHERE prefix = :prefix AND language <> :language", array(':prefix' => $form_state['values']['prefix'], ':language' => $form_state['values']['langcode']))->fetchField()) { - form_set_error('prefix', t('The prefix (%prefix) is already tied to a language (%language).', array('%prefix' => $form_state['values']['prefix'], '%language' => $duplicate->language))); - } -} - -/** - * Process the language editing form submission. - */ -function locale_languages_edit_form_submit($form, &$form_state) { - db_update('languages') - ->fields(array( - 'name' => $form_state['values']['name'], - 'native' => $form_state['values']['native'], - 'domain' => $form_state['values']['domain'], - 'prefix' => $form_state['values']['prefix'], - 'direction' => $form_state['values']['direction'], - )) - ->condition('language', $form_state['values']['langcode']) - ->execute(); - $default = language_default(); - if ($default->language == $form_state['values']['langcode']) { - $properties = array('name', 'native', 'direction', 'enabled', 'plurals', 'formula', 'domain', 'prefix', 'weight'); - foreach ($properties as $keyname) { - if (isset($form_state['values'][$keyname])) { - $default->$keyname = $form_state['values'][$keyname]; - } - } - variable_set('language_default', $default); - } - $form_state['redirect'] = 'admin/config/regional/language'; - return; -} - -/** - * User interface for the language deletion confirmation screen. - */ -function locale_languages_delete_form($form, &$form_state, $langcode) { - - // Do not allow deletion of English locale. - if ($langcode == 'en') { - drupal_set_message(t('The English language cannot be deleted.')); - drupal_goto('admin/config/regional/language'); - } - - if (language_default('language') == $langcode) { - drupal_set_message(t('The default language cannot be deleted.')); - drupal_goto('admin/config/regional/language'); - } - - // For other languages, warn user that data loss is ahead. - $languages = language_list(); - - if (!isset($languages[$langcode])) { - drupal_not_found(); - drupal_exit(); - } - else { - $form['langcode'] = array('#type' => 'value', '#value' => $langcode); - return confirm_form($form, t('Are you sure you want to delete the language %name?', array('%name' => t($languages[$langcode]->name))), 'admin/config/regional/language', t('Deleting a language will remove all interface translations associated with it, and posts in this language will be set to be language neutral. This action cannot be undone.'), t('Delete'), t('Cancel')); - } -} - -/** - * Process language deletion submissions. - */ -function locale_languages_delete_form_submit($form, &$form_state) { - $languages = language_list(); - if (isset($languages[$form_state['values']['langcode']])) { - // Remove translations first. - db_delete('locales_target') - ->condition('language', $form_state['values']['langcode']) - ->execute(); - cache_clear_all('locale:' . $form_state['values']['langcode'], 'cache'); - // With no translations, this removes existing JavaScript translations file. - _locale_rebuild_js($form_state['values']['langcode']); - // Remove the language. - db_delete('languages') - ->condition('language', $form_state['values']['langcode']) - ->execute(); - db_update('node') - ->fields(array('language' => '')) - ->condition('language', $form_state['values']['langcode']) - ->execute(); - if ($languages[$form_state['values']['langcode']]->enabled) { - variable_set('language_count', variable_get('language_count', 1) - 1); - } - module_invoke_all('multilingual_settings_changed'); - $variables = array('%locale' => $languages[$form_state['values']['langcode']]->name); - drupal_set_message(t('The language %locale has been removed.', $variables)); - watchdog('locale', 'The language %locale has been removed.', $variables); - } - - // Changing the language settings impacts the interface: - cache_clear_all('*', 'cache_page', TRUE); - - $form_state['redirect'] = 'admin/config/regional/language'; - return; -} - -/** - * Setting for language negotiation options - */ -function locale_languages_configure_form() { - include_once DRUPAL_ROOT . '/includes/language.inc'; - - $form = array( - '#submit' => array('locale_languages_configure_form_submit'), - '#theme' => 'locale_languages_configure_form', - '#language_types' => language_types_configurable(FALSE), - '#language_types_info' => language_types_info(), - '#language_providers' => language_negotiation_info(), - ); - - foreach ($form['#language_types'] as $type) { - _locale_languages_configure_form_language_table($form, $type); - } - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Save settings'), - ); - - return $form; -} - -/** - * Helper function to build a language provider table. - */ -function _locale_languages_configure_form_language_table(&$form, $type) { - $info = $form['#language_types_info'][$type]; - - $table_form = array( - '#title' => t('@type language detection', array('@type' => $info['name'])), - '#tree' => TRUE, - '#description' => $info['description'], - '#language_providers' => array(), - '#show_operations' => FALSE, - 'weight' => array('#tree' => TRUE), - 'enabled' => array('#tree' => TRUE), - ); - - $language_providers = $form['#language_providers']; - $enabled_providers = variable_get("language_negotiation_$type", array()); - $providers_weight = variable_get("locale_language_providers_weight_$type", array()); - - // Add missing data to the providers lists. - foreach ($language_providers as $id => $provider) { - if (!isset($providers_weight[$id])) { - $providers_weight[$id] = language_provider_weight($provider); - } - } - - // Order providers list by weight. - asort($providers_weight); - - foreach ($providers_weight as $id => $weight) { - // A language provider might be no more available if the defining module has - // been disabled after the last configuration saving. - if (!isset($language_providers[$id])) { - continue; - } - - $enabled = isset($enabled_providers[$id]); - $provider = $language_providers[$id]; - - // List the provider only if the current type is defined in its 'types' key. - // If it is not defined default to all the configurable language types. - $types = array_flip(isset($provider['types']) ? $provider['types'] : $form['#language_types']); - - if (isset($types[$type])) { - $table_form['#language_providers'][$id] = $provider; - $provider_name = check_plain($provider['name']); - - $table_form['weight'][$id] = array( - '#type' => 'weight', - '#title' => t('Weight for !title language detection method', array('!title' => drupal_strtolower($provider_name))), - '#title_display' => 'invisible', - '#default_value' => $weight, - '#attributes' => array('class' => array("language-provider-weight-$type")), - ); - - $table_form['title'][$id] = array('#markup' => $provider_name); - - $table_form['enabled'][$id] = array( - '#type' => 'checkbox', - '#title' => t('Enable !title language detection method', array('!title' => drupal_strtolower($provider_name))), - '#title_display' => 'invisible', - '#default_value' => $enabled, - ); - if ($id === LANGUAGE_NEGOTIATION_DEFAULT) { - $table_form['enabled'][$id]['#default_value'] = TRUE; - $table_form['enabled'][$id]['#attributes'] = array('disabled' => 'disabled'); - } - - $table_form['description'][$id] = array('#markup' => filter_xss_admin($provider['description'])); - - $config_op = array(); - if (isset($provider['config'])) { - $config_op = array('#type' => 'link', '#title' => t('Configure'), '#href' => $provider['config']); - // If there is at least one operation enabled show the operation column. - $table_form['#show_operations'] = TRUE; - } - $table_form['operation'][$id] = $config_op; - } - } - - $form[$type] = $table_form; -} - -/** - * Returns HTML for a language configuration form. - * - * @param $variables - * An associative array containing: - * - form: A render element representing the form. - * - * @ingroup themeable - */ -function theme_locale_languages_configure_form($variables) { - $form = $variables['form']; - $output = ''; - - foreach ($form['#language_types'] as $type) { - $rows = array(); - $info = $form['#language_types_info'][$type]; - $title = ''; - $description = '
    ' . $form[$type]['#description'] . '
    '; - - foreach ($form[$type]['title'] as $id => $element) { - // Do not take form control structures. - if (is_array($element) && element_child($id)) { - $row = array( - 'data' => array( - '' . drupal_render($form[$type]['title'][$id]) . '', - drupal_render($form[$type]['description'][$id]), - drupal_render($form[$type]['enabled'][$id]), - drupal_render($form[$type]['weight'][$id]), - ), - 'class' => array('draggable'), - ); - if ($form[$type]['#show_operations']) { - $row['data'][] = drupal_render($form[$type]['operation'][$id]); - } - $rows[] = $row; - } - } - - $header = array( - array('data' => t('Detection method')), - array('data' => t('Description')), - array('data' => t('Enabled')), - array('data' => t('Weight')), - ); - - // If there is at least one operation enabled show the operation column. - if ($form[$type]['#show_operations']) { - $header[] = array('data' => t('Operations')); - } - - $variables = array( - 'header' => $header, - 'rows' => $rows, - 'attributes' => array('id' => "language-negotiation-providers-$type"), - ); - $table = theme('table', $variables); - $table .= drupal_render_children($form[$type]); - - drupal_add_tabledrag("language-negotiation-providers-$type", 'order', 'sibling', "language-provider-weight-$type"); - - $output .= '
    ' . $title . $description . $table . '
    '; - } - - $output .= drupal_render_children($form); - return $output; -} - -/** - * Submit handler for language negotiation settings. - */ -function locale_languages_configure_form_submit($form, &$form_state) { - $configurable_types = $form['#language_types']; - - foreach ($configurable_types as $type) { - $negotiation = array(); - $enabled_providers = $form_state['values'][$type]['enabled']; - $enabled_providers[LANGUAGE_NEGOTIATION_DEFAULT] = TRUE; - $providers_weight = $form_state['values'][$type]['weight']; - - foreach ($providers_weight as $id => $weight) { - if ($enabled_providers[$id]) { - $provider = $form[$type]['#language_providers'][$id]; - $provider['weight'] = $weight; - $negotiation[$id] = $provider; - } - } - - language_negotiation_set($type, $negotiation); - variable_set("locale_language_providers_weight_$type", $providers_weight); - } - - // Update non-configurable language types and the related language negotiation - // configuration. - language_types_set(); - - $form_state['redirect'] = 'admin/config/regional/language/configure'; - drupal_set_message(t('Language negotiation configuration saved.')); -} - -/** - * The URL language provider configuration form. - */ -function locale_language_providers_url_form($form, &$form_state) { - $form['locale_language_negotiation_url_part'] = array( - '#title' => t('Part of the URL that determines language'), - '#type' => 'radios', - '#options' => array( - LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX => t('Path prefix'), - LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN => t('Domain'), - ), - '#default_value' => variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX), - '#description' => t('Path prefix: URLs like http://example.com/de/contact set language to German (de). Domain: URLs like http://de.example.com/contact set the language to German. Warning: Changing this setting may break incoming URLs. Use with caution on a production site.'), - ); - - $form_state['redirect'] = 'admin/config/regional/language/configure'; - - return system_settings_form($form); -} - -/** - * The URL language provider configuration form. - */ -function locale_language_providers_session_form($form, &$form_state) { - $form['locale_language_negotiation_session_param'] = array( - '#title' => t('Request/session parameter'), - '#type' => 'textfield', - '#default_value' => variable_get('locale_language_negotiation_session_param', 'language'), - '#description' => t('Name of the request/session parameter used to determine the desired language.'), - ); - - $form_state['redirect'] = 'admin/config/regional/language/configure'; - - return system_settings_form($form); -} - -/** - * @} End of "locale-language-administration" - */ - -/** - * @defgroup locale-translate-administration-screens Translation administration screens - * @{ - * Screens for translation administration. - * - * These functions provide various screens as administration interface - * to import, export and view translations. - */ - -/** - * Overview screen for translations. - */ -function locale_translate_overview_screen() { - drupal_static_reset('language_list'); - $languages = language_list('language'); - $groups = module_invoke_all('locale', 'groups'); - - // Build headers with all groups in order. - $headers = array_merge(array(t('Language')), array_values($groups)); - - // Collect summaries of all source strings in all groups. - $sums = db_query("SELECT COUNT(*) AS strings, textgroup FROM {locales_source} GROUP BY textgroup"); - $groupsums = array(); - foreach ($sums as $group) { - $groupsums[$group->textgroup] = $group->strings; - } - - // Set up overview table with default values, ensuring common order for values. - $rows = array(); - foreach ($languages as $langcode => $language) { - $rows[$langcode] = array('name' => ($langcode == 'en' ? t('English (built-in)') : t($language->name))); - foreach ($groups as $group => $name) { - $rows[$langcode][$group] = ($langcode == 'en' ? t('n/a') : '0/' . (isset($groupsums[$group]) ? $groupsums[$group] : 0) . ' (0%)'); - } - } - - // Languages with at least one record in the locale table. - $translations = db_query("SELECT COUNT(*) AS translation, t.language, s.textgroup FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid GROUP BY textgroup, language"); - foreach ($translations as $data) { - $ratio = (!empty($groupsums[$data->textgroup]) && $data->translation > 0) ? round(($data->translation/$groupsums[$data->textgroup]) * 100.0, 2) : 0; - $rows[$data->language][$data->textgroup] = $data->translation . '/' . $groupsums[$data->textgroup] . " ($ratio%)"; - } - - return theme('table', array('header' => $headers, 'rows' => $rows)); -} - -/** - * String search screen. - */ -function locale_translate_seek_screen() { - // Add CSS. - drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css'); - - $elements = drupal_get_form('locale_translation_filter_form'); - $output = drupal_render($elements); - $output .= _locale_translate_seek(); - return $output; -} - -/** - * List locale translation filters that can be applied. - */ -function locale_translation_filters() { - $filters = array(); - - // Get all languages, except English - drupal_static_reset('language_list'); - $languages = locale_language_list('name'); - unset($languages['en']); - - $filters['string'] = array( - 'title' => t('String contains'), - 'description' => t('Leave blank to show all strings. The search is case sensitive.'), - ); - - $filters['language'] = array( - 'title' => t('Language'), - 'options' => array_merge(array('all' => t('All languages'), 'en' => t('English (provided by Drupal)')), $languages), - ); - - $filters['translation'] = array( - 'title' => t('Search in'), - 'options' => array('all' => t('Both translated and untranslated strings'), 'translated' => t('Only translated strings'), 'untranslated' => t('Only untranslated strings')), - ); - - $groups = module_invoke_all('locale', 'groups'); - $filters['group'] = array( - 'title' => t('Limit search to'), - 'options' => array_merge(array('all' => t('All text groups')), $groups), - ); - - return $filters; -} - -/** - * Return form for locale translation filters. - * - * @ingroup forms - */ -function locale_translation_filter_form() { - $filters = locale_translation_filters(); - - $form['filters'] = array( - '#type' => 'fieldset', - '#title' => t('Filter translatable strings'), - '#collapsible' => TRUE, - '#collapsed' => FALSE, - ); - foreach ($filters as $key => $filter) { - // Special case for 'string' filter. - if ($key == 'string') { - $form['filters']['status']['string'] = array( - '#type' => 'textfield', - '#title' => $filter['title'], - '#description' => $filter['description'], - ); - } - else { - $form['filters']['status'][$key] = array( - '#title' => $filter['title'], - '#type' => 'select', - '#empty_value' => 'all', - '#empty_option' => $filter['options']['all'], - '#size' => 0, - '#options' => $filter['options'], - ); - } - if (!empty($_SESSION['locale_translation_filter'][$key])) { - $form['filters']['status'][$key]['#default_value'] = $_SESSION['locale_translation_filter'][$key]; - } - } - - $form['filters']['actions'] = array( - '#type' => 'actions', - '#attributes' => array('class' => array('container-inline')), - ); - $form['filters']['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Filter'), - ); - if (!empty($_SESSION['locale_translation_filter'])) { - $form['filters']['actions']['reset'] = array( - '#type' => 'submit', - '#value' => t('Reset') - ); - } - - return $form; -} - -/** - * Validate result from locale translation filter form. - */ -function locale_translation_filter_form_validate($form, &$form_state) { - if ($form_state['values']['op'] == t('Filter') && empty($form_state['values']['language']) && empty($form_state['values']['group'])) { - form_set_error('type', t('You must select something to filter by.')); - } -} - -/** - * Process result from locale translation filter form. - */ -function locale_translation_filter_form_submit($form, &$form_state) { - $op = $form_state['values']['op']; - $filters = locale_translation_filters(); - switch ($op) { - case t('Filter'): - foreach ($filters as $name => $filter) { - if (isset($form_state['values'][$name])) { - $_SESSION['locale_translation_filter'][$name] = $form_state['values'][$name]; - } - } - break; - case t('Reset'): - $_SESSION['locale_translation_filter'] = array(); - break; - } - - $form_state['redirect'] = 'admin/config/regional/translate/translate'; -} - -/** - * User interface for the translation import screen. - */ -function locale_translate_import_form($form) { - // Get all languages, except English - drupal_static_reset('language_list'); - $names = locale_language_list('name'); - unset($names['en']); - - if (!count($names)) { - $languages = _locale_prepare_predefined_list(); - $default = key($languages); - } - else { - $languages = array( - t('Already added languages') => $names, - t('Languages not yet added') => _locale_prepare_predefined_list() - ); - $default = key($names); - } - - $form['import'] = array('#type' => 'fieldset', - '#title' => t('Import translation'), - ); - $form['import']['file'] = array('#type' => 'file', - '#title' => t('Language file'), - '#size' => 50, - '#description' => t('A Gettext Portable Object (.po) file.'), - ); - $form['import']['langcode'] = array('#type' => 'select', - '#title' => t('Import into'), - '#options' => $languages, - '#default_value' => $default, - '#description' => t('Choose the language you want to add strings into. If you choose a language which is not yet set up, it will be added.'), - ); - $form['import']['group'] = array('#type' => 'radios', - '#title' => t('Text group'), - '#default_value' => 'default', - '#options' => module_invoke_all('locale', 'groups'), - '#description' => t('Imported translations will be added to this text group.'), - ); - $form['import']['mode'] = array('#type' => 'radios', - '#title' => t('Mode'), - '#default_value' => LOCALE_IMPORT_KEEP, - '#options' => array( - LOCALE_IMPORT_OVERWRITE => t('Strings in the uploaded file replace existing ones, new ones are added. The plural format is updated.'), - LOCALE_IMPORT_KEEP => t('Existing strings and the plural format are kept, only new strings are added.') - ), - ); - $form['import']['submit'] = array('#type' => 'submit', '#value' => t('Import')); - - return $form; -} - -/** - * Process the locale import form submission. - */ -function locale_translate_import_form_submit($form, &$form_state) { - $validators = array('file_validate_extensions' => array('po')); - // Ensure we have the file uploaded - if ($file = file_save_upload('file', $validators)) { - - // Add language, if not yet supported - drupal_static_reset('language_list'); - $languages = language_list('language'); - $langcode = $form_state['values']['langcode']; - if (!isset($languages[$langcode])) { - include_once DRUPAL_ROOT . '/includes/iso.inc'; - $predefined = _locale_get_predefined_list(); - locale_add_language($langcode); - drupal_set_message(t('The language %language has been created.', array('%language' => t($predefined[$langcode][0])))); - } - - // Now import strings into the language - if ($return = _locale_import_po($file, $langcode, $form_state['values']['mode'], $form_state['values']['group']) == FALSE) { - $variables = array('%filename' => $file->filename); - drupal_set_message(t('The translation import of %filename failed.', $variables), 'error'); - watchdog('locale', 'The translation import of %filename failed.', $variables, LOG_ERR); - } - } - else { - drupal_set_message(t('File to import not found.'), 'error'); - $form_state['redirect'] = 'admin/config/regional/translate/import'; - return; - } - - $form_state['redirect'] = 'admin/config/regional/translate'; - return; -} - -/** - * User interface for the translation export screen. - */ -function locale_translate_export_screen() { - // Get all languages, except English - drupal_static_reset('language_list'); - $names = locale_language_list('name'); - unset($names['en']); - $output = ''; - // Offer translation export if any language is set up. - if (count($names)) { - $elements = drupal_get_form('locale_translate_export_po_form', $names); - $output = drupal_render($elements); - } - $elements = drupal_get_form('locale_translate_export_pot_form'); - $output .= drupal_render($elements); - return $output; -} - -/** - * Form to export PO files for the languages provided. - * - * @param $names - * An associate array with localized language names - */ -function locale_translate_export_po_form($form, &$form_state, $names) { - $form['export_title'] = array('#type' => 'item', - '#title' => t('Export translation'), - ); - $form['langcode'] = array('#type' => 'select', - '#title' => t('Language name'), - '#options' => $names, - '#description' => t('Select the language to export in Gettext Portable Object (.po) format.'), - ); - $form['group'] = array('#type' => 'radios', - '#title' => t('Text group'), - '#default_value' => 'default', - '#options' => module_invoke_all('locale', 'groups'), - ); - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Export')); - return $form; -} - -/** - * Translation template export form. - */ -function locale_translate_export_pot_form() { - // Complete template export of the strings - $form['export_title'] = array('#type' => 'item', - '#title' => t('Export template'), - '#description' => t('Generate a Gettext Portable Object Template (.pot) file with all strings from the Drupal locale database.'), - ); - $form['group'] = array('#type' => 'radios', - '#title' => t('Text group'), - '#default_value' => 'default', - '#options' => module_invoke_all('locale', 'groups'), - ); - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Export')); - // Reuse PO export submission callback. - $form['#submit'][] = 'locale_translate_export_po_form_submit'; - return $form; -} - -/** - * Process a translation (or template) export form submission. - */ -function locale_translate_export_po_form_submit($form, &$form_state) { - // If template is required, language code is not given. - $language = NULL; - if (isset($form_state['values']['langcode'])) { - $languages = language_list(); - $language = $languages[$form_state['values']['langcode']]; - } - _locale_export_po($language, _locale_export_po_generate($language, _locale_export_get_strings($language, $form_state['values']['group']))); -} -/** - * @} End of "locale-translate-administration-screens" - */ - -/** - * @defgroup locale-translate-edit-delete Translation editing/deletion interface - * @{ - * Edit and delete translation strings. - * - * These functions provide the user interface to edit and delete - * translation strings. - */ - -/** - * User interface for string editing. - */ -function locale_translate_edit_form($form, &$form_state, $lid) { - // Fetch source string, if possible. - $source = db_query('SELECT source, context, textgroup, location FROM {locales_source} WHERE lid = :lid', array(':lid' => $lid))->fetchObject(); - if (!$source) { - drupal_set_message(t('String not found.'), 'error'); - drupal_goto('admin/config/regional/translate/translate'); - } - - // Add original text to the top and some values for form altering. - $form['original'] = array( - '#type' => 'item', - '#title' => t('Original text'), - '#markup' => check_plain(wordwrap($source->source, 0)), - ); - if (!empty($source->context)) { - $form['context'] = array( - '#type' => 'item', - '#title' => t('Context'), - '#markup' => check_plain($source->context), - ); - } - $form['lid'] = array( - '#type' => 'value', - '#value' => $lid - ); - $form['textgroup'] = array( - '#type' => 'value', - '#value' => $source->textgroup, - ); - $form['location'] = array( - '#type' => 'value', - '#value' => $source->location - ); - - // Include default form controls with empty values for all languages. - // This ensures that the languages are always in the same order in forms. - $languages = language_list(); - $default = language_default(); - // We don't need the default language value, that value is in $source. - $omit = $source->textgroup == 'default' ? 'en' : $default->language; - unset($languages[($omit)]); - $form['translations'] = array('#tree' => TRUE); - // Approximate the number of rows to use in the default textarea. - $rows = min(ceil(str_word_count($source->source) / 12), 10); - foreach ($languages as $langcode => $language) { - $form['translations'][$langcode] = array( - '#type' => 'textarea', - '#title' => t($language->name), - '#rows' => $rows, - '#default_value' => '', - ); - } - - // Fetch translations and fill in default values in the form. - $result = db_query("SELECT DISTINCT translation, language FROM {locales_target} WHERE lid = :lid AND language <> :omit", array(':lid' => $lid, ':omit' => $omit)); - foreach ($result as $translation) { - $form['translations'][$translation->language]['#default_value'] = $translation->translation; - } - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save translations')); - return $form; -} - -/** - * Validate string editing form submissions. - */ -function locale_translate_edit_form_validate($form, &$form_state) { - // Locale string check is needed for default textgroup only. - $safe_check_needed = $form_state['values']['textgroup'] == 'default'; - foreach ($form_state['values']['translations'] as $key => $value) { - if ($safe_check_needed && !locale_string_is_safe($value)) { - form_set_error('translations', t('The submitted string contains disallowed HTML: %string', array('%string' => $value))); - watchdog('locale', 'Attempted submission of a translation string with disallowed HTML: %string', array('%string' => $value), LOG_WARNING); - } - } -} - -/** - * Process string editing form submissions. - * - * Saves all translations of one string submitted from a form. - */ -function locale_translate_edit_form_submit($form, &$form_state) { - $lid = $form_state['values']['lid']; - foreach ($form_state['values']['translations'] as $key => $value) { - $translation = db_query("SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $key))->fetchField(); - if (!empty($value)) { - // Only update or insert if we have a value to use. - if (!empty($translation)) { - db_update('locales_target') - ->fields(array( - 'translation' => $value, - )) - ->condition('lid', $lid) - ->condition('language', $key) - ->execute(); - } - else { - db_insert('locales_target') - ->fields(array( - 'lid' => $lid, - 'translation' => $value, - 'language' => $key, - )) - ->execute(); - } - } - elseif (!empty($translation)) { - // Empty translation entered: remove existing entry from database. - db_delete('locales_target') - ->condition('lid', $lid) - ->condition('language', $key) - ->execute(); - } - - // Force JavaScript translation file recreation for this language. - _locale_invalidate_js($key); - } - - drupal_set_message(t('The string has been saved.')); - - // Clear locale cache. - _locale_invalidate_js(); - cache_clear_all('locale:', 'cache', TRUE); - - $form_state['redirect'] = 'admin/config/regional/translate/translate'; - return; -} - -/** - * String deletion confirmation page. - */ -function locale_translate_delete_page($lid) { - if ($source = db_query('SELECT lid, source FROM {locales_source} WHERE lid = :lid', array(':lid' => $lid))->fetchObject()) { - return drupal_get_form('locale_translate_delete_form', $source); - } - else { - return drupal_not_found(); - } -} - -/** - * User interface for the string deletion confirmation screen. - */ -function locale_translate_delete_form($form, &$form_state, $source) { - $form['lid'] = array('#type' => 'value', '#value' => $source->lid); - return confirm_form($form, t('Are you sure you want to delete the string "%source"?', array('%source' => $source->source)), 'admin/config/regional/translate/translate', t('Deleting the string will remove all translations of this string in all languages. This action cannot be undone.'), t('Delete'), t('Cancel')); -} - -/** - * Process string deletion submissions. - */ -function locale_translate_delete_form_submit($form, &$form_state) { - db_delete('locales_source') - ->condition('lid', $form_state['values']['lid']) - ->execute(); - db_delete('locales_target') - ->condition('lid', $form_state['values']['lid']) - ->execute(); - // Force JavaScript translation file recreation for all languages. - _locale_invalidate_js(); - cache_clear_all('locale:', 'cache', TRUE); - drupal_set_message(t('The string has been removed.')); - $form_state['redirect'] = 'admin/config/regional/translate/translate'; -} -/** - * @} End of "locale-translate-edit-delete" - */ - -/** - * Returns HTML for a locale date format form. - * - * @param $variables - * An associative array containing: - * - form: A render element representing the form. - * - * @ingroup themeable - */ -function theme_locale_date_format_form($variables) { - $form = $variables['form']; - $header = array( - t('Date type'), - t('Format'), - ); - - foreach (element_children($form['date_formats']) as $key) { - $row = array(); - $row[] = $form['date_formats'][$key]['#title']; - unset($form['date_formats'][$key]['#title']); - $row[] = array('data' => drupal_render($form['date_formats'][$key])); - $rows[] = $row; - } - - $output = drupal_render($form['language']); - $output .= theme('table', array('header' => $header, 'rows' => $rows)); - $output .= drupal_render_children($form); - - return $output; -} - -/** - * Display edit date format links for each language. - */ -function locale_date_format_language_overview_page() { - $header = array( - t('Language'), - array('data' => t('Operations'), 'colspan' => '2'), - ); - - // Get list of languages. - $languages = locale_language_list('native'); - - foreach ($languages as $langcode => $info) { - $row = array(); - $row[] = $languages[$langcode]; - $row[] = l(t('edit'), 'admin/config/regional/date-time/locale/' . $langcode . '/edit'); - $row[] = l(t('reset'), 'admin/config/regional/date-time/locale/' . $langcode . '/reset'); - $rows[] = $row; - } - - return theme('table', array('header' => $header, 'rows' => $rows)); -} - -/** - * Provide date localization configuration options to users. - */ -function locale_date_format_form($form, &$form_state, $langcode) { - $languages = locale_language_list('native'); - $language_name = $languages[$langcode]; - - // Display the current language name. - $form['language'] = array( - '#type' => 'item', - '#title' => t('Language'), - '#markup' => check_plain($language_name), - '#weight' => -10, - ); - $form['langcode'] = array( - '#type' => 'value', - '#value' => $langcode, - ); - - // Get list of date format types. - $types = system_get_date_types(); - - // Get list of available formats. - $formats = system_get_date_formats(); - $choices = array(); - foreach ($formats as $type => $list) { - foreach ($list as $f => $format) { - $choices[$f] = format_date(REQUEST_TIME, 'custom', $f); - } - } - reset($formats); - - // Get configured formats for each language. - $locale_formats = system_date_format_locale($langcode); - // Display a form field for each format type. - foreach ($types as $type => $type_info) { - if (!empty($locale_formats) && in_array($type, array_keys($locale_formats))) { - $default = $locale_formats[$type]; - } - else { - $default = variable_get('date_format_' . $type, key($formats)); - } - - // Show date format select list. - $form['date_formats']['date_format_' . $type] = array( - '#type' => 'select', - '#title' => check_plain($type_info['title']), - '#attributes' => array('class' => array('date-format')), - '#default_value' => (isset($choices[$default]) ? $default : 'custom'), - '#options' => $choices, - ); - } - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Save configuration'), - ); - - return $form; -} - -/** - * Submit handler for configuring localized date formats on the locale_date_format_form. - */ -function locale_date_format_form_submit($form, &$form_state) { - include_once DRUPAL_ROOT . '/includes/locale.inc'; - $langcode = $form_state['values']['langcode']; - - // Get list of date format types. - $types = system_get_date_types(); - foreach ($types as $type => $type_info) { - $format = $form_state['values']['date_format_' . $type]; - if ($format == 'custom') { - $format = $form_state['values']['date_format_' . $type . '_custom']; - } - locale_date_format_save($langcode, $type, $format); - } - drupal_set_message(t('Configuration saved.')); - $form_state['redirect'] = 'admin/config/regional/date-time/locale'; -} - -/** - * Reset locale specific date formats to the global defaults. - * - * @param $langcode - * Language code, e.g. 'en'. - */ -function locale_date_format_reset_form($form, &$form_state, $langcode) { - $form['langcode'] = array('#type' => 'value', '#value' => $langcode); - $languages = language_list(); - return confirm_form($form, - t('Are you sure you want to reset the date formats for %language to the global defaults?', array('%language' => $languages[$langcode]->name)), - 'admin/config/regional/date-time/locale', - t('Resetting will remove all localized date formats for this language. This action cannot be undone.'), - t('Reset'), t('Cancel')); -} - -/** - * Reset date formats for a specific language to global defaults. - */ -function locale_date_format_reset_form_submit($form, &$form_state) { - db_delete('date_format_locale') - ->condition('language', $form_state['values']['langcode']) - ->execute(); - $form_state['redirect'] = 'admin/config/regional/date-time/locale'; -} diff --git a/modules/locale/locale.module b/modules/locale/locale.module deleted file mode 100644 index 0788461..0000000 --- a/modules/locale/locale.module +++ /dev/null @@ -1,1038 +0,0 @@ -' . t('About') . ''; - $output .= '

    ' . t('The Locale module allows your Drupal site to be presented in languages other than the default English, and to be multilingual. The Locale module works by maintaining a database of translations, and examining text as it is about to be displayed. When a translation of the text is available in the language to be displayed, the translation is displayed rather than the original text. When a translation is unavailable, the original text is displayed, and then stored for review by a translator. For more information, see the online handbook entry for Locale module.', array('@locale' => 'http://drupal.org/handbook/modules/locale/')) . '

    '; - $output .= '

    ' . t('Uses') . '

    '; - $output .= '
    '; - $output .= '
    ' . t('Translating interface text') . '
    '; - $output .= '
    ' . t('Translations of text in the Drupal interface may be provided by:'); - $output .= '
      '; - $output .= '
    • ' . t("Translating within your site, using the Locale module's integrated translation interface.", array('@translate' => url('admin/config/regional/translate'))) . '
    • '; - $output .= '
    • ' . t('Importing files from a set of existing translations, known as a translation package. A translation package enables the display of a specific version of Drupal in a specific language, and contains files in the Gettext Portable Object (.po) format. Although not all languages are available for every version of Drupal, translation packages for many languages are available for download from the Drupal translations page.', array('@translations' => 'http://localize.drupal.org')) . '
    • '; - $output .= '
    • ' . t("If an existing translation package does not meet your needs, the Gettext Portable Object (.po) files within a package may be modified, or new .po files may be created, using a desktop Gettext editor. The Locale module's import feature allows the translated strings from a new or modified .po file to be added to your site. The Locale module's export feature generates files from your site's translated strings, that can either be shared with others or edited offline by a Gettext translation editor.", array('@import' => url('admin/config/regional/translate/import'), '@export' => url('admin/config/regional/translate/export'))) . '
    • '; - $output .= '
    '; - $output .= '
    ' . t('Configuring a multilingual site') . '
    '; - $output .= '
    ' . t("Language negotiation allows your site to automatically change language based on the domain or path used for each request. Users may (optionally) select their preferred language on their My account page, and your site can be configured to honor a web browser's preferred language settings. Site content can be translated using the Content translation module.", array('@content-help' => url('admin/help/translation'))) . '
    '; - $output .= '
    '; - return $output; - case 'admin/config/regional/language': - $output = '

    ' . t('With multiple languages enabled, interface text can be translated, registered users may select their preferred language, and authors can assign a specific language to content. Download contributed translations from Drupal.org.', array('@translations' => 'http://localize.drupal.org')) . '

    '; - return $output; - case 'admin/config/regional/language/add': - return '

    ' . t('Add a language to be supported by your site. If your desired language is not available in the Language name drop-down, click Custom language and provide a language code and other details manually. When providing a language code manually, be sure to enter a standardized language code, since this code may be used by browsers to determine an appropriate display language.') . '

    '; - case 'admin/config/regional/language/configure': - $output = '

    ' . t("Define how to decide which language is used to display page elements (primarily text provided by Drupal and modules, such as field labels and help text). This decision is made by evaluating a series of detection methods for languages; the first detection method that gets a result will determine which language is used for that type of text. Define the order of evaluation of language detection methods on this page.") . '

    '; - return $output; - case 'admin/config/regional/language/configure/session': - $output = '

    ' . t('Determine the language from a request/session parameter. Example: "http://example.com?language=de" sets language to German based on the use of "de" within the "language" parameter.') . '

    '; - return $output; - case 'admin/config/regional/translate': - $output = '

    ' . t('This page provides an overview of available translatable strings. Drupal displays translatable strings in text groups; modules may define additional text groups containing other translatable strings. Because text groups provide a method of grouping related strings, they are often used to focus translation efforts on specific areas of the Drupal interface.') . '

    '; - $output .= '

    ' . t('See the Languages page for more information on adding support for additional languages.', array('@languages' => url('admin/config/regional/language'))) . '

    '; - return $output; - case 'admin/config/regional/translate/import': - $output = '

    ' . t('This page imports the translated strings contained in an individual Gettext Portable Object (.po) file. Normally distributed as part of a translation package (each translation package may contain several .po files), a .po file may need to be imported after offline editing in a Gettext translation editor. Importing an individual .po file may be a lengthy process.') . '

    '; - $output .= '

    ' . t('Note that the .po files within a translation package are imported automatically (if available) when new modules or themes are enabled, or as new languages are added. Since this page only allows the import of one .po file at a time, it may be simpler to download and extract a translation package into your Drupal installation directory and add the language (which automatically imports all .po files within the package). Translation packages are available for download on the Drupal translation page.', array('@language-add' => url('admin/config/regional/language/add'), '@translations' => 'http://localize.drupal.org')) . '

    '; - return $output; - case 'admin/config/regional/translate/export': - return '

    ' . t('This page exports the translated strings used by your site. An export file may be in Gettext Portable Object (.po) form, which includes both the original string and the translation (used to share translations with others), or in Gettext Portable Object Template (.pot) form, which includes the original strings only (used to create new translations with a Gettext translation editor).') . '

    '; - case 'admin/config/regional/translate/translate': - return '

    ' . t('This page allows a translator to search for specific translated and untranslated strings, and is used when creating or editing translations. (Note: For translation tasks involving many strings, it may be more convenient to export strings for offline editing in a desktop Gettext translation editor.) Searches may be limited to strings found within a specific text group or in a specific language.', array('@export' => url('admin/config/regional/translate/export'))) . '

    '; - case 'admin/structure/block/manage/%/%': - if ($arg[4] == 'locale' && $arg[5] == 'language') { - return '

    ' . t('This block is only shown if at least two languages are enabled and language negotiation is set to URL or Session.', array('@languages' => url('admin/config/regional/language'), '@configuration' => url('admin/config/regional/language/configure'))) . '

    '; - } - break; - } -} - -/** - * Implements hook_menu(). - */ -function locale_menu() { - // Manage languages - $items['admin/config/regional/language'] = array( - 'title' => 'Languages', - 'description' => 'Configure languages for content and the user interface.', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('locale_languages_overview_form'), - 'access arguments' => array('administer languages'), - 'file' => 'locale.admin.inc', - 'weight' => -10, - ); - $items['admin/config/regional/language/overview'] = array( - 'title' => 'List', - 'weight' => 0, - 'type' => MENU_DEFAULT_LOCAL_TASK, - ); - $items['admin/config/regional/language/add'] = array( - 'title' => 'Add language', - 'page callback' => 'locale_languages_add_screen', // two forms concatenated - 'access arguments' => array('administer languages'), - 'weight' => 5, - 'type' => MENU_LOCAL_ACTION, - 'file' => 'locale.admin.inc', - ); - $items['admin/config/regional/language/configure'] = array( - 'title' => 'Detection and selection', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('locale_languages_configure_form'), - 'access arguments' => array('administer languages'), - 'weight' => 10, - 'file' => 'locale.admin.inc', - 'type' => MENU_LOCAL_TASK, - ); - $items['admin/config/regional/language/configure/url'] = array( - 'title' => 'URL language detection configuration', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('locale_language_providers_url_form'), - 'access arguments' => array('administer languages'), - 'file' => 'locale.admin.inc', - 'type' => MENU_VISIBLE_IN_BREADCRUMB, - ); - $items['admin/config/regional/language/configure/session'] = array( - 'title' => 'Session language detection configuration', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('locale_language_providers_session_form'), - 'access arguments' => array('administer languages'), - 'file' => 'locale.admin.inc', - 'type' => MENU_VISIBLE_IN_BREADCRUMB, - ); - $items['admin/config/regional/language/edit/%'] = array( - 'title' => 'Edit language', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('locale_languages_edit_form', 5), - 'access arguments' => array('administer languages'), - 'file' => 'locale.admin.inc', - ); - $items['admin/config/regional/language/delete/%'] = array( - 'title' => 'Confirm', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('locale_languages_delete_form', 5), - 'access arguments' => array('administer languages'), - 'file' => 'locale.admin.inc', - ); - - // Translation functionality - $items['admin/config/regional/translate'] = array( - 'title' => 'Translate interface', - 'description' => 'Translate the built in interface and optionally other text.', - 'page callback' => 'locale_translate_overview_screen', - 'access arguments' => array('translate interface'), - 'file' => 'locale.admin.inc', - 'weight' => -5, - ); - $items['admin/config/regional/translate/overview'] = array( - 'title' => 'Overview', - 'weight' => 0, - 'type' => MENU_DEFAULT_LOCAL_TASK, - ); - $items['admin/config/regional/translate/translate'] = array( - 'title' => 'Translate', - 'weight' => 10, - 'type' => MENU_LOCAL_TASK, - 'page callback' => 'locale_translate_seek_screen', // search results and form concatenated - 'access arguments' => array('translate interface'), - 'file' => 'locale.admin.inc', - ); - $items['admin/config/regional/translate/import'] = array( - 'title' => 'Import', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('locale_translate_import_form'), - 'access arguments' => array('translate interface'), - 'weight' => 20, - 'type' => MENU_LOCAL_TASK, - 'file' => 'locale.admin.inc', - ); - $items['admin/config/regional/translate/export'] = array( - 'title' => 'Export', - 'page callback' => 'locale_translate_export_screen', // possibly multiple forms concatenated - 'access arguments' => array('translate interface'), - 'weight' => 30, - 'type' => MENU_LOCAL_TASK, - 'file' => 'locale.admin.inc', - ); - $items['admin/config/regional/translate/edit/%'] = array( - 'title' => 'Edit string', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('locale_translate_edit_form', 5), - 'access arguments' => array('translate interface'), - 'file' => 'locale.admin.inc', - ); - $items['admin/config/regional/translate/delete/%'] = array( - 'title' => 'Delete string', - 'page callback' => 'locale_translate_delete_page', - 'page arguments' => array(5), - 'access arguments' => array('translate interface'), - 'file' => 'locale.admin.inc', - ); - - // Localize date formats. - $items['admin/config/regional/date-time/locale'] = array( - 'title' => 'Localize', - 'description' => 'Configure date formats for each locale', - 'page callback' => 'locale_date_format_language_overview_page', - 'access arguments' => array('administer site configuration'), - 'type' => MENU_LOCAL_TASK, - 'weight' => -8, - 'file' => 'locale.admin.inc', - ); - $items['admin/config/regional/date-time/locale/%/edit'] = array( - 'title' => 'Localize date formats', - 'description' => 'Configure date formats for each locale', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('locale_date_format_form', 5), - 'access arguments' => array('administer site configuration'), - 'file' => 'locale.admin.inc', - ); - $items['admin/config/regional/date-time/locale/%/reset'] = array( - 'title' => 'Reset date formats', - 'description' => 'Reset localized date formats to global defaults', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('locale_date_format_reset_form', 5), - 'access arguments' => array('administer site configuration'), - 'file' => 'locale.admin.inc', - ); - - return $items; -} - -/** - * Implements hook_init(). - * - * Initialize date formats according to the user's current locale. - */ -function locale_init() { - global $conf, $language; - include_once DRUPAL_ROOT . '/includes/locale.inc'; - - // For each date type (e.g. long, short), get the localized date format - // for the user's current language and override the default setting for it - // in $conf. This should happen on all pages except the date and time formats - // settings page, where we want to display the site default and not the - // localized version. - if (strpos($_GET['q'], 'admin/config/regional/date-time/formats') !== 0) { - $languages = array($language->language); - - // Setup appropriate date formats for this locale. - $formats = locale_get_localized_date_format($languages); - foreach ($formats as $format_type => $format) { - $conf[$format_type] = $format; - } - } -} - -/** - * Implements hook_permission(). - */ -function locale_permission() { - return array( - 'administer languages' => array( - 'title' => t('Administer languages'), - ), - 'translate interface' => array( - 'title' => t('Translate interface texts'), - ), - ); -} - -/** - * Implements hook_locale(). - */ -function locale_locale($op = 'groups') { - switch ($op) { - case 'groups': - return array('default' => t('Built-in interface')); - } -} - -/** - * Form builder callback to display language selection widget. - * - * @ingroup forms - * @see locale_form_alter() - */ -function locale_language_selector_form(&$form, &$form_state, $user) { - global $language; - $languages = language_list('enabled'); - $languages = $languages[1]; - - // If the user is being created, we set the user language to the page language. - $user_preferred_language = $user->uid ? user_preferred_language($user) : $language; - - $names = array(); - foreach ($languages as $langcode => $item) { - $name = t($item->name); - $names[$langcode] = $name . ($item->native != $name ? ' (' . $item->native . ')' : ''); - } - $form['locale'] = array( - '#type' => 'fieldset', - '#title' => t('Language settings'), - '#weight' => 1, - '#access' => ($form['#user_category'] == 'account' || ($form['#user_category'] == 'register' && user_access('administer users'))), - ); - - // Get language negotiation settings. - $mode = language_negotiation_get(LANGUAGE_TYPE_INTERFACE) != LANGUAGE_NEGOTIATION_DEFAULT; - $form['locale']['language'] = array( - '#type' => (count($names) <= 5 ? 'radios' : 'select'), - '#title' => t('Language'), - '#default_value' => $user_preferred_language->language, - '#options' => $names, - '#description' => $mode ? t("This account's default language for e-mails, and preferred language for site presentation.") : t("This account's default language for e-mails."), - ); -} - -/** - * Implements hook_form_FORM_ID_alter(). - */ -function locale_form_path_admin_form_alter(&$form, &$form_state) { - $form['language'] = array( - '#type' => 'select', - '#title' => t('Language'), - '#options' => array(LANGUAGE_NONE => t('All languages')) + locale_language_list('name'), - '#default_value' => $form['language']['#value'], - '#weight' => -10, - '#description' => t('A path alias set for a specific language will always be used when displaying this page in that language, and takes precedence over path aliases set for All languages.'), - ); -} - -/** - * Implements hook_form_FORM_ID_alter(). - */ -function locale_form_node_type_form_alter(&$form, &$form_state) { - if (isset($form['type'])) { - $form['workflow']['language_content_type'] = array( - '#type' => 'radios', - '#title' => t('Multilingual support'), - '#default_value' => variable_get('language_content_type_' . $form['#node_type']->type, 0), - '#options' => array(t('Disabled'), t('Enabled')), - '#description' => t('Enable multilingual support for this content type. If enabled, a language selection field will be added to the editing form, allowing you to select from one of the enabled languages. If disabled, new posts are saved with the default language. Existing content will not be affected by changing this option.', array('!languages' => url('admin/config/regional/language'))), - ); - } -} - -/** - * Return whether the given content type has multilingual support. - * - * @return - * True if multilingual support is enabled. - */ -function locale_multilingual_node_type($type_name) { - return (bool) variable_get('language_content_type_' . $type_name, 0); -} - -/** - * Implements hook_form_alter(). - * - * Adds language fields to user forms. - */ -function locale_form_alter(&$form, &$form_state, $form_id) { - // Only alter user forms if there is more than one language. - if (drupal_multilingual()) { - // Display language selector when either creating a user on the admin - // interface or editing a user account. - if ($form_id == 'user_register_form' || ($form_id == 'user_profile_form' && $form['#user_category'] == 'account')) { - locale_language_selector_form($form, $form_state, $form['#user']); - } - } -} - -/** - * Implements hook_form_BASE_FORM_ID_alter(). - */ -function locale_form_node_form_alter(&$form, &$form_state) { - if (isset($form['#node']->type) && locale_multilingual_node_type($form['#node']->type)) { - $form['language'] = array( - '#type' => 'select', - '#title' => t('Language'), - '#default_value' => (isset($form['#node']->language) ? $form['#node']->language : ''), - '#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'), - ); - } - // Node type without language selector: assign the default for new nodes - elseif (!isset($form['#node']->nid)) { - $default = language_default(); - $form['language'] = array( - '#type' => 'value', - '#value' => $default->language - ); - } - $form['#submit'][] = 'locale_field_node_form_submit'; -} - -/** - * Form submit handler for node_form(). - * - * Checks if Locale is registered as a translation handler and handle possible - * node language changes. - * - * This submit handler needs to run before entity_form_submit_build_entity() - * is invoked by node_form_submit_build_node(), because it alters the values of - * attached fields. Therefore, it cannot be a hook_node_submit() implementation. - */ -function locale_field_node_form_submit($form, &$form_state) { - if (field_has_translation_handler('node', 'locale')) { - $node = (object) $form_state['values']; - $available_languages = field_content_languages(); - list(, , $bundle) = entity_extract_ids('node', $node); - - foreach (field_info_instances('node', $bundle) as $instance) { - $field_name = $instance['field_name']; - $field = field_info_field($field_name); - $previous_language = $form[$field_name]['#language']; - - // Handle a possible language change: new language values are inserted, - // previous ones are deleted. - if ($field['translatable'] && $previous_language != $node->language) { - $form_state['values'][$field_name][$node->language] = $node->{$field_name}[$previous_language]; - $form_state['values'][$field_name][$previous_language] = array(); - } - } - } -} - -/** - * Implements hook_theme(). - */ -function locale_theme() { - return array( - 'locale_languages_overview_form' => array( - 'render element' => 'form', - ), - 'locale_languages_configure_form' => array( - 'render element' => 'form', - ), - 'locale_date_format_form' => array( - 'render element' => 'form', - ), - ); -} - -/** - * Implements hook_field_language_alter(). - */ -function locale_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']); - } -} - -/** - * Applies language fallback rules to the fields attached to the given entity. - * - * Core language fallback rules simply check if fields have a field translation - * for the requested language code. If so the requested language is returned, - * otherwise all the fallback candidates are inspected to see if there is a - * field translation available in another language. - * By default this is called by locale_field_language_alter(), but this - * behavior can be disabled by setting the 'locale_field_language_fallback' - * variable to FALSE. - * - * @param $display_language - * A reference to an array of language codes keyed by field name. - * @param $entity - * The entity to be displayed. - * @param $langcode - * The language code $entity has to be displayed in. - */ -function locale_field_language_fallback(&$display_language, $entity, $langcode) { - // Lazily init fallback candidates to avoid unnecessary calls. - $fallback_candidates = NULL; - $field_languages = array(); - - foreach ($display_language as $field_name => $field_language) { - // If the requested language is defined for the current field use it, - // otherwise search for a fallback value among the fallback candidates. - if (isset($entity->{$field_name}[$langcode])) { - $display_language[$field_name] = $langcode; - } - elseif (!empty($entity->{$field_name})) { - if (!isset($fallback_candidates)) { - require_once DRUPAL_ROOT . '/includes/language.inc'; - $fallback_candidates = language_fallback_get_candidates(); - } - foreach ($fallback_candidates as $fallback_language) { - if (isset($entity->{$field_name}[$fallback_language])) { - $display_language[$field_name] = $fallback_language; - break; - } - } - } - } -} - -/** - * Implements hook_entity_info_alter(). - */ -function locale_entity_info_alter(&$entity_info) { - $entity_info['node']['translation']['locale'] = TRUE; -} - -/** - * Implements hook_language_types_info(). - * - * Defines the three core language types: - * - Interface language is the only configurable language type in core. It is - * used by t() as the default language if none is specified. - * - Content language is by default non-configurable and inherits the interface - * language negotiated value. It is used by the Field API to determine the - * display language for fields if no explicit value is specified. - * - URL language is by default non-configurable and is determined through the - * URL language provider or the URL fallback provider if no language can be - * detected. It is used by l() as the default language if none is specified. - */ -function locale_language_types_info() { - require_once DRUPAL_ROOT . '/includes/locale.inc'; - return array( - LANGUAGE_TYPE_INTERFACE => array( - 'name' => t('User interface text'), - 'description' => t('Order of language detection methods for user interface text. If a translation of user interface text is available in the detected language, it will be displayed.'), - ), - LANGUAGE_TYPE_CONTENT => array( - 'name' => t('Content'), - 'description' => t('Order of language detection methods for content. If a version of content is available in the detected language, it will be displayed.'), - 'fixed' => array(LOCALE_LANGUAGE_NEGOTIATION_INTERFACE), - ), - LANGUAGE_TYPE_URL => array( - 'fixed' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_URL_FALLBACK), - ), - ); -} - -/** - * Implements hook_language_negotiation_info(). - */ -function locale_language_negotiation_info() { - $file = 'includes/locale.inc'; - $providers = array(); - - $providers[LOCALE_LANGUAGE_NEGOTIATION_URL] = array( - 'types' => array(LANGUAGE_TYPE_CONTENT, LANGUAGE_TYPE_INTERFACE, LANGUAGE_TYPE_URL), - 'callbacks' => array( - 'language' => 'locale_language_from_url', - 'switcher' => 'locale_language_switcher_url', - 'url_rewrite' => 'locale_language_url_rewrite_url', - ), - 'file' => $file, - 'weight' => -8, - 'name' => t('URL'), - 'description' => t('Determine the language from the URL (Path prefix or domain).'), - 'config' => 'admin/config/regional/language/configure/url', - ); - - $providers[LOCALE_LANGUAGE_NEGOTIATION_SESSION] = array( - 'callbacks' => array( - 'language' => 'locale_language_from_session', - 'switcher' => 'locale_language_switcher_session', - 'url_rewrite' => 'locale_language_url_rewrite_session', - ), - 'file' => $file, - 'weight' => -6, - 'name' => t('Session'), - 'description' => t('Determine the language from a request/session parameter.'), - 'config' => 'admin/config/regional/language/configure/session', - ); - - $providers[LOCALE_LANGUAGE_NEGOTIATION_USER] = array( - 'callbacks' => array('language' => 'locale_language_from_user'), - 'file' => $file, - 'weight' => -4, - 'name' => t('User'), - 'description' => t("Follow the user's language preference."), - ); - - $providers[LOCALE_LANGUAGE_NEGOTIATION_BROWSER] = array( - 'callbacks' => array('language' => 'locale_language_from_browser'), - 'file' => $file, - 'weight' => -2, - 'cache' => 0, - 'name' => t('Browser'), - 'description' => t("Determine the language from the browser's language settings."), - ); - - $providers[LOCALE_LANGUAGE_NEGOTIATION_INTERFACE] = array( - 'types' => array(LANGUAGE_TYPE_CONTENT), - 'callbacks' => array('language' => 'locale_language_from_interface'), - 'file' => $file, - 'weight' => 8, - 'name' => t('Interface'), - 'description' => t('Use the detected interface language.'), - ); - - $providers[LOCALE_LANGUAGE_NEGOTIATION_URL_FALLBACK] = array( - 'types' => array(LANGUAGE_TYPE_URL), - 'callbacks' => array('language' => 'locale_language_url_fallback'), - 'file' => $file, - 'weight' => 8, - 'name' => t('URL fallback'), - 'description' => t('Use an already detected language for URLs if none is found.'), - ); - - return $providers; -} - -/** - * Implements hook_modules_enabled(). - */ -function locale_modules_enabled($modules) { - include_once DRUPAL_ROOT . '/includes/language.inc'; - language_types_set(); - language_negotiation_purge(); -} - -/** - * Implements hook_modules_disabled(). - */ -function locale_modules_disabled($modules) { - locale_modules_enabled($modules); -} - -// --------------------------------------------------------------------------------- -// Locale core functionality - -/** - * Provides interface translation services. - * - * This function is called from t() to translate a string if needed. - * - * @param $string - * A string to look up translation for. If omitted, all the - * cached strings will be returned in all languages already - * used on the page. - * @param $context - * The context of this string. - * @param $langcode - * Language code to use for the lookup. - */ -function locale($string = NULL, $context = NULL, $langcode = NULL) { - global $language; - $locale_t = &drupal_static(__FUNCTION__); - - if (!isset($string)) { - // Return all cached strings if no string was specified - return $locale_t; - } - - $langcode = isset($langcode) ? $langcode : $language->language; - - // Store database cached translations in a static variable. Only build the - // cache after $language has been set to avoid an unnecessary cache rebuild. - if (!isset($locale_t[$langcode]) && isset($language)) { - $locale_t[$langcode] = array(); - // Disabling the usage of string caching allows a module to watch for - // the exact list of strings used on a page. From a performance - // perspective that is a really bad idea, so we have no user - // interface for this. Be careful when turning this option off! - if (variable_get('locale_cache_strings', 1) == 1) { - if ($cache = cache_get('locale:' . $langcode, 'cache')) { - $locale_t[$langcode] = $cache->data; - } - elseif (lock_acquire('locale_cache_' . $langcode)) { - // Refresh database stored cache of translations for given language. - // We only store short strings used in current version, to improve - // performance and consume less memory. - $result = db_query("SELECT s.source, s.context, t.translation, t.language FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.textgroup = 'default' AND s.version = :version AND LENGTH(s.source) < :length", array(':language' => $langcode, ':version' => VERSION, ':length' => variable_get('locale_cache_length', 75))); - foreach ($result as $data) { - $locale_t[$langcode][$data->context][$data->source] = (empty($data->translation) ? TRUE : $data->translation); - } - cache_set('locale:' . $langcode, $locale_t[$langcode]); - lock_release('locale_cache_' . $langcode); - } - } - } - - // If we have the translation cached, skip checking the database - if (!isset($locale_t[$langcode][$context][$string])) { - - // We do not have this translation cached, so get it from the DB. - $translation = db_query("SELECT s.lid, t.translation, s.version FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.source = :source AND s.context = :context AND s.textgroup = 'default'", array( - ':language' => $langcode, - ':source' => $string, - ':context' => (string) $context, - ))->fetchObject(); - if ($translation) { - // We have the source string at least. - // Cache translation string or TRUE if no translation exists. - $locale_t[$langcode][$context][$string] = (empty($translation->translation) ? TRUE : $translation->translation); - - if ($translation->version != VERSION) { - // This is the first use of this string under current Drupal version. Save version - // and clear cache, to include the string into caching next time. Saved version is - // also a string-history information for later pruning of the tables. - db_update('locales_source') - ->fields(array('version' => VERSION)) - ->condition('lid', $translation->lid) - ->execute(); - cache_clear_all('locale:', 'cache', TRUE); - } - } - else { - // We don't have the source string, cache this as untranslated. - db_insert('locales_source') - ->fields(array( - 'location' => request_uri(), - 'source' => $string, - 'context' => (string) $context, - 'textgroup' => 'default', - 'version' => VERSION, - )) - ->execute(); - $locale_t[$langcode][$context][$string] = TRUE; - // Clear locale cache so this string can be added in a later request. - cache_clear_all('locale:', 'cache', TRUE); - } - } - - return ($locale_t[$langcode][$context][$string] === TRUE ? $string : $locale_t[$langcode][$context][$string]); -} - -/** - * Reset static variables used by locale(). - */ -function locale_reset() { - drupal_static_reset('locale'); -} - -/** - * Returns plural form index for a specific number. - * - * The index is computed from the formula of this language. - * - * @param $count - * Number to return plural for. - * @param $langcode - * Optional language code to translate to a language other than - * what is used to display the page. - */ -function locale_get_plural($count, $langcode = NULL) { - global $language; - $locale_formula = &drupal_static(__FUNCTION__, array()); - $plurals = &drupal_static(__FUNCTION__ . ':plurals', array()); - - $langcode = $langcode ? $langcode : $language->language; - - if (!isset($plurals[$langcode][$count])) { - if (empty($locale_formula)) { - $language_list = language_list(); - $locale_formula[$langcode] = $language_list[$langcode]->formula; - } - if ($locale_formula[$langcode]) { - $n = $count; - $plurals[$langcode][$count] = @eval('return intval(' . $locale_formula[$langcode] . ');'); - return $plurals[$langcode][$count]; - } - else { - $plurals[$langcode][$count] = -1; - return -1; - } - } - return $plurals[$langcode][$count]; -} - - -/** - * Returns a language name - */ -function locale_language_name($lang) { - $list = &drupal_static(__FUNCTION__); - if (!isset($list)) { - $list = locale_language_list(); - } - return ($lang && isset($list[$lang])) ? $list[$lang] : t('All'); -} - -/** - * Returns array of language names - * - * @param $field - * 'name' => names in current language, localized - * 'native' => native names - * @param $all - * Boolean to return all languages or only enabled ones - */ -function locale_language_list($field = 'name', $all = FALSE) { - if ($all) { - $languages = language_list(); - } - else { - $languages = language_list('enabled'); - $languages = $languages[1]; - } - $list = array(); - foreach ($languages as $language) { - $list[$language->language] = ($field == 'name') ? t($language->name) : $language->$field; - } - return $list; -} - -/** - * Implements hook_modules_installed(). - */ -function locale_modules_installed($modules) { - locale_system_update($modules); -} - -/** - * Implements hook_themes_enabled(). - * - * @todo This is technically wrong. We must not import upon enabling, but upon - * initial installation. The theme system is missing an installation hook. - */ -function locale_themes_enabled($themes) { - locale_system_update($themes); -} - -/** - * Imports translations when new modules or themes are installed. - * - * This function will either import translation for the component change - * right away, or start a batch if more files need to be imported. - * - * @param $components - * An array of component (theme and/or module) names to import - * translations for. - */ -function locale_system_update($components) { - include_once DRUPAL_ROOT . '/includes/locale.inc'; - if ($batch = locale_batch_by_component($components)) { - batch_set($batch); - } -} - -/** - * Implements hook_js_alter(). - * - * This function checks all JavaScript files currently added via drupal_add_js() - * and invokes parsing if they have not yet been parsed for Drupal.t() - * and Drupal.formatPlural() calls. Also refreshes the JavaScript translation - * file if necessary, and adds it to the page. - */ -function locale_js_alter(&$javascript) { - global $language; - - $dir = 'public://' . variable_get('locale_js_directory', 'languages'); - $parsed = variable_get('javascript_parsed', array()); - $files = $new_files = FALSE; - - // Require because locale_js_alter() could be called without locale_init(). - require_once DRUPAL_ROOT . '/includes/locale.inc'; - - foreach ($javascript as $item) { - if ($item['type'] == 'file') { - $files = TRUE; - $filepath = $item['data']; - if (!in_array($filepath, $parsed)) { - // Don't parse our own translations files. - if (substr($filepath, 0, strlen($dir)) != $dir) { - _locale_parse_js_file($filepath); - $parsed[] = $filepath; - $new_files = TRUE; - } - } - } - } - - // If there are any new source files we parsed, invalidate existing - // JavaScript translation files for all languages, adding the refresh - // flags into the existing array. - if ($new_files) { - $parsed += _locale_invalidate_js(); - } - - // If necessary, rebuild the translation file for the current language. - if (!empty($parsed['refresh:' . $language->language])) { - // Don't clear the refresh flag on failure, so that another try will - // be performed later. - if (_locale_rebuild_js()) { - unset($parsed['refresh:' . $language->language]); - } - // Store any changes after refresh was attempted. - variable_set('javascript_parsed', $parsed); - } - // If no refresh was attempted, but we have new source files, we need - // to store them too. This occurs if current page is in English. - elseif ($new_files) { - variable_set('javascript_parsed', $parsed); - } - - // Add the translation JavaScript file to the page. - if ($files && !empty($language->javascript)) { - // Add the translation JavaScript file to the page. - $file = $dir . '/' . $language->language . '_' . $language->javascript . '.js'; - $javascript[$file] = drupal_js_defaults($file); - } -} - -/** - * Implements hook_css_alter(). - * - * This function checks all CSS files currently added via drupal_add_css() and - * and checks to see if a related right to left CSS file should be included. - */ -function locale_css_alter(&$css) { - global $language; - - // If the current language is RTL, add the CSS file with the RTL overrides. - if ($language->direction == LANGUAGE_RTL) { - foreach ($css as $data => $item) { - // Only provide RTL overrides for files. - if ($item['type'] == 'file') { - $rtl_path = str_replace('.css', '-rtl.css', $item['data']); - if (file_exists($rtl_path) && !isset($css[$rtl_path])) { - // Replicate the same item, but with the RTL path and a little larger - // weight so that it appears directly after the original CSS file. - $item['data'] = $rtl_path; - $item['weight'] += 0.01; - $css[$rtl_path] = $item; - } - } - } - } -} - - /** - * Implement hook_library_alter(). - * - * Provides the language support for the jQuery UI Date Picker. - */ -function locale_library_alter(&$libraries, $module) { - global $language; - if ($module == 'system' && isset($libraries['system']['ui.datepicker'])) { - $datepicker = drupal_get_path('module', 'locale') . '/locale.datepicker.js'; - $libraries['system']['ui.datepicker']['js'][$datepicker] = array('group' => JS_THEME); - $libraries['system']['ui.datepicker']['js'][] = array( - 'data' => array( - 'jqueryuidatepicker' => array( - 'rtl' => $language->direction == LANGUAGE_RTL, - 'firstDay' => variable_get('date_first_day', 0), - ), - ), - 'type' => 'setting', - ); - } -} - -// --------------------------------------------------------------------------------- -// Language switcher block - -/** - * Implements hook_block_info(). - */ -function locale_block_info() { - include_once DRUPAL_ROOT . '/includes/language.inc'; - $block = array(); - $info = language_types_info(); - foreach (language_types_configurable(FALSE) as $type) { - $block[$type] = array( - 'info' => t('Language switcher (@type)', array('@type' => $info[$type]['name'])), - // Not worth caching. - 'cache' => DRUPAL_NO_CACHE, - ); - } - return $block; -} - -/** - * Implements hook_block_view(). - * - * Displays a language switcher. Only show if we have at least two languages. - */ -function locale_block_view($type) { - if (drupal_multilingual()) { - $path = drupal_is_front_page() ? '' : $_GET['q']; - $links = language_negotiation_get_switch_links($type, $path); - - if (isset($links->links)) { - drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css'); - $class = "language-switcher-{$links->provider}"; - $variables = array('links' => $links->links, 'attributes' => array('class' => array($class))); - $block['content'] = theme('links__locale_block', $variables); - $block['subject'] = t('Languages'); - return $block; - } - } -} - -/** - * Implements hook_url_outbound_alter(). - * - * Rewrite outbound URLs with language based prefixes. - */ -function locale_url_outbound_alter(&$path, &$options, $original_path) { - // Only modify internal URLs. - if (!$options['external'] && drupal_multilingual()) { - static $drupal_static_fast; - if (!isset($drupal_static_fast)) { - $drupal_static_fast['callbacks'] = &drupal_static(__FUNCTION__); - } - $callbacks = &$drupal_static_fast['callbacks']; - - if (!isset($callbacks)) { - $callbacks = array(); - include_once DRUPAL_ROOT . '/includes/language.inc'; - - foreach (language_types_configurable() as $type) { - // Get url rewriter callbacks only from enabled language providers. - $negotiation = variable_get("language_negotiation_$type", array()); - - foreach ($negotiation as $id => $provider) { - if (isset($provider['file'])) { - require_once DRUPAL_ROOT . '/' . $provider['file']; - } - - // Avoid duplicate callback entries. - if (isset($provider['callbacks']['url_rewrite'])) { - $callbacks[$provider['callbacks']['url_rewrite']] = NULL; - } - } - } - - $callbacks = array_keys($callbacks); - } - - foreach ($callbacks as $callback) { - $callback($path, $options); - } - - // No language dependent path allowed in this mode. - if (empty($callbacks)) { - unset($options['language']); - } - } -} - -/** - * Implements hook_form_FORM_ID_alter(). - */ -function locale_form_comment_form_alter(&$form, &$form_state, $form_id) { - // If a content type has multilingual support we set the content language as - // comment language. - if ($form['language']['#value'] == LANGUAGE_NONE && locale_multilingual_node_type($form['#node']->type)) { - global $language_content; - $form['language']['#value'] = $language_content->language; - } -} diff --git a/modules/locale/locale.test b/modules/locale/locale.test deleted file mode 100644 index 42a6dbc..0000000 --- a/modules/locale/locale.test +++ /dev/null @@ -1,2534 +0,0 @@ - 'Language configuration', - 'description' => 'Adds a new locale and tests changing its status and the default language.', - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp('locale'); - } - - /** - * Functional tests for adding, editing and deleting languages. - */ - function testLanguageConfiguration() { - global $base_url; - - // User to add and remove language. - $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages')); - $this->drupalLogin($admin_user); - - // Add predefined language. - $edit = array( - 'langcode' => 'fr', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); - $this->assertText('fr', t('Language added successfully.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); - - // Add custom language. - // Code for the language. - $langcode = 'xx'; - // The English name for the language. - $name = $this->randomName(16); - // The native name for the language. - $native = $this->randomName(16); - // The domain prefix. - $prefix = $langcode; - $edit = array( - 'langcode' => $langcode, - 'name' => $name, - 'native' => $native, - 'prefix' => $prefix, - 'direction' => '0', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->assertText($langcode, t('Language code found.')); - $this->assertText($name, t('Name found.')); - $this->assertText($native, t('Native found.')); - $this->assertText($native, t('Test language added.')); - - // Check if we can change the default language. - $path = 'admin/config/regional/language'; - $this->drupalGet($path); - $this->assertFieldChecked('edit-site-default-en', t('English is the default language.')); - // Change the default language. - $edit = array( - 'site_default' => $langcode, - ); - $this->drupalPost(NULL, $edit, t('Save configuration')); - $this->assertNoFieldChecked('edit-site-default-en', t('Default language updated.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); - - // Check if a valid language prefix is added after changing the default - // language. - $this->drupalGet('admin/config/regional/language/edit/en'); - $this->assertFieldByXPath('//input[@name="prefix"]', 'en', t('A valid path prefix has been added to the previous default language.')); - - // Ensure we can't delete the default language. - $this->drupalGet('admin/config/regional/language/delete/' . $langcode); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->assertText(t('The default language cannot be deleted.'), t('Failed to delete the default language.')); - - // Check if we can disable a language. - $edit = array( - 'enabled[en]' => FALSE, - ); - $this->drupalPost($path, $edit, t('Save configuration')); - $this->assertNoFieldChecked('edit-enabled-en', t('Language disabled.')); - - // Set disabled language to be the default and ensure it is re-enabled. - $edit = array( - 'site_default' => 'en', - ); - $this->drupalPost(NULL, $edit, t('Save configuration')); - $this->assertFieldChecked('edit-enabled-en', t('Default language re-enabled.')); - - // Ensure 'edit' link works. - $this->clickLink(t('edit')); - $this->assertTitle(t('Edit language | Drupal'), t('Page title is "Edit language".')); - // Edit a language. - $name = $this->randomName(16); - $edit = array( - 'name' => $name, - ); - $this->drupalPost('admin/config/regional/language/edit/' . $langcode, $edit, t('Save language')); - $this->assertRaw($name, t('The language has been updated.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); - - // Ensure 'delete' link works. - $this->drupalGet('admin/config/regional/language'); - $this->clickLink(t('delete')); - $this->assertText(t('Are you sure you want to delete the language'), t('"delete" link is correct.')); - // Delete an enabled language. - $this->drupalGet('admin/config/regional/language/delete/' . $langcode); - // First test the 'cancel' link. - $this->clickLink(t('Cancel')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->assertRaw($name, t('The language was not deleted.')); - // Delete the language for real. This a confirm form, we do not need any - // fields changed. - $this->drupalPost('admin/config/regional/language/delete/' . $langcode, array(), t('Delete')); - // We need raw here because %locale will add HTML. - $this->assertRaw(t('The language %locale has been removed.', array('%locale' => $name)), t('The test language has been removed.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); - // Verify that language is no longer found. - $this->drupalGet('admin/config/regional/language/delete/' . $langcode); - $this->assertResponse(404, t('Language no longer found.')); - // Make sure the "language_count" variable has been updated correctly. - drupal_static_reset('language_list'); - $enabled = language_list('enabled'); - $this->assertEqual(variable_get('language_count', 1), count($enabled[1]), t('Language count is correct.')); - // Delete a disabled language. - // Disable an enabled language. - $edit = array( - 'enabled[fr]' => FALSE, - ); - $this->drupalPost($path, $edit, t('Save configuration')); - $this->assertNoFieldChecked('edit-enabled-fr', t('French language disabled.')); - // Get the count of enabled languages. - drupal_static_reset('language_list'); - $enabled = language_list('enabled'); - // Delete the disabled language. - $this->drupalPost('admin/config/regional/language/delete/fr', array(), t('Delete')); - // We need raw here because %locale will add HTML. - $this->assertRaw(t('The language %locale has been removed.', array('%locale' => 'French')), t('Disabled language has been removed.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); - // Verify that language is no longer found. - $this->drupalGet('admin/config/regional/language/delete/fr'); - $this->assertResponse(404, t('Language no longer found.')); - // Make sure the "language_count" variable has not changed. - $this->assertEqual(variable_get('language_count', 1), count($enabled[1]), t('Language count is correct.')); - - - // Ensure we can't delete the English language. - $this->drupalGet('admin/config/regional/language/delete/en'); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->assertText(t('The English language cannot be deleted.'), t('Failed to delete English language.')); - } - -} - -/** - * Functional test for string translation and validation. - */ -class LocaleTranslationFunctionalTest extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'String translate, search and validate', - 'description' => 'Adds a new locale and translates its name. Checks the validation of translation strings and search results.', - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp('locale'); - } - - /** - * Adds a language and tests string translation by users with the appropriate permissions. - */ - function testStringTranslation() { - global $base_url; - - // User to add and remove language. - $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages')); - // User to translate and delete string. - $translate_user = $this->drupalCreateUser(array('translate interface', 'access administration pages')); - // Code for the language. - $langcode = 'xx'; - // The English name for the language. This will be translated. - $name = $this->randomName(16); - // The native name for the language. - $native = $this->randomName(16); - // The domain prefix. - $prefix = $langcode; - // This is the language indicator on the translation search screen for - // untranslated strings. Copied straight from locale.inc. - $language_indicator = "$langcode "; - // This will be the translation of $name. - $translation = $this->randomName(16); - - // Add custom language. - $this->drupalLogin($admin_user); - $edit = array( - 'langcode' => $langcode, - 'name' => $name, - 'native' => $native, - 'prefix' => $prefix, - 'direction' => '0', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); - // Add string. - t($name, array(), array('langcode' => $langcode)); - // Reset locale cache. - locale_reset(); - $this->assertText($langcode, t('Language code found.')); - $this->assertText($name, t('Name found.')); - $this->assertText($native, t('Native found.')); - // No t() here, we do not want to add this string to the database and it's - // surely not translated yet. - $this->assertText($native, t('Test language added.')); - $this->drupalLogout(); - - // Search for the name and translate it. - $this->drupalLogin($translate_user); - $search = array( - 'string' => $name, - 'language' => 'all', - 'translation' => 'all', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - // assertText() seems to remove the input field where $name always could be - // found, so this is not a false assert. See how assertNoText succeeds - // later. - $this->assertText($name, t('Search found the name.')); - $this->assertRaw($language_indicator, t('Name is untranslated.')); - // Assume this is the only result, given the random name. - $this->clickLink(t('edit')); - // We save the lid from the path. - $matches = array(); - preg_match('!admin/config/regional/translate/edit/(\d+)!', $this->getUrl(), $matches); - $lid = $matches[1]; - // No t() here, it's surely not translated yet. - $this->assertText($name, t('name found on edit screen.')); - $edit = array( - "translations[$langcode]" => $translation, - ); - $this->drupalPost(NULL, $edit, t('Save translations')); - $this->assertText(t('The string has been saved.'), t('The string has been saved.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->assertTrue($name != $translation && t($name, array(), array('langcode' => $langcode)) == $translation, t('t() works.')); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - // The indicator should not be here. - $this->assertNoRaw($language_indicator, t('String is translated.')); - - // Try to edit a non-existent string and ensure we're redirected correctly. - // Assuming we don't have 999,999 strings already. - $random_lid = 999999; - $this->drupalGet('admin/config/regional/translate/edit/' . $random_lid); - $this->assertText(t('String not found'), t('String not found.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->drupalLogout(); - - // Delete the language. - $this->drupalLogin($admin_user); - $path = 'admin/config/regional/language/delete/' . $langcode; - // This a confirm form, we do not need any fields changed. - $this->drupalPost($path, array(), t('Delete')); - // We need raw here because %locale will add HTML. - $this->assertRaw(t('The language %locale has been removed.', array('%locale' => $name)), t('The test language has been removed.')); - // Reload to remove $name. - $this->drupalGet($path); - $this->assertNoText($langcode, t('Language code not found.')); - $this->assertNoText($name, t('Name not found.')); - $this->assertNoText($native, t('Native not found.')); - $this->drupalLogout(); - - // Delete the string. - $this->drupalLogin($translate_user); - $search = array( - 'string' => $name, - 'language' => 'all', - 'translation' => 'all', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - // Assume this is the only result, given the random name. - $this->clickLink(t('delete')); - $this->assertText(t('Are you sure you want to delete the string'), t('"delete" link is correct.')); - // Delete the string. - $path = 'admin/config/regional/translate/delete/' . $lid; - $this->drupalGet($path); - // First test the 'cancel' link. - $this->clickLink(t('Cancel')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->assertRaw($name, t('The string was not deleted.')); - // Delete the name string. - $this->drupalPost('admin/config/regional/translate/delete/' . $lid, array(), t('Delete')); - $this->assertText(t('The string has been removed.'), t('The string has been removed message.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertNoText($name, t('Search now can not find the name.')); - } - - /* - * Adds a language and checks that the JavaScript translation files are - * properly created and rebuilt on deletion. - */ - function testJavaScriptTranslation() { - $user = $this->drupalCreateUser(array('translate interface', 'administer languages', 'access administration pages')); - $this->drupalLogin($user); - - $langcode = 'xx'; - // The English name for the language. This will be translated. - $name = $this->randomName(16); - // The native name for the language. - $native = $this->randomName(16); - // The domain prefix. - $prefix = $langcode; - - // Add custom language. - $edit = array( - 'langcode' => $langcode, - 'name' => $name, - 'native' => $native, - 'prefix' => $prefix, - 'direction' => '0', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); - drupal_static_reset('language_list'); - - // Build the JavaScript translation file. - $this->drupalGet('admin/config/regional/translate/translate'); - - // Retrieve the id of the first string available in the {locales_source} - // table and translate it. - $query = db_select('locales_source', 'l'); - $query->addExpression('min(l.lid)', 'lid'); - $result = $query->condition('l.location', '%.js%', 'LIKE') - ->condition('l.textgroup', 'default') - ->execute(); - $url = 'admin/config/regional/translate/edit/' . $result->fetchObject()->lid; - $edit = array('translations['. $langcode .']' => $this->randomName()); - $this->drupalPost($url, $edit, t('Save translations')); - - // Trigger JavaScript translation parsing and building. - require_once DRUPAL_ROOT . '/includes/locale.inc'; - _locale_rebuild_js($langcode); - - // Retrieve the JavaScript translation hash code for the custom language to - // check that the translation file has been properly built. - $file = db_select('languages', 'l') - ->fields('l', array('javascript')) - ->condition('language', $langcode) - ->execute() - ->fetchObject(); - $js_file = 'public://' . variable_get('locale_js_directory', 'languages') . '/' . $langcode . '_' . $file->javascript . '.js'; - $this->assertTrue($result = file_exists($js_file), t('JavaScript file created: %file', array('%file' => $result ? $js_file : t('not found')))); - - // Test JavaScript translation rebuilding. - file_unmanaged_delete($js_file); - $this->assertTrue($result = !file_exists($js_file), t('JavaScript file deleted: %file', array('%file' => $result ? $js_file : t('found')))); - cache_clear_all(); - _locale_rebuild_js($langcode); - $this->assertTrue($result = file_exists($js_file), t('JavaScript file rebuilt: %file', array('%file' => $result ? $js_file : t('not found')))); - } - - /** - * Tests the validation of the translation input. - */ - function testStringValidation() { - global $base_url; - - // User to add language and strings. - $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'translate interface')); - $this->drupalLogin($admin_user); - $langcode = 'xx'; - // The English name for the language. This will be translated. - $name = $this->randomName(16); - // The native name for the language. - $native = $this->randomName(16); - // The domain prefix. - $prefix = $langcode; - // This is the language indicator on the translation search screen for - // untranslated strings. Copied straight from locale.inc. - $language_indicator = "$langcode "; - // These will be the invalid translations of $name. - $key = $this->randomName(16); - $bad_translations[$key] = "" . $key; - $key = $this->randomName(16); - $bad_translations[$key] = '' . $key; - $key = $this->randomName(16); - $bad_translations[$key] = '<' . $key; - $key = $this->randomName(16); - $bad_translations[$key] ="" . $key; - - // Add custom language. - $edit = array( - 'langcode' => $langcode, - 'name' => $name, - 'native' => $native, - 'prefix' => $prefix, - 'direction' => '0', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); - // Add string. - t($name, array(), array('langcode' => $langcode)); - // Reset locale cache. - $search = array( - 'string' => $name, - 'language' => 'all', - 'translation' => 'all', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - // Find the edit path. - $content = $this->drupalGetContent(); - $this->assertTrue(preg_match('@(admin/config/regional/translate/edit/[0-9]+)@', $content, $matches), t('Found the edit path.')); - $path = $matches[0]; - foreach ($bad_translations as $key => $translation) { - $edit = array( - "translations[$langcode]" => $translation, - ); - $this->drupalPost($path, $edit, t('Save translations')); - // Check for a form error on the textarea. - $form_class = $this->xpath('//form[@id="locale-translate-edit-form"]//textarea/@class'); - $this->assertNotIdentical(FALSE, strpos($form_class[0], 'error'), t('The string was rejected as unsafe.')); - $this->assertNoText(t('The string has been saved.'), t('The string was not saved.')); - } - } - - /** - * Tests translation search form. - */ - function testStringSearch() { - global $base_url; - - // User to add and remove language. - $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages')); - // User to translate and delete string. - $translate_user = $this->drupalCreateUser(array('translate interface', 'access administration pages')); - - // Code for the language. - $langcode = 'xx'; - // The English name for the language. This will be translated. - $name = $this->randomName(16); - // The native name for the language. - $native = $this->randomName(16); - // The domain prefix. - $prefix = $langcode; - // This is the language indicator on the translation search screen for - // untranslated strings. Copied straight from locale.inc. - $language_indicator = "$langcode "; - // This will be the translation of $name. - $translation = $this->randomName(16); - - // Add custom language. - $this->drupalLogin($admin_user); - $edit = array( - 'langcode' => $langcode, - 'name' => $name, - 'native' => $native, - 'prefix' => $prefix, - 'direction' => '0', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); - // Add string. - t($name, array(), array('langcode' => $langcode)); - // Reset locale cache. - locale_reset(); - $this->drupalLogout(); - - // Search for the name. - $this->drupalLogin($translate_user); - $search = array( - 'string' => $name, - 'language' => 'all', - 'translation' => 'all', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - // assertText() seems to remove the input field where $name always could be - // found, so this is not a false assert. See how assertNoText succeeds - // later. - $this->assertText($name, t('Search found the string.')); - - // Ensure untranslated string doesn't appear if searching on 'only - // translated strings'. - $search = array( - 'string' => $name, - 'language' => 'all', - 'translation' => 'translated', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertText(t('No strings available.'), t("Search didn't find the string.")); - - // Ensure untranslated string appears if searching on 'only untranslated - // strings'. - $search = array( - 'string' => $name, - 'language' => 'all', - 'translation' => 'untranslated', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertNoText(t('No strings available.'), t('Search found the string.')); - - // Add translation. - // Assume this is the only result, given the random name. - $this->clickLink(t('edit')); - // We save the lid from the path. - $matches = array(); - preg_match('!admin/config/regional/translate/edit/(\d)+!', $this->getUrl(), $matches); - $lid = $matches[1]; - $edit = array( - "translations[$langcode]" => $translation, - ); - $this->drupalPost(NULL, $edit, t('Save translations')); - - // Ensure translated string does appear if searching on 'only - // translated strings'. - $search = array( - 'string' => $translation, - 'language' => 'all', - 'translation' => 'translated', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertNoText(t('No strings available.'), t('Search found the translation.')); - - // Ensure translated source string doesn't appear if searching on 'only - // untranslated strings'. - $search = array( - 'string' => $name, - 'language' => 'all', - 'translation' => 'untranslated', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertText(t('No strings available.'), t("Search didn't find the source string.")); - - // Ensure translated string doesn't appear if searching on 'only - // untranslated strings'. - $search = array( - 'string' => $translation, - 'language' => 'all', - 'translation' => 'untranslated', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertText(t('No strings available.'), t("Search didn't find the translation.")); - - // Ensure translated string does appear if searching on the custom language. - $search = array( - 'string' => $translation, - 'language' => $langcode, - 'translation' => 'all', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertNoText(t('No strings available.'), t('Search found the translation.')); - - // Ensure translated string doesn't appear if searching on English. - $search = array( - 'string' => $translation, - 'language' => 'en', - 'translation' => 'all', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertText(t('No strings available.'), t("Search didn't find the translation.")); - - // Search for a string that isn't in the system. - $unavailable_string = $this->randomName(16); - $search = array( - 'string' => $unavailable_string, - 'language' => 'all', - 'translation' => 'all', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertText(t('No strings available.'), t("Search didn't find the invalid string.")); - } -} - -/** - * Functional tests for the import of translation files. - */ -class LocaleImportFunctionalTest extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'Translation import', - 'description' => 'Tests the import of locale files.', - 'group' => 'Locale', - ); - } - - /** - * A user able to create languages and import translations. - */ - protected $admin_user = NULL; - - function setUp() { - parent::setUp('locale', 'locale_test'); - - $this->admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages')); - $this->drupalLogin($this->admin_user); - } - - /** - * Test import of standalone .po files. - */ - function testStandalonePoFile() { - // Try importing a .po file. - $this->importPoFile($this->getPoFile(), array( - 'langcode' => 'fr', - )); - - // The import should automatically create the corresponding language. - $this->assertRaw(t('The language %language has been created.', array('%language' => 'French')), t('The language has been automatically created.')); - - // The import should have created 7 strings. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 9, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); - - // This import should have saved plural forms to have 2 variants. - $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, t('Plural number initialized.')); - - // Ensure we were redirected correctly. - $this->assertEqual($this->getUrl(), url('admin/config/regional/translate', array('absolute' => TRUE)), t('Correct page redirection.')); - - - // Try importing a .po file with invalid tags in the default text group. - $this->importPoFile($this->getBadPoFile(), array( - 'langcode' => 'fr', - )); - - // The import should have created 1 string and rejected 2. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); - $skip_message = format_plural(2, 'One translation string was skipped because it contains disallowed HTML.', '@count translation strings were skipped because they contain disallowed HTML.'); - $this->assertRaw($skip_message, t('Unsafe strings were skipped.')); - - - // Try importing a .po file with invalid tags in a non default text group. - $this->importPoFile($this->getBadPoFile(), array( - 'langcode' => 'fr', - 'group' => 'custom', - )); - - // The import should have created 3 strings. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 3, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); - - - // Try importing a .po file which doesn't exist. - $name = $this->randomName(16); - $this->drupalPost('admin/config/regional/translate/import', array( - 'langcode' => 'fr', - 'files[file]' => $name, - 'group' => 'custom', - ), t('Import')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/import', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->assertText(t('File to import not found.'), t('File to import not found message.')); - - - // Try importing a .po file with overriding strings, and ensure existing - // strings are kept. - $this->importPoFile($this->getOverwritePoFile(), array( - 'langcode' => 'fr', - 'mode' => 1, // Existing strings are kept, only new strings are added. - )); - - // The import should have created 1 string. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); - // Ensure string wasn't overwritten. - $search = array( - 'string' => 'Montag', - 'language' => 'fr', - 'translation' => 'translated', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertText(t('No strings available.'), t('String not overwritten by imported string.')); - - // This import should not have changed number of plural forms. - $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, t('Plural numbers untouched.')); - - // Try importing a .po file with overriding strings, and ensure existing - // strings are overwritten. - $this->importPoFile($this->getOverwritePoFile(), array( - 'langcode' => 'fr', - 'mode' => 0, // Strings in the uploaded file replace existing ones, new ones are added. - )); - - // The import should have updated 2 strings. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The translation file was successfully imported.')); - // Ensure string was overwritten. - $search = array( - 'string' => 'Montag', - 'language' => 'fr', - 'translation' => 'translated', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertNoText(t('No strings available.'), t('String overwritten by imported string.')); - // This import should have changed number of plural forms. - $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 3, t('Plural numbers changed.')); - } - - /** - * Test automatic import of a module's translation files when a language is - * enabled. - */ - function testAutomaticModuleTranslationImportLanguageEnable() { - // Code for the language - manually set to match the test translation file. - $langcode = 'xx'; - // The English name for the language. - $name = $this->randomName(16); - // The native name for the language. - $native = $this->randomName(16); - // The domain prefix. - $prefix = $langcode; - - // Create a custom language. - $edit = array( - 'langcode' => $langcode, - 'name' => $name, - 'native' => $native, - 'prefix' => $prefix, - 'direction' => '0', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); - - // Ensure the translation file was automatically imported when language was - // added. - $this->assertText(t('One translation file imported for the enabled modules.'), t('Language file automatically imported.')); - - // Ensure strings were successfully imported. - $search = array( - 'string' => 'lundi', - 'language' => $langcode, - 'translation' => 'translated', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertNoText(t('No strings available.'), t('String successfully imported.')); - } - - /** - * Test msgctxt context support. - */ - function testLanguageContext() { - // Try importing a .po file. - $this->importPoFile($this->getPoFileWithContext(), array( - 'langcode' => 'hr', - )); - - $this->assertIdentical(t('May', array(), array('langcode' => 'hr', 'context' => 'Long month name')), 'Svibanj', t('Long month name context is working.')); - $this->assertIdentical(t('May', array(), array('langcode' => 'hr')), 'Svi.', t('Default context is working.')); - } - - /** - * Test empty msgstr at end of .po file see #611786. - */ - function testEmptyMsgstr() { - $langcode = 'hu'; - - // Try importing a .po file. - $this->importPoFile($this->getPoFileWithMsgstr(), array( - 'langcode' => $langcode, - )); - - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); - $this->assertIdentical(t('Operations', array(), array('langcode' => $langcode)), 'Műveletek', t('String imported and translated.')); - - // Try importing a .po file. - $this->importPoFile($this->getPoFileWithEmptyMsgstr(), array( - 'langcode' => $langcode, - 'mode' => 0, - )); - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 0, '%delete' => 1)), t('The translation file was successfully imported.')); - // This is the language indicator on the translation search screen for - // untranslated strings. Copied straight from locale.inc. - $language_indicator = "$langcode "; - $str = "Operations"; - $search = array( - 'string' => $str, - 'language' => 'all', - 'translation' => 'all', - 'group' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - // assertText() seems to remove the input field where $str always could be - // found, so this is not a false assert. - $this->assertText($str, t('Search found the string.')); - $this->assertRaw($language_indicator, t('String is untranslated again.')); - } - - /** - * Helper function: import a standalone .po file in a given language. - * - * @param $contents - * Contents of the .po file to import. - * @param $options - * Additional options to pass to the translation import form. - */ - function importPoFile($contents, array $options = array()) { - $name = tempnam('temporary://', "po_") . '.po'; - file_put_contents($name, $contents); - $options['files[file]'] = $name; - $this->drupalPost('admin/config/regional/translate/import', $options, t('Import')); - drupal_unlink($name); - } - - /** - * Helper function that returns a proper .po file. - */ - function getPoFile() { - return <<< EOF -msgid "" -msgstr "" -"Project-Id-Version: Drupal 7\\n" -"MIME-Version: 1.0\\n" -"Content-Type: text/plain; charset=UTF-8\\n" -"Content-Transfer-Encoding: 8bit\\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\\n" - -msgid "One sheep" -msgid_plural "@count sheep" -msgstr[0] "un mouton" -msgstr[1] "@count moutons" - -msgid "Monday" -msgstr "lundi" - -msgid "Tuesday" -msgstr "mardi" - -msgid "Wednesday" -msgstr "mercredi" - -msgid "Thursday" -msgstr "jeudi" - -msgid "Friday" -msgstr "vendredi" - -msgid "Saturday" -msgstr "samedi" - -msgid "Sunday" -msgstr "dimanche" -EOF; - } - - /** - * Helper function that returns a bad .po file. - */ - function getBadPoFile() { - return <<< EOF -msgid "" -msgstr "" -"Project-Id-Version: Drupal 7\\n" -"MIME-Version: 1.0\\n" -"Content-Type: text/plain; charset=UTF-8\\n" -"Content-Transfer-Encoding: 8bit\\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\\n" - -msgid "Save configuration" -msgstr "Enregistrer la configuration" - -msgid "edit" -msgstr "modifier" - -msgid "delete" -msgstr "supprimer" - -EOF; - } - - /** - * Helper function that returns a proper .po file, for testing overwriting - * existing translations. - */ - function getOverwritePoFile() { - return <<< EOF -msgid "" -msgstr "" -"Project-Id-Version: Drupal 7\\n" -"MIME-Version: 1.0\\n" -"Content-Type: text/plain; charset=UTF-8\\n" -"Content-Transfer-Encoding: 8bit\\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n" - -msgid "Monday" -msgstr "Montag" - -msgid "Day" -msgstr "Jour" -EOF; - } - - /** - * Helper function that returns a .po file with context. - */ - function getPoFileWithContext() { - // Croatian (code hr) is one the the languages that have a different - // form for the full name and the abbreviated name for the month May. - return <<< EOF -msgid "" -msgstr "" -"Project-Id-Version: Drupal 7\\n" -"MIME-Version: 1.0\\n" -"Content-Type: text/plain; charset=UTF-8\\n" -"Content-Transfer-Encoding: 8bit\\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n" - -msgctxt "Long month name" -msgid "May" -msgstr "Svibanj" - -msgid "May" -msgstr "Svi." -EOF; - } - - /** - * Helper function that returns a .po file with an empty last item. - */ - function getPoFileWithEmptyMsgstr() { - return <<< EOF -msgid "" -msgstr "" -"Project-Id-Version: Drupal 7\\n" -"MIME-Version: 1.0\\n" -"Content-Type: text/plain; charset=UTF-8\\n" -"Content-Transfer-Encoding: 8bit\\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\\n" - -msgid "Operations" -msgstr "" - -EOF; - } - /** - * Helper function that returns a .po file with an empty last item. - */ - function getPoFileWithMsgstr() { - return <<< EOF -msgid "" -msgstr "" -"Project-Id-Version: Drupal 7\\n" -"MIME-Version: 1.0\\n" -"Content-Type: text/plain; charset=UTF-8\\n" -"Content-Transfer-Encoding: 8bit\\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\\n" - -msgid "Operations" -msgstr "Műveletek" - -msgid "Will not appear in Drupal core, so we can ensure the test passes" -msgstr "" - -EOF; - } - -} - -/** - * Functional tests for the export of translation files. - */ -class LocaleExportFunctionalTest extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'Translation export', - 'description' => 'Tests the exportation of locale files.', - 'group' => 'Locale', - ); - } - - /** - * A user able to create languages and export translations. - */ - protected $admin_user = NULL; - - function setUp() { - parent::setUp('locale', 'locale_test'); - - $this->admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages')); - $this->drupalLogin($this->admin_user); - } - - /** - * Test exportation of translations. - */ - function testExportTranslation() { - // First import some known translations. - // This will also automatically enable the 'fr' language. - $name = tempnam('temporary://', "po_") . '.po'; - file_put_contents($name, $this->getPoFile()); - $this->drupalPost('admin/config/regional/translate/import', array( - 'langcode' => 'fr', - 'files[file]' => $name, - ), t('Import')); - drupal_unlink($name); - - // Get the French translations. - $this->drupalPost('admin/config/regional/translate/export', array( - 'langcode' => 'fr', - ), t('Export')); - - // Ensure we have a translation file. - $this->assertRaw('# French translation of Drupal', t('Exported French translation file.')); - // Ensure our imported translations exist in the file. - $this->assertRaw('msgstr "lundi"', t('French translations present in exported file.')); - } - - /** - * Test exportation of translation template file. - */ - function testExportTranslationTemplateFile() { - // Get the translation template file. - // There are two 'Export' buttons on this page, but it somehow works. It'd - // be better if we could use the submit button id like documented but that - // doesn't work. - $this->drupalPost('admin/config/regional/translate/export', array(), t('Export')); - // Ensure we have a translation file. - $this->assertRaw('# LANGUAGE translation of PROJECT', t('Exported translation template file.')); - } - - /** - * Helper function that returns a proper .po file. - */ - function getPoFile() { - return <<< EOF -msgid "" -msgstr "" -"Project-Id-Version: Drupal 6\\n" -"MIME-Version: 1.0\\n" -"Content-Type: text/plain; charset=UTF-8\\n" -"Content-Transfer-Encoding: 8bit\\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\\n" - -msgid "Monday" -msgstr "lundi" -EOF; - } - -} - -/** - * Tests for the st() function. - */ -class LocaleInstallTest extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'String translation using st()', - 'description' => 'Tests that st() works like t().', - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp('locale'); - - // st() lives in install.inc, so ensure that it is loaded for all tests. - require_once DRUPAL_ROOT . '/includes/install.inc'; - } - - /** - * Verify that function signatures of t() and st() are equal. - */ - function testFunctionSignatures() { - $reflector_t = new ReflectionFunction('t'); - $reflector_st = new ReflectionFunction('st'); - $this->assertEqual($reflector_t->getParameters(), $reflector_st->getParameters(), t('Function signatures of t() and st() are equal.')); - } -} - -/** - * Locale uninstall with English UI functional test. - */ -class LocaleUninstallFunctionalTest extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'Locale uninstall (EN)', - 'description' => 'Tests the uninstall process using the built-in UI language.', - 'group' => 'Locale', - ); - } - - /** - * The default language set for the UI before uninstall. - */ - protected $language; - - function setUp() { - parent::setUp('locale'); - $this->language = 'en'; - } - - /** - * Check if the values of the Locale variables are correct after uninstall. - */ - function testUninstallProcess() { - $locale_module = array('locale'); - - // Add a new language and optionally set it as default. - require_once DRUPAL_ROOT . '/includes/locale.inc'; - locale_add_language('fr', 'French', 'Français', LANGUAGE_LTR, '', '', TRUE, $this->language == 'fr'); - - // Check the UI language. - drupal_language_initialize(); - global $language; - $this->assertEqual($language->language, $this->language, t('Current language: %lang', array('%lang' => $language->language))); - - // Enable multilingual workflow option for articles. - variable_set('language_content_type_article', 1); - - // Change JavaScript translations directory. - variable_set('locale_js_directory', 'js_translations'); - - // Build the JavaScript translation file for French. - $user = $this->drupalCreateUser(array('translate interface', 'access administration pages')); - $this->drupalLogin($user); - $this->drupalGet('admin/config/regional/translate/translate'); - $string = db_query('SELECT min(lid) AS lid FROM {locales_source} WHERE location LIKE :location AND textgroup = :textgroup', array( - ':location' => '%.js%', - ':textgroup' => 'default', - ))->fetchObject(); - $edit = array('translations[fr]' => 'french translation'); - $this->drupalPost('admin/config/regional/translate/edit/' . $string->lid, $edit, t('Save translations')); - _locale_rebuild_js('fr'); - $file = db_query('SELECT javascript FROM {languages} WHERE language = :language', array(':language' => 'fr'))->fetchObject(); - $js_file = 'public://' . variable_get('locale_js_directory', 'languages') . '/fr_' . $file->javascript . '.js'; - $this->assertTrue($result = file_exists($js_file), t('JavaScript file created: %file', array('%file' => $result ? $js_file : t('none')))); - - // Disable string caching. - variable_set('locale_cache_strings', 0); - - // Change language negotiation options. - drupal_load('module', 'locale'); - variable_set('language_types', drupal_language_types() + array('language_custom' => TRUE)); - variable_set('language_negotiation_' . LANGUAGE_TYPE_INTERFACE, locale_language_negotiation_info()); - variable_set('language_negotiation_' . LANGUAGE_TYPE_CONTENT, locale_language_negotiation_info()); - variable_set('language_negotiation_' . LANGUAGE_TYPE_URL, locale_language_negotiation_info()); - - // Change language providers settings. - variable_set('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX); - variable_set('locale_language_negotiation_session_param', TRUE); - - // Uninstall Locale. - module_disable($locale_module); - drupal_uninstall_modules($locale_module); - - // Visit the front page. - $this->drupalGet(''); - - // Check the init language logic. - drupal_language_initialize(); - $this->assertEqual($language->language, 'en', t('Language after uninstall: %lang', array('%lang' => $language->language))); - - // Check JavaScript files deletion. - $this->assertTrue($result = !file_exists($js_file), t('JavaScript file deleted: %file', array('%file' => $result ? $js_file : t('found')))); - - // Check language count. - $language_count = variable_get('language_count', 1); - $this->assertEqual($language_count, 1, t('Language count: %count', array('%count' => $language_count))); - - // Check language negotiation. - require_once DRUPAL_ROOT . '/includes/language.inc'; - $this->assertTrue(count(language_types()) == count(drupal_language_types()), t('Language types reset')); - $language_negotiation = language_negotiation_get(LANGUAGE_TYPE_INTERFACE) == LANGUAGE_NEGOTIATION_DEFAULT; - $this->assertTrue($language_negotiation, t('Interface language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set')))); - $language_negotiation = language_negotiation_get(LANGUAGE_TYPE_CONTENT) == LANGUAGE_NEGOTIATION_DEFAULT; - $this->assertTrue($language_negotiation, t('Content language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set')))); - $language_negotiation = language_negotiation_get(LANGUAGE_TYPE_URL) == LANGUAGE_NEGOTIATION_DEFAULT; - $this->assertTrue($language_negotiation, t('URL language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set')))); - - // Check language providers settings. - $this->assertFalse(variable_get('locale_language_negotiation_url_part', FALSE), t('URL language provider indicator settings cleared.')); - $this->assertFalse(variable_get('locale_language_negotiation_session_param', FALSE), t('Visit language provider settings cleared.')); - - // Check JavaScript parsed. - $javascript_parsed_count = count(variable_get('javascript_parsed', array())); - $this->assertEqual($javascript_parsed_count, 0, t('JavaScript parsed count: %count', array('%count' => $javascript_parsed_count))); - - // Check multilingual workflow option for articles. - $multilingual = variable_get('language_content_type_article', 0); - $this->assertEqual($multilingual, 0, t('Multilingual workflow option: %status', array('%status' => t($multilingual ? 'enabled': 'disabled')))); - - // Check JavaScript translations directory. - $locale_js_directory = variable_get('locale_js_directory', 'languages'); - $this->assertEqual($locale_js_directory, 'languages', t('JavaScript translations directory: %dir', array('%dir' => $locale_js_directory))); - - // Check string caching. - $locale_cache_strings = variable_get('locale_cache_strings', 1); - $this->assertEqual($locale_cache_strings, 1, t('String caching: %status', array('%status' => t($locale_cache_strings ? 'enabled': 'disabled')))); - } -} - -/** - * Locale uninstall with French UI functional test. - * - * Because this class extends LocaleUninstallFunctionalTest, it doesn't require a new - * test of its own. Rather, it switches the default UI language in setUp and then - * runs the testUninstallProcess (which it inherits from LocaleUninstallFunctionalTest) - * to test with this new language. - */ -class LocaleUninstallFrenchFunctionalTest extends LocaleUninstallFunctionalTest { - public static function getInfo() { - return array( - 'name' => 'Locale uninstall (FR)', - 'description' => 'Tests the uninstall process using French as interface language.', - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp(); - $this->language = 'fr'; - } -} - - -/** - * Functional tests for the language switching feature. - */ -class LocaleLanguageSwitchingFunctionalTest extends DrupalWebTestCase { - - public static function getInfo() { - return array( - 'name' => 'Language switching', - 'description' => 'Tests for the language switching feature.', - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp('locale'); - - // Create and login user. - $admin_user = $this->drupalCreateUser(array('administer blocks', 'administer languages', 'translate interface', 'access administration pages')); - $this->drupalLogin($admin_user); - } - - /** - * Functional tests for the language switcher block. - */ - function testLanguageBlock() { - // Enable the language switching block. - $language_type = LANGUAGE_TYPE_INTERFACE; - $edit = array( - "blocks[locale_{$language_type}][region]" => 'sidebar_first', - ); - $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); - - // Add language. - $edit = array( - 'langcode' => 'fr', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); - - // Enable URL language detection and selection. - $edit = array('language[enabled][locale-url]' => '1'); - $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); - - // Assert that the language switching block is displayed on the frontpage. - $this->drupalGet(''); - $this->assertText(t('Languages'), t('Language switcher block found.')); - - // Assert that only the current language is marked as active. - list($language_switcher) = $this->xpath('//div[@id=:id]/div[@class="content"]', array(':id' => 'block-locale-' . $language_type)); - $links = array( - 'active' => array(), - 'inactive' => array(), - ); - $anchors = array( - 'active' => array(), - 'inactive' => array(), - ); - foreach ($language_switcher->ul->li as $link) { - $classes = explode(" ", (string) $link['class']); - list($language) = array_intersect($classes, array('en', 'fr')); - if (in_array('active', $classes)) { - $links['active'][] = $language; - } - else { - $links['inactive'][] = $language; - } - $anchor_classes = explode(" ", (string) $link->a['class']); - if (in_array('active', $anchor_classes)) { - $anchors['active'][] = $language; - } - else { - $anchors['inactive'][] = $language; - } - } - $this->assertIdentical($links, array('active' => array('en'), 'inactive' => array('fr')), t('Only the current language list item is marked as active on the language switcher block.')); - $this->assertIdentical($anchors, array('active' => array('en'), 'inactive' => array('fr')), t('Only the current language anchor is marked as active on the language switcher block.')); - } -} - -/** - * Functional tests for a user's ability to change their default language. - */ -class LocaleUserLanguageFunctionalTest extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'User language settings', - 'description' => "Tests user's ability to change their default language.", - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp('locale'); - } - - /** - * Test if user can change their default language. - */ - function testUserLanguageConfiguration() { - global $base_url; - - // User to add and remove language. - $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages')); - // User to change their default language. - $web_user = $this->drupalCreateUser(); - - // Add custom language. - $this->drupalLogin($admin_user); - // Code for the language. - $langcode = 'xx'; - // The English name for the language. - $name = $this->randomName(16); - // The native name for the language. - $native = $this->randomName(16); - // The domain prefix. - $prefix = 'xx'; - $edit = array( - 'langcode' => $langcode, - 'name' => $name, - 'native' => $native, - 'prefix' => $prefix, - 'direction' => '0', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); - - // Add custom language and disable it. - // Code for the language. - $langcode_disabled = 'xx-yy'; - // The English name for the language. This will be translated. - $name_disabled = $this->randomName(16); - // The native name for the language. - $native_disabled = $this->randomName(16); - // The domain prefix. - $prefix_disabled = $langcode_disabled; - $edit = array( - 'langcode' => $langcode_disabled, - 'name' => $name_disabled, - 'native' => $native_disabled, - 'prefix' => $prefix_disabled, - 'direction' => '0', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); - // Disable the language. - $edit = array( - 'enabled[' . $langcode_disabled . ']' => FALSE, - ); - $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration')); - $this->drupalLogout(); - - // Login as normal user and edit account settings. - $this->drupalLogin($web_user); - $path = 'user/' . $web_user->uid . '/edit'; - $this->drupalGet($path); - // Ensure language settings fieldset is available. - $this->assertText(t('Language settings'), t('Language settings available.')); - // Ensure custom language is present. - $this->assertText($name, t('Language present on form.')); - // Ensure disabled language isn't present. - $this->assertNoText($name_disabled, t('Disabled language not present on form.')); - // Switch to our custom language. - $edit = array( - 'language' => $langcode, - ); - $this->drupalPost($path, $edit, t('Save')); - // Ensure form was submitted successfully. - $this->assertText(t('The changes have been saved.'), t('Changes were saved.')); - // Check if language was changed. - $elements = $this->xpath('//input[@id=:id]', array(':id' => 'edit-language-' . $langcode)); - $this->assertTrue(isset($elements[0]) && !empty($elements[0]['checked']), t('Default language successfully updated.')); - - $this->drupalLogout(); - } -} - -/** - * Functional test for language handling during user creation. - */ -class LocaleUserCreationTest extends DrupalWebTestCase { - - public static function getInfo() { - return array( - 'name' => 'User creation', - 'description' => 'Tests whether proper language is stored for new users and access to language selector.', - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp('locale'); - variable_set('user_register', USER_REGISTER_VISITORS); - } - - /** - * Functional test for language handling during user creation. - */ - function testLocalUserCreation() { - // User to add and remove language and create new users. - $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'administer users')); - $this->drupalLogin($admin_user); - - // Add predefined language. - $langcode = 'fr'; - $edit = array( - 'langcode' => 'fr', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); - $this->assertText($langcode, t('Language added successfully.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); - - // Set language negotiation. - $edit = array( - 'language[enabled][locale-url]' => TRUE, - ); - $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); - $this->assertText(t('Language negotiation configuration saved.'), t('Set language negotiation.')); - - // Check if the language selector is available on admin/people/create and - // set to the currently active language. - $this->drupalGet($langcode . '/admin/people/create'); - $this->assertFieldChecked("edit-language-$langcode", t('Global language set in the language selector.')); - - // Create a user with the admin/people/create form and check if the correct - // language is set. - $username = $this->randomName(10); - $edit = array( - 'name' => $username, - 'mail' => $this->randomName(4) . '@example.com', - 'pass[pass1]' => $username, - 'pass[pass2]' => $username, - ); - - $this->drupalPost($langcode . '/admin/people/create', $edit, t('Create new account')); - - $user = user_load_by_name($username); - $this->assertEqual($user->language, $langcode, t('New user has correct language set.')); - - // Register a new user and check if the language selector is hidden. - $this->drupalLogout(); - - $this->drupalGet($langcode . '/user/register'); - $this->assertNoFieldByName('language[fr]', t('Language selector is not accessible.')); - - $username = $this->randomName(10); - $edit = array( - 'name' => $username, - 'mail' => $this->randomName(4) . '@example.com', - ); - - $this->drupalPost($langcode . '/user/register', $edit, t('Create new account')); - - $user = user_load_by_name($username); - $this->assertEqual($user->language, $langcode, t('New user has correct language set.')); - - // Test if the admin can use the language selector and if the - // correct language is was saved. - $user_edit = $langcode . '/user/' . $user->uid . '/edit'; - - $this->drupalLogin($admin_user); - $this->drupalGet($user_edit); - $this->assertFieldChecked("edit-language-$langcode", t('Language selector is accessible and correct language is selected.')); - - // Set pass_raw so we can login the new user. - $user->pass_raw = $this->randomName(10); - $edit = array( - 'pass[pass1]' => $user->pass_raw, - 'pass[pass2]' => $user->pass_raw, - ); - - $this->drupalPost($user_edit, $edit, t('Save')); - - $this->drupalLogin($user); - $this->drupalGet($user_edit); - $this->assertFieldChecked("edit-language-$langcode", t('Language selector is accessible and correct language is selected.')); - } -} - -/** - * Functional tests for configuring a different path alias per language. - */ -class LocalePathFunctionalTest extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'Path language settings', - 'description' => 'Checks you can configure a language for individual url aliases.', - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp('locale', 'path'); - } - - /** - * Test if a language can be associated with a path alias. - */ - function testPathLanguageConfiguration() { - global $base_url; - - // User to add and remove language. - $admin_user = $this->drupalCreateUser(array('administer languages', 'create page content', 'administer url aliases', 'create url aliases', 'access administration pages')); - - // Add custom language. - $this->drupalLogin($admin_user); - // Code for the language. - $langcode = 'xx'; - // The English name for the language. - $name = $this->randomName(16); - // The native name for the language. - $native = $this->randomName(16); - // The domain prefix. - $prefix = $langcode; - $edit = array( - 'langcode' => $langcode, - 'name' => $name, - 'native' => $native, - 'prefix' => $prefix, - 'direction' => '0', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); - - // Check that the "xx" front page is not available when path prefixes are - // not enabled yet. - $this->drupalPost('admin/config/regional/language/configure', array(), t('Save settings')); - $this->drupalGet($prefix); - $this->assertResponse(404, t('The "xx" front page is not available yet.')); - - // Enable URL language detection and selection. - $edit = array('language[enabled][locale-url]' => 1); - $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); - - // Create a node. - $node = $this->drupalCreateNode(array('type' => 'page')); - - // Create a path alias in default language (English). - $path = 'admin/config/search/path/add'; - $english_path = $this->randomName(8); - $edit = array( - 'source' => 'node/' . $node->nid, - 'alias' => $english_path, - 'language' => 'en', - ); - $this->drupalPost($path, $edit, t('Save')); - - // Create a path alias in new custom language. - $custom_language_path = $this->randomName(8); - $edit = array( - 'source' => 'node/' . $node->nid, - 'alias' => $custom_language_path, - 'language' => $langcode, - ); - $this->drupalPost($path, $edit, t('Save')); - - // Confirm English language path alias works. - $this->drupalGet($english_path); - $this->assertText($node->title, t('English alias works.')); - - // Confirm custom language path alias works. - $this->drupalGet($prefix . '/' . $custom_language_path); - $this->assertText($node->title, t('Custom language alias works.')); - - // Create a custom path. - $custom_path = $this->randomName(8); - - // Check priority of language for alias by source path. - $edit = array( - 'source' => 'node/' . $node->nid, - 'alias' => $custom_path, - 'language' => LANGUAGE_NONE, - ); - path_save($edit); - $lookup_path = drupal_lookup_path('alias', 'node/' . $node->nid, 'en'); - $this->assertEqual($english_path, $lookup_path, t('English language alias has priority.')); - // Same check for language 'xx'. - $lookup_path = drupal_lookup_path('alias', 'node/' . $node->nid, $prefix); - $this->assertEqual($custom_language_path, $lookup_path, t('Custom language alias has priority.')); - path_delete($edit); - - // Create language nodes to check priority of aliases. - $first_node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); - $second_node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); - - // Assign a custom path alias to the first node with the English language. - $edit = array( - 'source' => 'node/' . $first_node->nid, - 'alias' => $custom_path, - 'language' => 'en', - ); - path_save($edit); - - // Assign a custom path alias to second node with LANGUAGE_NONE. - $edit = array( - 'source' => 'node/' . $second_node->nid, - 'alias' => $custom_path, - 'language' => LANGUAGE_NONE, - ); - path_save($edit); - - // Test that both node titles link to our path alias. - $this->drupalGet(''); - $custom_path_url = base_path() . (variable_get('clean_url', 0) ? $custom_path : '?q=' . $custom_path); - $elements = $this->xpath('//a[@href=:href and .=:title]', array(':href' => $custom_path_url, ':title' => $first_node->title)); - $this->assertTrue(!empty($elements), t('First node links to the path alias.')); - $elements = $this->xpath('//a[@href=:href and .=:title]', array(':href' => $custom_path_url, ':title' => $second_node->title)); - $this->assertTrue(!empty($elements), t('Second node links to the path alias.')); - - // Confirm that the custom path leads to the first node. - $this->drupalGet($custom_path); - $this->assertText($first_node->title, t('Custom alias returns first node.')); - - // Confirm that the custom path with prefix leads to the second node. - $this->drupalGet($prefix . '/' . $custom_path); - $this->assertText($second_node->title, t('Custom alias with prefix returns second node.')); - } -} - -/** - * Functional tests for multilingual support on nodes. - */ -class LocaleContentFunctionalTest extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'Content language settings', - 'description' => 'Checks you can enable multilingual support on content types and configure a language for a node.', - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp('locale'); - } - - /** - * Test if a content type can be set to multilingual and language setting is - * present on node add and edit forms. - */ - function testContentTypeLanguageConfiguration() { - global $base_url; - - // User to add and remove language. - $admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages')); - // User to create a node. - $web_user = $this->drupalCreateUser(array('create article content', 'create page content', 'edit any page content')); - - // Add custom language. - $this->drupalLogin($admin_user); - // Code for the language. - $langcode = 'xx'; - // The English name for the language. - $name = $this->randomName(16); - // The native name for the language. - $native = $this->randomName(16); - // The domain prefix. - $prefix = $langcode; - $edit = array( - 'langcode' => $langcode, - 'name' => $name, - 'native' => $native, - 'prefix' => $prefix, - 'direction' => '0', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); - - // Add disabled custom language. - // Code for the language. - $langcode_disabled = 'xx-yy'; - // The English name for the language. - $name_disabled = $this->randomName(16); - // The native name for the language. - $native_disabled = $this->randomName(16); - // The domain prefix. - $prefix_disabled = $langcode_disabled; - $edit = array( - 'langcode' => $langcode_disabled, - 'name' => $name_disabled, - 'native' => $native_disabled, - 'prefix' => $prefix_disabled, - 'direction' => '0', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); - // Disable second custom language. - $path = 'admin/config/regional/language'; - $edit = array( - 'enabled[' . $langcode_disabled . ']' => FALSE, - ); - $this->drupalPost($path, $edit, t('Save configuration')); - - // Set "Basic page" content type to use multilingual support. - $this->drupalGet('admin/structure/types/manage/page'); - $this->assertText(t('Multilingual support'), t('Multilingual support fieldset present on content type configuration form.')); - $edit = array( - 'language_content_type' => 1, - ); - $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type')); - $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), t('Basic page content type has been updated.')); - $this->drupalLogout(); - - // Verify language selection is not present on add article form. - $this->drupalLogin($web_user); - $this->drupalGet('node/add/article'); - // Verify language select list is not present. - $this->assertNoFieldByName('language', NULL, t('Language select not present on add article form.')); - - // Verify language selection appears on add "Basic page" form. - $this->drupalGet('node/add/page'); - // Verify language select list is present. - $this->assertFieldByName('language', NULL, t('Language select present on add Basic page form.')); - // Ensure enabled language appears. - $this->assertText($name, t('Enabled language present.')); - // Ensure disabled language doesn't appear. - $this->assertNoText($name_disabled, t('Disabled language not present.')); - - // Create "Basic page" content. - $node_title = $this->randomName(); - $node_body = $this->randomName(); - $edit = array( - 'type' => 'page', - 'title' => $node_title, - 'body' => array($langcode => array(array('value' => $node_body))), - 'language' => $langcode, - ); - $node = $this->drupalCreateNode($edit); - // Edit the content and ensure correct language is selected. - $path = 'node/' . $node->nid . '/edit'; - $this->drupalGet($path); - $this->assertRaw('', t('Correct language selected.')); - // Ensure we can change the node language. - $edit = array( - 'language' => 'en', - ); - $this->drupalPost($path, $edit, t('Save')); - $this->assertRaw(t('%title has been updated.', array('%title' => $node_title)), t('Basic page content updated.')); - - $this->drupalLogout(); - } -} - -/** - * Test UI language negotiation - * 1. URL (PATH) > DEFAULT - * UI Language base on URL prefix, browser language preference has no - * influence: - * admin/config - * UI in site default language - * zh-hans/admin/config - * UI in Chinese - * blah-blah/admin/config - * 404 - * 2. URL (PATH) > BROWSER > DEFAULT - * admin/config - * UI in user's browser language preference if the site has that - * language enabled, if not, the default language - * zh-hans/admin/config - * UI in Chinese - * blah-blah/admin/config - * 404 - * 3. URL (DOMAIN) > DEFAULT - * http://example.com/admin/config - * UI language in site default - * http://example.cn/admin/config - * UI language in Chinese - */ -class LocaleUILanguageNegotiationTest extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'UI language negotiation', - 'description' => 'Test UI language switching by url path prefix and domain.', - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp('locale', 'locale_test'); - require_once DRUPAL_ROOT . '/includes/language.inc'; - drupal_load('module', 'locale'); - $admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages', 'administer blocks')); - $this->drupalLogin($admin_user); - } - - /** - * Tests for language switching by URL path. - */ - function testUILanguageNegotiation() { - // A few languages to switch to. - // This one is unknown, should get the default lang version. - $language_unknown = 'blah-blah'; - // For testing browser lang preference. - $language_browser_fallback = 'vi'; - // For testing path prefix. - $language = 'zh-hans'; - // For setting browser language preference to 'vi'. - $http_header_browser_fallback = array("Accept-Language: $language_browser_fallback;q=1"); - // For setting browser language preference to some unknown. - $http_header_blah = array("Accept-Language: blah;q=1"); - - // This domain should switch the UI to Chinese. - $language_domain = 'example.cn'; - - // Setup the site languages by installing two languages. - require_once DRUPAL_ROOT . '/includes/locale.inc'; - locale_add_language($language_browser_fallback); - locale_add_language($language); - - // We will look for this string in the admin/config screen to see if the - // corresponding translated string is shown. - $default_string = 'Configure languages for content and the user interface'; - - // Set the default language in order for the translated string to be registered - // into database when seen by t(). Without doing this, our target string - // is for some reason not found when doing translate search. This might - // be some bug. - drupal_static_reset('language_list'); - $languages = language_list('enabled'); - variable_set('language_default', $languages[1]['vi']); - // First visit this page to make sure our target string is searchable. - $this->drupalGet('admin/config'); - // Now the t()'ed string is in db so switch the language back to default. - variable_del('language_default'); - - // Translate the string. - $language_browser_fallback_string = "In $language_browser_fallback In $language_browser_fallback In $language_browser_fallback"; - $language_string = "In $language In $language In $language"; - // Do a translate search of our target string. - $edit = array( 'string' => $default_string); - $this->drupalPost('admin/config/regional/translate/translate', $edit, t('Filter')); - // Should find the string and now click edit to post translated string. - $this->clickLink('edit'); - $edit = array( - "translations[$language_browser_fallback]" => $language_browser_fallback_string, - "translations[$language]" => $language_string, - ); - $this->drupalPost(NULL, $edit, t('Save translations')); - - // Configure URL language rewrite. - variable_set('locale_language_negotiation_url_type', LANGUAGE_TYPE_INTERFACE); - - $tests = array( - // Default, browser preference should have no influence. - array( - 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT), - 'path' => 'admin/config', - 'expect' => $default_string, - 'http_header' => $http_header_browser_fallback, - 'message' => 'URL (PATH) > DEFAULT: no language prefix, UI language is default and the browser language preference setting is not used.', - ), - // Language prefix. - array( - 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT), - 'path' => "$language/admin/config", - 'expect' => $language_string, - 'http_header' => $http_header_browser_fallback, - 'message' => 'URL (PATH) > DEFAULT: with language prefix, UI language is switched based on path prefix', - ), - // Default, go by browser preference. - array( - 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_BROWSER), - 'path' => 'admin/config', - 'expect' => $language_browser_fallback_string, - 'http_header' => $http_header_browser_fallback, - 'message' => 'URL (PATH) > BROWSER: no language prefix, UI language is determined by browser language preference', - ), - // Prefix, switch to the language. - array( - 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_BROWSER), - 'path' => "$language/admin/config", - 'expect' => $language_string, - 'http_header' => $http_header_browser_fallback, - 'message' => 'URL (PATH) > BROWSER: with langage prefix, UI language is based on path prefix', - ), - // Default, browser language preference is not one of site's lang. - array( - 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_BROWSER, LANGUAGE_NEGOTIATION_DEFAULT), - 'path' => 'admin/config', - 'expect' => $default_string, - 'http_header' => $http_header_blah, - 'message' => 'URL (PATH) > BROWSER > DEFAULT: no language prefix and browser language preference set to unknown language should use default language', - ), - ); - - foreach ($tests as $test) { - $this->runTest($test); - } - - // Unknown language prefix should return 404. - variable_set('language_negotiation_' . LANGUAGE_TYPE_INTERFACE, locale_language_negotiation_info()); - $this->drupalGet("$language_unknown/admin/config", array(), $http_header_browser_fallback); - $this->assertResponse(404, "Unknown language path prefix should return 404"); - - // Setup for domain negotiation, first configure the language to have domain - // URL. - $edit = array('prefix' => '', 'domain' => "http://$language_domain"); - $this->drupalPost("admin/config/regional/language/edit/$language", $edit, t('Save language')); - // Set the site to use domain language negotiation. - - $tests = array( - // Default domain, browser preference should have no influence. - array( - 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT), - 'locale_language_negotiation_url_part' => LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN, - 'path' => 'admin/config', - 'expect' => $default_string, - 'http_header' => $http_header_browser_fallback, - 'message' => 'URL (DOMAIN) > DEFAULT: default domain should get default language', - ), - // Language domain specific URL, we set the $_SERVER['HTTP_HOST'] in - // locale_test.module hook_boot() to simulate this. - array( - 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT), - 'locale_language_negotiation_url_part' => LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN, - 'locale_test_domain' => $language_domain, - 'path' => 'admin/config', - 'expect' => $language_string, - 'http_header' => $http_header_browser_fallback, - 'message' => 'URL (DOMAIN) > DEFAULT: domain example.cn should switch to Chinese', - ), - ); - - foreach ($tests as $test) { - $this->runTest($test); - } - } - - private function runTest($test) { - if (!empty($test['language_negotiation'])) { - $negotiation = array_flip($test['language_negotiation']); - language_negotiation_set(LANGUAGE_TYPE_INTERFACE, $negotiation); - } - if (!empty($test['locale_language_negotiation_url_part'])) { - variable_set('locale_language_negotiation_url_part', $test['locale_language_negotiation_url_part']); - } - if (!empty($test['locale_test_domain'])) { - variable_set('locale_test_domain', $test['locale_test_domain']); - } - $this->drupalGet($test['path'], array(), $test['http_header']); - $this->assertText($test['expect'], $test['message']); - } - - /** - * Test URL language detection when the requested URL has no language. - */ - function testUrlLanguageFallback() { - // Add the Italian language. - $language_browser_fallback = 'it'; - locale_add_language($language_browser_fallback); - $languages = language_list(); - - // Enable the path prefix for the default language: this way any unprefixed - // URL must have a valid fallback value. - $edit = array('prefix' => 'en'); - $this->drupalPost('admin/config/regional/language/edit/en', $edit, t('Save language')); - - // Enable browser and URL language detection. - $edit = array( - 'language[enabled][locale-browser]' => TRUE, - 'language[enabled][locale-url]' => TRUE, - 'language[weight][locale-browser]' => -8, - 'language[weight][locale-url]' => -10, - ); - $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); - $this->drupalGet('admin/config/regional/language/configure'); - - // Enable the language switcher block. - $edit = array('blocks[locale_language][region]' => 'sidebar_first'); - $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); - - // Access the front page without specifying any valid URL language prefix - // and having as browser language preference a non-default language. - $http_header = array("Accept-Language: $language_browser_fallback;q=1"); - $this->drupalGet('', array(), $http_header); - - // Check that the language switcher active link matches the given browser - // language. - $args = array(':url' => base_path() . (!empty($GLOBALS['conf']['clean_url']) ? $language_browser_fallback : "?q=$language_browser_fallback")); - $fields = $this->xpath('//div[@id="block-locale-language"]//a[@class="language-link active" and @href=:url]', $args); - $this->assertTrue($fields[0] == $languages[$language_browser_fallback]->native, t('The browser language is the URL active language')); - - // Check that URLs are rewritten using the given browser language. - $fields = $this->xpath('//div[@id="site-name"]//a[@rel="home" and @href=:url]//span', $args); - $this->assertTrue($fields[0] == 'Drupal', t('URLs are rewritten using the browser language.')); - } -} - -/** - * Test that URL rewriting works as expected. - */ -class LocaleUrlRewritingTest extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'URL rewriting', - 'description' => 'Test that URL rewriting works as expected.', - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp('locale'); - - // Create and login user. - $this->web_user = $this->drupalCreateUser(array('administer languages', 'access administration pages')); - $this->drupalLogin($this->web_user); - - // Install French language. - $edit = array(); - $edit['langcode'] = 'fr'; - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); - - // Install Italian language. - $edit = array(); - $edit['langcode'] = 'it'; - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); - - // Disable Italian language. - $edit = array('enabled[it]' => FALSE); - $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration')); - - // Enable URL language detection and selection. - $edit = array('language[enabled][locale-url]' => 1); - $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); - - // Reset static caching. - drupal_static_reset('language_list'); - drupal_static_reset('locale_url_outbound_alter'); - drupal_static_reset('locale_language_url_rewrite_url'); - } - - /** - * Check that disabled or non-installed languages are not considered. - */ - function testUrlRewritingEdgeCases() { - // Check URL rewriting with a disabled language. - $languages = language_list(); - $this->checkUrl($languages['it'], t('Path language is ignored if language is disabled.'), t('URL language negotiation does not work with disabled languages')); - - // Check URL rewriting with a non-installed language. - $non_existing = language_default(); - $non_existing->language = $this->randomName(); - $non_existing->prefix = $this->randomName(); - $this->checkUrl($non_existing, t('Path language is ignored if language is not installed.'), t('URL language negotiation does not work with non-installed languages')); - } - - /** - * Check URL rewriting for the given language. - * - * The test is performed with a fixed URL (the default front page) to simply - * check that language prefixes are not added to it and that the prefixed URL - * is actually not working. - */ - private function checkUrl($language, $message1, $message2) { - $options = array('language' => $language); - $base_path = trim(base_path(), '/'); - $rewritten_path = trim(str_replace(array('?q=', $base_path), '', url('node', $options)), '/'); - $segments = explode('/', $rewritten_path, 2); - $prefix = $segments[0]; - $path = isset($segments[1]) ? $segments[1] : $prefix; - // If the rewritten URL has not a language prefix we pick the right one from - // the language object so we can always check the prefixed URL. - if ($this->assertNotEqual($language->prefix, $prefix, $message1)) { - $prefix = $language->prefix; - } - $this->drupalGet("$prefix/$path"); - $this->assertResponse(404, $message2); - } -} - -/** - * Functional test for multilingual fields. - */ -class LocaleMultilingualFieldsFunctionalTest extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'Multilingual fields', - 'description' => 'Test multilingual support for fields.', - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp('locale'); - // Setup users. - $admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages', 'create page content', 'edit own page content')); - $this->drupalLogin($admin_user); - - // Add a new language. - require_once DRUPAL_ROOT . '/includes/locale.inc'; - locale_add_language('it', 'Italian', 'Italiano', LANGUAGE_LTR, '', '', TRUE, FALSE); - - // Enable URL language detection and selection. - $edit = array('language[enabled][locale-url]' => '1'); - $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); - - // Set "Basic page" content type to use multilingual support. - $edit = array( - 'language_content_type' => 1, - ); - $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type')); - $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), t('Basic page content type has been updated.')); - } - - /** - * Test if field languages are correctly set through the node form. - */ - function testMultilingualNodeForm() { - // Create "Basic page" content. - $langcode = LANGUAGE_NONE; - $title_key = "title"; - $title_value = $this->randomName(8); - $body_key = "body[$langcode][0][value]"; - $body_value = $this->randomName(16); - - // Create node to edit. - $edit = array(); - $edit[$title_key] = $title_value; - $edit[$body_key] = $body_value; - $edit['language'] = 'en'; - $this->drupalPost('node/add/page', $edit, t('Save')); - - // Check that the node exists in the database. - $node = $this->drupalGetNodeByTitle($edit[$title_key]); - $this->assertTrue($node, t('Node found in database.')); - - $assert = isset($node->body['en']) && !isset($node->body[LANGUAGE_NONE]) && $node->body['en'][0]['value'] == $body_value; - $this->assertTrue($assert, t('Field language correctly set.')); - - // Change node language. - $this->drupalGet("node/$node->nid/edit"); - $edit = array( - $title_key => $this->randomName(8), - 'language' => 'it' - ); - $this->drupalPost(NULL, $edit, t('Save')); - $node = $this->drupalGetNodeByTitle($edit[$title_key]); - $this->assertTrue($node, t('Node found in database.')); - - $assert = isset($node->body['it']) && !isset($node->body['en']) && $node->body['it'][0]['value'] == $body_value; - $this->assertTrue($assert, t('Field language correctly changed.')); - - // Enable content language URL detection. - language_negotiation_set(LANGUAGE_TYPE_CONTENT, array(LOCALE_LANGUAGE_NEGOTIATION_URL => 0)); - - // Test multilingual field language fallback logic. - $this->drupalGet("it/node/$node->nid"); - $this->assertRaw($body_value, t('Body correctly displayed using Italian as requested language')); - - $this->drupalGet("node/$node->nid"); - $this->assertRaw($body_value, t('Body correctly displayed using English as requested language')); - } - - /* - * Test multilingual field display settings. - */ - function testMultilingualDisplaySettings() { - // Create "Basic page" content. - $langcode = LANGUAGE_NONE; - $title_key = "title"; - $title_value = $this->randomName(8); - $body_key = "body[$langcode][0][value]"; - $body_value = $this->randomName(16); - - // Create node to edit. - $edit = array(); - $edit[$title_key] = $title_value; - $edit[$body_key] = $body_value; - $edit['language'] = 'en'; - $this->drupalPost('node/add/page', $edit, t('Save')); - - // Check that the node exists in the database. - $node = $this->drupalGetNodeByTitle($edit[$title_key]); - $this->assertTrue($node, t('Node found in database.')); - - // Check if node body is showed. - $this->drupalGet("node/$node->nid"); - $body = $this->xpath('//div[@id=:id]//div[@property="content:encoded"]/p', array(':id' => 'node-' . $node->nid)); - $this->assertEqual(current($body), $node->body['en'][0]['value'], 'Node body is correctly showed.'); - } -} - -/** - * Functional tests for comment language. - */ -class LocaleCommentLanguageFunctionalTest extends DrupalWebTestCase { - - public static function getInfo() { - return array( - 'name' => 'Comment language', - 'description' => 'Tests for comment language.', - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp('locale', 'locale_test'); - - // Create and login user. - $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'administer content types', 'create article content')); - $this->drupalLogin($admin_user); - - // Add language. - $edit = array('langcode' => 'fr'); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); - - // Set "Article" content type to use multilingual support. - $edit = array('language_content_type' => 1); - $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type')); - - // Enable content language negotiation UI. - variable_set('locale_test_content_language_type', TRUE); - - // Set interface language detection to user and content language detection - // to URL. Disable inheritance from interface language to ensure content - // language will fall back to the default language if no URL language can be - // detected. - $edit = array( - 'language[enabled][locale-user]' => TRUE, - 'language_content[enabled][locale-url]' => TRUE, - 'language_content[enabled][locale-interface]' => FALSE, - ); - $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); - - // Change user language preference, this way interface language is always - // French no matter what path prefix the URLs have. - $edit = array('language' => 'fr'); - $this->drupalPost("user/{$admin_user->uid}/edit", $edit, t('Save')); - } - - /** - * Test that comment language is properly set. - */ - function testCommentLanguage() { - drupal_static_reset('language_list'); - - // Create two nodes, one for english and one for french, and comment each - // node using both english and french as content language by changing URL - // language prefixes. Meanwhile interface language is always French, which - // is the user language preference. This way we can ensure that node - // language and interface language do not influence comment language, as - // only content language has to. - foreach (language_list() as $node_langcode => $node_language) { - $language_none = LANGUAGE_NONE; - - // Create "Article" content. - $title = $this->randomName(); - $edit = array( - "title" => $title, - "body[$language_none][0][value]" => $this->randomName(), - "language" => $node_langcode, - ); - $this->drupalPost("node/add/article", $edit, t('Save')); - $node = $this->drupalGetNodeByTitle($title); - - foreach (language_list() as $langcode => $language) { - // Post a comment with content language $langcode. - $prefix = empty($language->prefix) ? '' : $language->prefix . '/'; - $edit = array("comment_body[$language_none][0][value]" => $this->randomName()); - $this->drupalPost("{$prefix}node/{$node->nid}", $edit, t('Save')); - - // Check that comment language matches the current content language. - $comment = db_select('comment', 'c') - ->fields('c') - ->condition('nid', $node->nid) - ->orderBy('cid', 'DESC') - ->execute() - ->fetchObject(); - $args = array('%node_language' => $node_langcode, '%comment_language' => $comment->language, '%langcode' => $langcode); - $this->assertEqual($comment->language, $langcode, t('The comment posted with content language %langcode and belonging to the node with language %node_language has language %comment_language', $args)); - } - } - } -} -/** - * Functional tests for localizing date formats. - */ -class LocaleDateFormatsFunctionalTest extends DrupalWebTestCase { - - public static function getInfo() { - return array( - 'name' => 'Localize date formats', - 'description' => 'Tests for the localization of date formats.', - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp('locale'); - - // Create and login user. - $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'create article content')); - $this->drupalLogin($admin_user); - } - - /** - * Functional tests for localizing date formats. - */ - function testLocalizeDateFormats() { - // Add language. - $edit = array( - 'langcode' => 'fr', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); - - // Set language negotiation. - $language_type = LANGUAGE_TYPE_INTERFACE; - $edit = array( - "{$language_type}[enabled][locale-url]" => TRUE, - ); - $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); - - // Configure date formats. - $this->drupalGet('admin/config/regional/date-time/locale'); - $this->assertText('Français', 'Configured languages appear.'); - $edit = array( - 'date_format_long' => 'd.m.Y - H:i', - 'date_format_medium' => 'd.m.Y - H:i', - 'date_format_short' => 'd.m.Y - H:i', - ); - $this->drupalPost('admin/config/regional/date-time/locale/fr/edit', $edit, t('Save configuration')); - $this->assertText(t('Configuration saved.'), 'French date formats updated.'); - $edit = array( - 'date_format_long' => 'j M Y - g:ia', - 'date_format_medium' => 'j M Y - g:ia', - 'date_format_short' => 'j M Y - g:ia', - ); - $this->drupalPost('admin/config/regional/date-time/locale/en/edit', $edit, t('Save configuration')); - $this->assertText(t('Configuration saved.'), 'English date formats updated.'); - - // Create node content. - $node = $this->drupalCreateNode(array('type' => 'article')); - - // Configure format for the node posted date changes with the language. - $this->drupalGet('node/' . $node->nid); - $english_date = format_date($node->created, 'custom', 'j M Y'); - $this->assertText($english_date, t('English date format appears')); - $this->drupalGet('fr/node/' . $node->nid); - $french_date = format_date($node->created, 'custom', 'd.m.Y'); - $this->assertText($french_date, t('French date format appears')); - } -} - -/** - * Functional test for language types/negotiation info. - */ -class LocaleLanguageNegotiationInfoFunctionalTest extends DrupalWebTestCase { - - public static function getInfo() { - return array( - 'name' => 'Language negotiation info', - 'description' => 'Tests alterations to language types/negotiation info.', - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp('locale'); - require_once DRUPAL_ROOT .'/includes/language.inc'; - $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'view the administration theme')); - $this->drupalLogin($admin_user); - $this->drupalPost('admin/config/regional/language/add', array('langcode' => 'it'), t('Add language')); - } - - /** - * Tests alterations to language types/negotiation info. - */ - function testInfoAlterations() { - // Enable language type/negotiation info alterations. - variable_set('locale_test_language_types', TRUE); - variable_set('locale_test_language_negotiation_info', TRUE); - $this->languageNegotiationUpdate(); - - // Check that fixed language types are properly configured without the need - // of saving the language negotiation settings. - $this->checkFixedLanguageTypes(); - - // Make the content language type configurable by updating the language - // negotiation settings with the proper flag enabled. - variable_set('locale_test_content_language_type', TRUE); - $this->languageNegotiationUpdate(); - $type = LANGUAGE_TYPE_CONTENT; - $language_types = variable_get('language_types', drupal_language_types()); - $this->assertTrue($language_types[$type], t('Content language type is configurable.')); - - // Enable some core and custom language providers. The test language type is - // supposed to be configurable. - $test_type = 'test_language_type'; - $provider = LOCALE_LANGUAGE_NEGOTIATION_INTERFACE; - $test_provider = 'test_language_provider'; - $form_field = $type . '[enabled]['. $provider .']'; - $edit = array( - $form_field => TRUE, - $type . '[enabled][' . $test_provider . ']' => TRUE, - $test_type . '[enabled][' . $test_provider . ']' => TRUE, - ); - $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); - - // Remove the interface language provider by updating the language - // negotiation settings with the proper flag enabled. - variable_set('locale_test_language_negotiation_info_alter', TRUE); - $this->languageNegotiationUpdate(); - $negotiation = variable_get("language_negotiation_$type", array()); - $this->assertFalse(isset($negotiation[$provider]), t('Interface language provider removed from the stored settings.')); - $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, t('Interface language provider unavailable.')); - - // Check that type-specific language providers can be assigned only to the - // corresponding language types. - foreach (language_types_configurable() as $type) { - $form_field = $type . '[enabled][test_language_provider_ts]'; - if ($type == $test_type) { - $this->assertFieldByXPath("//input[@name=\"$form_field\"]", NULL, t('Type-specific test language provider available for %type.', array('%type' => $type))); - } - else { - $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, t('Type-specific test language provider unavailable for %type.', array('%type' => $type))); - } - } - - // Check language negotiation results. - $this->drupalGet(''); - $last = variable_get('locale_test_language_negotiation_last', array()); - foreach (language_types() as $type) { - $langcode = $last[$type]; - $value = $type == LANGUAGE_TYPE_CONTENT || strpos($type, 'test') !== FALSE ? 'it' : 'en'; - $this->assertEqual($langcode, $value, t('The negotiated language for %type is %language', array('%type' => $type, '%language' => $langcode))); - } - - // Disable locale_test and check that everything is set back to the original - // status. - $this->languageNegotiationUpdate('disable'); - - // Check that only the core language types are available. - foreach (language_types() as $type) { - $this->assertTrue(strpos($type, 'test') === FALSE, t('The %type language is still available', array('%type' => $type))); - } - - // Check that fixed language types are properly configured, even those - // previously set to configurable. - $this->checkFixedLanguageTypes(); - - // Check that unavailable language providers are not present in the - // negotiation settings. - $negotiation = variable_get("language_negotiation_$type", array()); - $this->assertFalse(isset($negotiation[$test_provider]), t('The disabled test language provider is not part of the content language negotiation settings.')); - - // Check that configuration page presents the correct options and settings. - $this->assertNoRaw(t('Test language detection'), t('No test language type configuration available.')); - $this->assertNoRaw(t('This is a test language provider'), t('No test language provider available.')); - } - - /** - * Update language types/negotiation information. - * - * Manually invoke locale_modules_enabled()/locale_modules_disabled() since - * they would not be invoked after enabling/disabling locale_test the first - * time. - */ - private function languageNegotiationUpdate($op = 'enable') { - static $last_op = NULL; - $modules = array('locale_test'); - - // Enable/disable locale_test only if we did not already before. - if ($last_op != $op) { - $function = "module_{$op}"; - $function($modules); - // Reset hook implementation cache. - module_implements(NULL, FALSE, TRUE); - } - - drupal_static_reset('language_types_info'); - drupal_static_reset('language_negotiation_info'); - $function = "locale_modules_{$op}d"; - if (function_exists($function)) { - $function($modules); - } - - $this->drupalGet('admin/config/regional/language/configure'); - } - - /** - * Check that language negotiation for fixed types matches the stored one. - */ - private function checkFixedLanguageTypes() { - drupal_static_reset('language_types_info'); - foreach (language_types_info() as $type => $info) { - if (isset($info['fixed'])) { - $negotiation = variable_get("language_negotiation_$type", array()); - $equal = count($info['fixed']) == count($negotiation); - while ($equal && list($id) = each($negotiation)) { - list(, $info_id) = each($info['fixed']); - $equal = $info_id == $id; - } - $this->assertTrue($equal, t('language negotiation for %type is properly set up', array('%type' => $type))); - } - } - } -} diff --git a/modules/node/node.admin.inc b/modules/node/node.admin.inc deleted file mode 100644 index a6ea1b5..0000000 --- a/modules/node/node.admin.inc +++ /dev/null @@ -1,600 +0,0 @@ - array( - 'label' => t('Publish selected content'), - 'callback' => 'node_mass_update', - 'callback arguments' => array('updates' => array('status' => NODE_PUBLISHED)), - ), - 'unpublish' => array( - 'label' => t('Unpublish selected content'), - 'callback' => 'node_mass_update', - 'callback arguments' => array('updates' => array('status' => NODE_NOT_PUBLISHED)), - ), - 'promote' => array( - 'label' => t('Promote selected content to front page'), - 'callback' => 'node_mass_update', - 'callback arguments' => array('updates' => array('status' => NODE_PUBLISHED, 'promote' => NODE_PROMOTED)), - ), - 'demote' => array( - 'label' => t('Demote selected content from front page'), - 'callback' => 'node_mass_update', - 'callback arguments' => array('updates' => array('promote' => NODE_NOT_PROMOTED)), - ), - 'sticky' => array( - 'label' => t('Make selected content sticky'), - 'callback' => 'node_mass_update', - 'callback arguments' => array('updates' => array('status' => NODE_PUBLISHED, 'sticky' => NODE_STICKY)), - ), - 'unsticky' => array( - 'label' => t('Make selected content not sticky'), - 'callback' => 'node_mass_update', - 'callback arguments' => array('updates' => array('sticky' => NODE_NOT_STICKY)), - ), - 'delete' => array( - 'label' => t('Delete selected content'), - 'callback' => NULL, - ), - ); - return $operations; -} - -/** - * List node administration filters that can be applied. - */ -function node_filters() { - // Regular filters - $filters['status'] = array( - 'title' => t('status'), - 'options' => array( - '[any]' => t('any'), - 'status-1' => t('published'), - 'status-0' => t('not published'), - 'promote-1' => t('promoted'), - 'promote-0' => t('not promoted'), - 'sticky-1' => t('sticky'), - 'sticky-0' => t('not sticky'), - ), - ); - // Include translation states if we have this module enabled - if (module_exists('translation')) { - $filters['status']['options'] += array( - 'translate-0' => t('Up to date translation'), - 'translate-1' => t('Outdated translation'), - ); - } - - $filters['type'] = array( - 'title' => t('type'), - 'options' => array( - '[any]' => t('any'), - ) + node_type_get_names(), - ); - - // Language filter if there is a list of languages - if ($languages = module_invoke('locale', 'language_list')) { - $languages = array(LANGUAGE_NONE => t('Language neutral')) + $languages; - $filters['language'] = array( - 'title' => t('language'), - 'options' => array( - '[any]' => t('any'), - ) + $languages, - ); - } - return $filters; -} - -/** - * Apply filters for node administration filters based on session. - * - * @param $query - * A SelectQuery to which the filters should be applied. - */ -function node_build_filter_query(SelectQueryInterface $query) { - // Build query - $filter_data = isset($_SESSION['node_overview_filter']) ? $_SESSION['node_overview_filter'] : array(); - foreach ($filter_data as $index => $filter) { - list($key, $value) = $filter; - switch ($key) { - case 'term': - $alias = $query->join('taxonomy_index', 'ti', "n.nid = %alias.nid"); - $query->condition($alias . '.tid', $value); - break; - case 'status': - // Note: no exploitable hole as $key/$value have already been checked when submitted - list($key, $value) = explode('-', $value, 2); - case 'type': - case 'language': - $query->condition('n.' . $key, $value); - break; - } - } -} - -/** - * Return form for node administration filters. - */ -function node_filter_form() { - $session = isset($_SESSION['node_overview_filter']) ? $_SESSION['node_overview_filter'] : array(); - $filters = node_filters(); - - $i = 0; - $form['filters'] = array( - '#type' => 'fieldset', - '#title' => t('Show only items where'), - '#theme' => 'exposed_filters__node', - ); - foreach ($session as $filter) { - list($type, $value) = $filter; - if ($type == 'term') { - // Load term name from DB rather than search and parse options array. - $value = module_invoke('taxonomy', 'term_load', $value); - $value = $value->name; - } - elseif ($type == 'language') { - $value = $value == LANGUAGE_NONE ? t('Language neutral') : module_invoke('locale', 'language_name', $value); - } - else { - $value = $filters[$type]['options'][$value]; - } - $t_args = array('%property' => $filters[$type]['title'], '%value' => $value); - if ($i++) { - $form['filters']['current'][] = array('#markup' => t('and where %property is %value', $t_args)); - } - else { - $form['filters']['current'][] = array('#markup' => t('where %property is %value', $t_args)); - } - if (in_array($type, array('type', 'language'))) { - // Remove the option if it is already being filtered on. - unset($filters[$type]); - } - } - - $form['filters']['status'] = array( - '#type' => 'container', - '#attributes' => array('class' => array('clearfix')), - '#prefix' => ($i ? '
    ' . t('and where') . '
    ' : ''), - ); - $form['filters']['status']['filters'] = array( - '#type' => 'container', - '#attributes' => array('class' => array('filters')), - ); - foreach ($filters as $key => $filter) { - $form['filters']['status']['filters'][$key] = array( - '#type' => 'select', - '#options' => $filter['options'], - '#title' => $filter['title'], - '#default_value' => '[any]', - ); - } - - $form['filters']['status']['actions'] = array( - '#type' => 'actions', - '#attributes' => array('class' => array('container-inline')), - ); - $form['filters']['status']['actions']['submit'] = array( - '#type' => 'submit', - '#value' => count($session) ? t('Refine') : t('Filter'), - ); - if (count($session)) { - $form['filters']['status']['actions']['undo'] = array('#type' => 'submit', '#value' => t('Undo')); - $form['filters']['status']['actions']['reset'] = array('#type' => 'submit', '#value' => t('Reset')); - } - - drupal_add_js('misc/form.js'); - - return $form; -} - -/** - * Process result from node administration filter form. - */ -function node_filter_form_submit($form, &$form_state) { - $filters = node_filters(); - switch ($form_state['values']['op']) { - case t('Filter'): - case t('Refine'): - // Apply every filter that has a choice selected other than 'any'. - foreach ($filters as $filter => $options) { - if (isset($form_state['values'][$filter]) && $form_state['values'][$filter] != '[any]') { - // Flatten the options array to accommodate hierarchical/nested options. - $flat_options = form_options_flatten($filters[$filter]['options']); - // Only accept valid selections offered on the dropdown, block bad input. - if (isset($flat_options[$form_state['values'][$filter]])) { - $_SESSION['node_overview_filter'][] = array($filter, $form_state['values'][$filter]); - } - } - } - break; - case t('Undo'): - array_pop($_SESSION['node_overview_filter']); - break; - case t('Reset'): - $_SESSION['node_overview_filter'] = array(); - break; - } -} - -/** - * Make mass update of nodes, changing all nodes in the $nodes array - * to update them with the field values in $updates. - * - * IMPORTANT NOTE: This function is intended to work when called - * from a form submit handler. Calling it outside of the form submission - * process may not work correctly. - * - * @param array $nodes - * Array of node nids to update. - * @param array $updates - * Array of key/value pairs with node field names and the - * value to update that field to. - */ -function node_mass_update($nodes, $updates) { - // We use batch processing to prevent timeout when updating a large number - // of nodes. - if (count($nodes) > 10) { - $batch = array( - 'operations' => array( - array('_node_mass_update_batch_process', array($nodes, $updates)) - ), - 'finished' => '_node_mass_update_batch_finished', - 'title' => t('Processing'), - // We use a single multi-pass operation, so the default - // 'Remaining x of y operations' message will be confusing here. - 'progress_message' => '', - 'error_message' => t('The update has encountered an error.'), - // The operations do not live in the .module file, so we need to - // tell the batch engine which file to load before calling them. - 'file' => drupal_get_path('module', 'node') . '/node.admin.inc', - ); - batch_set($batch); - } - else { - foreach ($nodes as $nid) { - _node_mass_update_helper($nid, $updates); - } - drupal_set_message(t('The update has been performed.')); - } -} - -/** - * Node Mass Update - helper function. - */ -function _node_mass_update_helper($nid, $updates) { - $node = node_load($nid, NULL, TRUE); - // For efficiency manually save the original node before applying any changes. - $node->original = clone $node; - foreach ($updates as $name => $value) { - $node->$name = $value; - } - node_save($node); - return $node; -} - -/** - * Node Mass Update Batch operation - */ -function _node_mass_update_batch_process($nodes, $updates, &$context) { - if (!isset($context['sandbox']['progress'])) { - $context['sandbox']['progress'] = 0; - $context['sandbox']['max'] = count($nodes); - $context['sandbox']['nodes'] = $nodes; - } - - // Process nodes by groups of 5. - $count = min(5, count($context['sandbox']['nodes'])); - for ($i = 1; $i <= $count; $i++) { - // For each nid, load the node, reset the values, and save it. - $nid = array_shift($context['sandbox']['nodes']); - $node = _node_mass_update_helper($nid, $updates); - - // Store result for post-processing in the finished callback. - $context['results'][] = l($node->title, 'node/' . $node->nid); - - // Update our progress information. - $context['sandbox']['progress']++; - } - - // Inform the batch engine that we are not finished, - // and provide an estimation of the completion level we reached. - if ($context['sandbox']['progress'] != $context['sandbox']['max']) { - $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; - } -} - -/** - * Node Mass Update Batch 'finished' callback. - */ -function _node_mass_update_batch_finished($success, $results, $operations) { - if ($success) { - drupal_set_message(t('The update has been performed.')); - } - else { - drupal_set_message(t('An error occurred and processing did not complete.'), 'error'); - $message = format_plural(count($results), '1 item successfully processed:', '@count items successfully processed:'); - $message .= theme('item_list', array('items' => $results)); - drupal_set_message($message); - } -} - -/** - * Menu callback: content administration. - */ -function node_admin_content($form, $form_state) { - if (isset($form_state['values']['operation']) && $form_state['values']['operation'] == 'delete') { - return node_multiple_delete_confirm($form, $form_state, array_filter($form_state['values']['nodes'])); - } - $form['filter'] = node_filter_form(); - $form['#submit'][] = 'node_filter_form_submit'; - $form['admin'] = node_admin_nodes(); - - return $form; -} - -/** - * Form builder: Builds the node administration overview. - */ -function node_admin_nodes() { - $admin_access = user_access('administer nodes'); - - // Build the 'Update options' form. - $form['options'] = array( - '#type' => 'fieldset', - '#title' => t('Update options'), - '#attributes' => array('class' => array('container-inline')), - '#access' => $admin_access, - ); - $options = array(); - foreach (module_invoke_all('node_operations') as $operation => $array) { - $options[$operation] = $array['label']; - } - $form['options']['operation'] = array( - '#type' => 'select', - '#title' => t('Operation'), - '#title_display' => 'invisible', - '#options' => $options, - '#default_value' => 'approve', - ); - $form['options']['submit'] = array( - '#type' => 'submit', - '#value' => t('Update'), - '#validate' => array('node_admin_nodes_validate'), - '#submit' => array('node_admin_nodes_submit'), - ); - - // Enable language column if translation module is enabled or if we have any - // node with language. - $multilanguage = (module_exists('translation') || db_query_range("SELECT 1 FROM {node} WHERE language <> :language", 0, 1, array(':language' => LANGUAGE_NONE))->fetchField()); - - // Build the sortable table header. - $header = array( - 'title' => array('data' => t('Title'), 'field' => 'n.title'), - 'type' => array('data' => t('Type'), 'field' => 'n.type'), - 'author' => t('Author'), - 'status' => array('data' => t('Status'), 'field' => 'n.status'), - 'changed' => array('data' => t('Updated'), 'field' => 'n.changed', 'sort' => 'desc') - ); - if ($multilanguage) { - $header['language'] = array('data' => t('Language'), 'field' => 'n.language'); - } - $header['operations'] = array('data' => t('Operations')); - - $query = db_select('node', 'n')->extend('PagerDefault')->extend('TableSort'); - node_build_filter_query($query); - - if (!user_access('bypass node access')) { - // If the user is able to view their own unpublished nodes, allow them - // to see these in addition to published nodes. Check that they actually - // have some unpublished nodes to view before adding the condition. - if (user_access('view own unpublished content') && $own_unpublished = db_query('SELECT nid FROM {node} WHERE uid = :uid AND status = :status', array(':uid' => $GLOBALS['user']->uid, ':status' => 0))->fetchCol()) { - $query->condition(db_or() - ->condition('n.status', 1) - ->condition('n.nid', $own_unpublished, 'IN') - ); - } - else { - // If not, restrict the query to published nodes. - $query->condition('n.status', 1); - } - } - $nids = $query - ->fields('n',array('nid')) - ->limit(50) - ->orderByHeader($header) - ->execute() - ->fetchCol(); - $nodes = node_load_multiple($nids); - - // Prepare the list of nodes. - $languages = language_list(); - $destination = drupal_get_destination(); - $options = array(); - foreach ($nodes as $node) { - $l_options = $node->language != LANGUAGE_NONE && isset($languages[$node->language]) ? array('language' => $languages[$node->language]) : array(); - $options[$node->nid] = array( - 'title' => array( - 'data' => array( - '#type' => 'link', - '#title' => $node->title, - '#href' => 'node/' . $node->nid, - '#options' => $l_options, - '#suffix' => ' ' . theme('mark', array('type' => node_mark($node->nid, $node->changed))), - ), - ), - 'type' => check_plain(node_type_get_name($node)), - 'author' => theme('username', array('account' => $node)), - 'status' => $node->status ? t('published') : t('not published'), - 'changed' => format_date($node->changed, 'short'), - ); - if ($multilanguage) { - if ($node->language == LANGUAGE_NONE || isset($languages[$node->language])) { - $options[$node->nid]['language'] = $node->language == LANGUAGE_NONE ? t('Language neutral') : t($languages[$node->language]->name); - } - else { - $options[$node->nid]['language'] = t('Undefined language (@langcode)', array('@langcode' => $node->language)); - } - } - // Build a list of all the accessible operations for the current node. - $operations = array(); - if (node_access('update', $node)) { - $operations['edit'] = array( - 'title' => t('edit'), - 'href' => 'node/' . $node->nid . '/edit', - 'query' => $destination, - ); - } - if (node_access('delete', $node)) { - $operations['delete'] = array( - 'title' => t('delete'), - 'href' => 'node/' . $node->nid . '/delete', - 'query' => $destination, - ); - } - $options[$node->nid]['operations'] = array(); - if (count($operations) > 1) { - // Render an unordered list of operations links. - $options[$node->nid]['operations'] = array( - 'data' => array( - '#theme' => 'links__node_operations', - '#links' => $operations, - '#attributes' => array('class' => array('links', 'inline')), - ), - ); - } - elseif (!empty($operations)) { - // Render the first and only operation as a link. - $link = reset($operations); - $options[$node->nid]['operations'] = array( - 'data' => array( - '#type' => 'link', - '#title' => $link['title'], - '#href' => $link['href'], - '#options' => array('query' => $link['query']), - ), - ); - } - } - - // Only use a tableselect when the current user is able to perform any - // operations. - if ($admin_access) { - $form['nodes'] = array( - '#type' => 'tableselect', - '#header' => $header, - '#options' => $options, - '#empty' => t('No content available.'), - ); - } - // Otherwise, use a simple table. - else { - $form['nodes'] = array( - '#theme' => 'table', - '#header' => $header, - '#rows' => $options, - '#empty' => t('No content available.'), - ); - } - - $form['pager'] = array('#markup' => theme('pager')); - return $form; -} - -/** - * Validate node_admin_nodes form submissions. - * - * Check if any nodes have been selected to perform the chosen - * 'Update option' on. - */ -function node_admin_nodes_validate($form, &$form_state) { - // Error if there are no items to select. - if (!is_array($form_state['values']['nodes']) || !count(array_filter($form_state['values']['nodes']))) { - form_set_error('', t('No items selected.')); - } -} - -/** - * Process node_admin_nodes form submissions. - * - * Execute the chosen 'Update option' on the selected nodes. - */ -function node_admin_nodes_submit($form, &$form_state) { - $operations = module_invoke_all('node_operations'); - $operation = $operations[$form_state['values']['operation']]; - // Filter out unchecked nodes - $nodes = array_filter($form_state['values']['nodes']); - if ($function = $operation['callback']) { - // Add in callback arguments if present. - if (isset($operation['callback arguments'])) { - $args = array_merge(array($nodes), $operation['callback arguments']); - } - else { - $args = array($nodes); - } - call_user_func_array($function, $args); - - cache_clear_all(); - } - else { - // We need to rebuild the form to go to a second step. For example, to - // show the confirmation form for the deletion of nodes. - $form_state['rebuild'] = TRUE; - } -} - -function node_multiple_delete_confirm($form, &$form_state, $nodes) { - $form['nodes'] = array('#prefix' => '
      ', '#suffix' => '
    ', '#tree' => TRUE); - // array_filter returns only elements with TRUE values - foreach ($nodes as $nid => $value) { - $title = db_query('SELECT title FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchField(); - $form['nodes'][$nid] = array( - '#type' => 'hidden', - '#value' => $nid, - '#prefix' => '
  • ', - '#suffix' => check_plain($title) . "
  • \n", - ); - } - $form['operation'] = array('#type' => 'hidden', '#value' => 'delete'); - $form['#submit'][] = 'node_multiple_delete_confirm_submit'; - $confirm_question = format_plural(count($nodes), - 'Are you sure you want to delete this item?', - 'Are you sure you want to delete these items?'); - return confirm_form($form, - $confirm_question, - 'admin/content', t('This action cannot be undone.'), - t('Delete'), t('Cancel')); -} - -function node_multiple_delete_confirm_submit($form, &$form_state) { - if ($form_state['values']['confirm']) { - node_delete_multiple(array_keys($form_state['values']['nodes'])); - $count = count($form_state['values']['nodes']); - watchdog('content', 'Deleted @count posts.', array('@count' => $count)); - drupal_set_message(format_plural($count, 'Deleted 1 post.', 'Deleted @count posts.')); - } - $form_state['redirect'] = 'admin/content'; -} diff --git a/modules/profile/profile.test b/modules/profile/profile.test deleted file mode 100644 index 83bed25..0000000 --- a/modules/profile/profile.test +++ /dev/null @@ -1,490 +0,0 @@ -admin_user = $this->drupalCreateUser(array('administer users', 'access user profiles', 'administer blocks')); - - // This is the user whose profile will be edited. - $this->normal_user = $this->drupalCreateUser(); - } - - /** - * Create a profile field. - * - * @param $type - * The field type to be created. - * @param $category - * The category in which the field should be created. - * @param $edit - * Additional parameters to be submitted. - * @return - * The fid of the field that was just created. - */ - function createProfileField($type = 'textfield', $category = 'simpletest', $edit = array()) { - $edit['title'] = $title = $this->randomName(8); - $edit['name'] = $form_name = 'profile_' . $title; - $edit['category'] = $category; - $edit['explanation'] = $this->randomName(50); - - $this->drupalPost('admin/config/people/profile/add/' . $type, $edit, t('Save field')); - $fid = db_query("SELECT fid FROM {profile_field} WHERE title = :title", array(':title' => $title))->fetchField(); - $this->assertTrue($fid, t('New Profile field has been entered in the database')); - - // Check that the new field is appearing on the user edit form. - $this->drupalGet('user/' . $this->admin_user->uid . '/edit/' . $category); - - // Checking field. - if ($type == 'date') { - $this->assertField($form_name . '[month]', t('Found month selection field')); - $this->assertField($form_name . '[day]', t('Found day selection field')); - $this->assertField($form_name . '[year]', t('Found day selection field')); - } - else { - $this->assertField($form_name , t('Found form named @name', array('@name' => $form_name))); - } - - // Checking name. - $this->assertText($title, t('Checking title for field %title', array('%title' => $title))); - // Checking explanation. - $this->assertText($edit['explanation'], t('Checking explanation for field %title', array('%title' => $title))); - - return array( - 'fid' => $fid, - 'type' => $type, - 'form_name' => $form_name, - 'title' => $title, - 'category' => $category, - ); - } - - /** - * Update a profile field. - * - * @param $fid - * The fid of the field to be updated. - * @param $type - * The type of field to be updated. - * @param $edit - * Field parameters to be submitted. - * @return - * Array representation of the updated field. - */ - function updateProfileField($fid, $type = 'textfield', $edit = array()) { - - $form_name = $edit['name']; - $title = $edit['title']; - $category = $edit['category']; - - $this->drupalPost('admin/config/people/profile/edit/' . $fid, $edit, t('Save field')); - - // Check that the updated field is appearing on the user edit form. - $this->drupalGet('user/' . $this->admin_user->uid . '/edit/' . $category); - - // Checking field. - if ($type == 'date') { - $this->assertField($form_name . '[month]', t('Found month selection field')); - $this->assertField($form_name . '[day]', t('Found day selection field')); - $this->assertField($form_name . '[year]', t('Found day selection field')); - } - else { - $this->assertField($form_name , t('Found form named @name', array('@name' => $form_name))); - } - - // Checking name. - $this->assertText($title, t('Checking title for field %title', array('%title' => $title))); - // Checking explanation. - $this->assertText($edit['explanation'], t('Checking explanation for field %title', array('%title' => $title))); - - return array( - 'fid' => $fid, - 'type' => $type, - 'form_name' => $form_name, - 'title' => $title, - 'category' => $category, - ); - } - - /** - * Set the profile field to a random value - * - * @param $field - * The field that should be set. - * @param $value - * The value for the field, defaults to a random string. - * @return - * The value that has been assigned to - */ - function setProfileField($field, $value = NULL) { - - if (!isset($value)) { - $value = $this->randomName(); - } - - $edit = array( - $field['form_name'] => $value, - ); - $this->drupalPost('user/' . $this->normal_user->uid . '/edit/' . $field['category'], $edit, t('Save')); - - // Check profile page. - $content = $this->drupalGet('user/' . $this->normal_user->uid); - $this->assertText($field['title'], t('Found profile field with title %title', array('%title' => $field['title']))); - - if ($field['type'] != 'checkbox') { - // $value must be cast to a string in order to be found by assertText. - $this->assertText("$value", t('Found profile field with value %value', array('%value' => $value))); - } - - return $value; - } - - /** - * Delete a profile field. - * - * @param $field - * The field to be deleted. - */ - function deleteProfileField($field) { - $this->drupalPost('admin/config/people/profile/delete/' . $field['fid'], array(), t('Delete')); - $this->drupalGet('admin/config/people/profile'); - $this->assertNoText($field['title'], t('Checking deleted field %title', array('%title' => $field['title']))); - } -} - -class ProfileTestFields extends ProfileTestCase { - public static function getInfo() { - return array( - 'name' => 'Test single fields', - 'description' => 'Testing profile module with add/edit/delete textfield, textarea, list, checkbox, and url fields into profile page', - 'group' => 'Profile' - ); - } - - /** - * Test each of the field types. List selection and date fields are tested - * separately because they need some special handling. - */ - function testProfileFields() { - $this->drupalLogin($this->admin_user); - - // Set test values for every field type. - $field_types = array( - 'textfield' => $this->randomName(), - 'textarea' => $this->randomName(), - 'list' => $this->randomName(), - 'checkbox' => 1, - // An underscore is an invalid character in a domain name. The method randomName can - // return an underscore. - 'url' => 'http://www.' . str_replace('_', '', $this->randomName(10)) . '.org', - ); - - // For each field type, create a field, give it a value, update the field, - // and delete the field. - foreach ($field_types as $type => $value) { - $field = $this->createProfileField($type); - $this->setProfileField($field, $value); - $edit = array( - 'name' => $field['form_name'], - 'title' => $this->randomName(), - 'category' => $field['category'], - 'explanation' => $this->randomName(), - ); - $field = $this->updateProfileField($field['fid'], $field['type'], $edit); - $this->deleteProfileField($field); - } - } -} - -class ProfileTestSelect extends ProfileTestCase { - public static function getInfo() { - return array( - 'name' => 'Test select field', - 'description' => 'Testing profile module with add/edit/delete a select field', - 'group' => 'Profile' - ); - } - - /** - * Create a list selection field, give it a value, update and delete the field. - */ - function testProfileSelectionField() { - $this->drupalLogin($this->admin_user); - - $edit = array( - 'options' => implode("\n", range(1, 10)), - ); - $field = $this->createProfileField('selection', 'simpletest', $edit); - - $this->setProfileField($field, rand(1, 10)); - - $edit = array( - 'name' => $field['form_name'], - 'title' => $this->randomName(), - 'category' => $field['category'], - 'explanation' => $this->randomName(), - ); - $field = $this->updateProfileField($field['fid'], $field['type'], $edit); - $this->deleteProfileField($field); - } -} - -class ProfileTestDate extends ProfileTestCase { - public static function getInfo() { - return array( - 'name' => 'Test date field', - 'description' => 'Testing profile module with add/edit/delete a date field', - 'group' => 'Profile' - ); - } - - /** - * Create a date field, give it a value, update and delete the field. - */ - function testProfileDateField() { - $this->drupalLogin($this->admin_user); - - variable_set('date_format_short', 'm/d/Y - H:i'); - $field = $this->createProfileField('date'); - - // Set date to January 09, 1983 - $edit = array( - $field['form_name'] . '[month]' => 1, - $field['form_name'] . '[day]' => 9, - $field['form_name'] . '[year]' => 1983, - ); - - $this->drupalPost('user/' . $this->normal_user->uid . '/edit/' . $field['category'], $edit, t('Save')); - - // Check profile page. - $this->drupalGet('user/' . $this->normal_user->uid); - $this->assertText($field['title'], t('Found profile field with title %title', array('%title' => $field['title']))); - - $this->assertText('01/09/1983', t('Found date profile field.')); - - $edit = array( - 'name' => $field['form_name'], - 'title' => $this->randomName(), - 'category' => $field['category'], - 'explanation' => $this->randomName(), - ); - $field = $this->updateProfileField($field['fid'], $field['type'], $edit); - $this->deleteProfileField($field); - } -} - -class ProfileTestWeights extends ProfileTestCase { - public static function getInfo() { - return array( - 'name' => 'Test field weights', - 'description' => 'Testing profile modules weigting of fields', - 'group' => 'Profile' - ); - } - - function testProfileFieldWeights() { - $this->drupalLogin($this->admin_user); - - $category = $this->randomName(); - $field1 = $this->createProfileField('textfield', $category, array('weight' => 1)); - $field2 = $this->createProfileField('textfield', $category, array('weight' => -1)); - - $this->setProfileField($field1, $this->randomName(8)); - $this->setProfileField($field2, $this->randomName(8)); - - $profile_edit = $this->drupalGet('user/' . $this->normal_user->uid . '/edit/' . $category); - $this->assertTrue(strpos($profile_edit, $field1['title']) > strpos($profile_edit, $field2['title']), t('Profile field weights are respected on the user edit form.')); - - $profile_page = $this->drupalGet('user/' . $this->normal_user->uid); - $this->assertTrue(strpos($profile_page, $field1['title']) > strpos($profile_page, $field2['title']), t('Profile field weights are respected on the user profile page.')); - } -} - -/** - * Test profile field autocompletion and access. - */ -class ProfileTestAutocomplete extends ProfileTestCase { - public static function getInfo() { - return array( - 'name' => 'Autocompletion', - 'description' => 'Test profile fields with autocompletion.', - 'group' => 'Profile' - ); - } - - /** - * Tests profile field autocompletion and access. - */ - function testAutocomplete() { - $this->drupalLogin($this->admin_user); - - // Create a new profile field with autocompletion enabled. - $category = $this->randomName(); - $field = $this->createProfileField('textfield', $category, array('weight' => 1, 'autocomplete' => 1)); - - // Enter profile field value. - $field['value'] = $this->randomName(); - $this->setProfileField($field, $field['value']); - - // Set some html for what we want to see in the page output later. - $autocomplete_html = ''; - $field_html = ''; - - // Check that autocompletion html is found on the user's profile edit page. - $this->drupalGet('user/' . $this->admin_user->uid . '/edit/' . $category); - $this->assertRaw($autocomplete_html, t('Autocomplete found.')); - $this->assertRaw('misc/autocomplete.js', t('Autocomplete JavaScript found.')); - $this->assertRaw('class="form-text form-autocomplete"', t('Autocomplete form element class found.')); - - // Check the autocompletion path using the first letter of our user's profile - // field value to make sure access is allowed and a valid result if found. - $this->drupalGet('profile/autocomplete/' . $field['fid'] . '/' . $field['value'][0]); - $this->assertResponse(200, t('Autocomplete path allowed to user with permission.')); - $this->assertRaw($field['value'], t('Autocomplete value found.')); - - // Logout and login with a user without the 'access user profiles' permission. - $this->drupalLogout(); - $this->drupalLogin($this->normal_user); - - // Check that autocompletion html is not found on the user's profile edit page. - $this->drupalGet('user/' . $this->normal_user->uid . '/edit/' . $category); - $this->assertNoRaw($autocomplete_html, t('Autocomplete not found.')); - - // User should be denied access to the profile autocomplete path. - $this->drupalGet('profile/autocomplete/' . $field['fid'] . '/' . $field['value'][0]); - $this->assertResponse(403, t('Autocomplete path denied to user without permission.')); - } -} - -class ProfileBlockTestCase extends ProfileTestCase { - public static function getInfo() { - return array( - 'name' => 'Block availability', - 'description' => 'Check if the Author Information block is available.', - 'group' => 'Profile', - ); - } - - function setUp() { - parent::setUp(); - - // Login the admin user. - $this->drupalLogin($this->admin_user); - - // Create two fields. - $category = $this->randomName(); - $this->field1 = $this->createProfileField('textfield', $category, array('weight' => 0)); - $this->field2 = $this->createProfileField('textfield', $category, array('weight' => 1)); - - // Assign values to those fields. - $this->value1 = $this->setProfileField($this->field1); - $this->value2 = $this->setProfileField($this->field2); - - // Create a node authored by the normal user. - $this->node = $this->drupalCreateNode(array( - 'uid' => $this->normal_user->uid, - )); - } - - function testAuthorInformationBlock() { - // Set the block to a region to confirm the block is availble. - $edit = array(); - $edit['blocks[profile_author-information][region]'] = 'footer'; - $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); - $this->assertText(t('The block settings have been updated.'), t('Block successfully move to footer region.')); - - // Enable field 1. - $this->drupalPost('admin/structure/block/manage/profile/author-information/configure', array( - 'profile_block_author_fields[' . $this->field1['form_name'] . ']' => TRUE, - ), t('Save block')); - $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.')); - - // Visit the node and confirm that the field is displayed. - $this->drupalGet('node/' . $this->node->nid); - $this->assertRaw($this->value1, t('Field 1 is displayed')); - $this->assertNoRaw($this->value2, t('Field 2 is not displayed')); - - // Enable only field 2. - $this->drupalPost('admin/structure/block/manage/profile/author-information/configure', array( - 'profile_block_author_fields[' . $this->field1['form_name'] . ']' => FALSE, - 'profile_block_author_fields[' . $this->field2['form_name'] . ']' => TRUE, - ), t('Save block')); - $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.')); - - // Visit the node and confirm that the field is displayed. - $this->drupalGet('node/' . $this->node->nid); - $this->assertNoRaw($this->value1, t('Field 1 is not displayed')); - $this->assertRaw($this->value2, t('Field 2 is displayed')); - - // Enable both fields. - $this->drupalPost('admin/structure/block/manage/profile/author-information/configure', array( - 'profile_block_author_fields[' . $this->field1['form_name'] . ']' => TRUE, - 'profile_block_author_fields[' . $this->field2['form_name'] . ']' => TRUE, - ), t('Save block')); - $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.')); - - // Visit the node and confirm that the field is displayed. - $this->drupalGet('node/' . $this->node->nid); - $this->assertRaw($this->value1, t('Field 1 is displayed')); - $this->assertRaw($this->value2, t('Field 2 is displayed')); - - // Enable the link to the user profile. - $this->drupalPost('admin/structure/block/manage/profile/author-information/configure', array( - 'profile_block_author_fields[user_profile]' => TRUE, - ), t('Save block')); - $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.')); - - // Visit the node and confirm that the user profile link is displayed. - $this->drupalGet('node/' . $this->node->nid); - $this->clickLink(t('View full user profile')); - $this->assertEqual($this->getUrl(), url('user/' . $this->normal_user->uid, array('absolute' => TRUE))); - } -} - -/** - * Test profile browsing. - */ -class ProfileTestBrowsing extends ProfileTestCase { - public static function getInfo() { - return array( - 'name' => 'Profile browsing', - 'description' => 'Test profile browsing.', - 'group' => 'Profile', - ); - } - - /** - * Test profile browsing. - */ - function testProfileBrowsing() { - $this->drupalLogin($this->admin_user); - $field = $this->createProfileField('list', 'simpletest', array('page' => '%value')); - - // Set a random value for the profile field. - $value = $this->setProfileField($field); - - // Check that user is found on the profile browse page. - $this->drupalGet("profile/{$field['form_name']}/$value"); - $this->assertText($this->normal_user->name); - } -} - - /** - * TODO: - * - Test field visibility - * - Test required fields - * - Test fields on registration form - * - Test updating fields - */ diff --git a/modules/search/search.test b/modules/search/search.test deleted file mode 100644 index 4d37133..0000000 --- a/modules/search/search.test +++ /dev/null @@ -1,1949 +0,0 @@ - 'Search engine queries', - 'description' => 'Indexes content and queries it.', - 'group' => 'Search', - ); - } - - /** - * Implementation setUp(). - */ - function setUp() { - parent::setUp('search'); - } - - /** - * Test search indexing. - */ - function testMatching() { - $this->_setup(); - $this->_testQueries(); - } - - /** - * Set up a small index of items to test against. - */ - function _setup() { - variable_set('minimum_word_size', 3); - - for ($i = 1; $i <= 7; ++$i) { - search_index($i, SEARCH_TYPE, $this->getText($i)); - } - for ($i = 1; $i <= 5; ++$i) { - search_index($i + 7, SEARCH_TYPE_2, $this->getText2($i)); - } - // No getText builder function for Japanese text; just a simple array. - foreach (array( - 13 => '以呂波耳・ほへとち。リヌルヲ。', - 14 => 'ドルーパルが大好きよ!', - 15 => 'コーヒーとケーキ', - ) as $i => $jpn) { - search_index($i, SEARCH_TYPE_JPN, $jpn); - } - search_update_totals(); - } - - /** - * _test_: Helper method for generating snippets of content. - * - * Generated items to test against: - * 1 ipsum - * 2 dolore sit - * 3 sit am ut - * 4 am ut enim am - * 5 ut enim am minim veniam - * 6 enim am minim veniam es cillum - * 7 am minim veniam es cillum dolore eu - */ - function getText($n) { - $words = explode(' ', "Ipsum dolore sit am. Ut enim am minim veniam. Es cillum dolore eu."); - return implode(' ', array_slice($words, $n - 1, $n)); - } - - /** - * _test2_: Helper method for generating snippets of content. - * - * Generated items to test against: - * 8 dear - * 9 king philip - * 10 philip came over - * 11 came over from germany - * 12 over from germany swimming - */ - function getText2($n) { - $words = explode(' ', "Dear King Philip came over from Germany swimming."); - return implode(' ', array_slice($words, $n - 1, $n)); - } - - /** - * Run predefine queries looking for indexed terms. - */ - function _testQueries() { - /* - Note: OR queries that include short words in OR groups are only accepted - if the ORed terms are ANDed with at least one long word in the rest of the query. - - e.g. enim dolore OR ut = enim (dolore OR ut) = (enim dolor) OR (enim ut) -> good - e.g. dolore OR ut = (dolore) OR (ut) -> bad - - This is a design limitation to avoid full table scans. - */ - $queries = array( - // Simple AND queries. - 'ipsum' => array(1), - 'enim' => array(4, 5, 6), - 'xxxxx' => array(), - 'enim minim' => array(5, 6), - 'enim xxxxx' => array(), - 'dolore eu' => array(7), - 'dolore xx' => array(), - 'ut minim' => array(5), - 'xx minim' => array(), - 'enim veniam am minim ut' => array(5), - // Simple OR queries. - 'dolore OR ipsum' => array(1, 2, 7), - 'dolore OR xxxxx' => array(2, 7), - 'dolore OR ipsum OR enim' => array(1, 2, 4, 5, 6, 7), - 'ipsum OR dolore sit OR cillum' => array(2, 7), - 'minim dolore OR ipsum' => array(7), - 'dolore OR ipsum veniam' => array(7), - 'minim dolore OR ipsum OR enim' => array(5, 6, 7), - 'dolore xx OR yy' => array(), - 'xxxxx dolore OR ipsum' => array(), - // Negative queries. - 'dolore -sit' => array(7), - 'dolore -eu' => array(2), - 'dolore -xxxxx' => array(2, 7), - 'dolore -xx' => array(2, 7), - // Phrase queries. - '"dolore sit"' => array(2), - '"sit dolore"' => array(), - '"am minim veniam es"' => array(6, 7), - '"minim am veniam es"' => array(), - // Mixed queries. - '"am minim veniam es" OR dolore' => array(2, 6, 7), - '"minim am veniam es" OR "dolore sit"' => array(2), - '"minim am veniam es" OR "sit dolore"' => array(), - '"am minim veniam es" -eu' => array(6), - '"am minim veniam" -"cillum dolore"' => array(5, 6), - '"am minim veniam" -"dolore cillum"' => array(5, 6, 7), - 'xxxxx "minim am veniam es" OR dolore' => array(), - 'xx "minim am veniam es" OR dolore' => array() - ); - foreach ($queries as $query => $results) { - $result = db_select('search_index', 'i') - ->extend('SearchQuery') - ->searchExpression($query, SEARCH_TYPE) - ->execute(); - - $set = $result ? $result->fetchAll() : array(); - $this->_testQueryMatching($query, $set, $results); - $this->_testQueryScores($query, $set, $results); - } - - // These queries are run against the second index type, SEARCH_TYPE_2. - $queries = array( - // Simple AND queries. - 'ipsum' => array(), - 'enim' => array(), - 'enim minim' => array(), - 'dear' => array(8), - 'germany' => array(11, 12), - ); - foreach ($queries as $query => $results) { - $result = db_select('search_index', 'i') - ->extend('SearchQuery') - ->searchExpression($query, SEARCH_TYPE_2) - ->execute(); - - $set = $result ? $result->fetchAll() : array(); - $this->_testQueryMatching($query, $set, $results); - $this->_testQueryScores($query, $set, $results); - } - - // These queries are run against the third index type, SEARCH_TYPE_JPN. - $queries = array( - // Simple AND queries. - '呂波耳' => array(13), - '以呂波耳' => array(13), - 'ほへと ヌルヲ' => array(13), - 'とちリ' => array(), - 'ドルーパル' => array(14), - 'パルが大' => array(14), - 'コーヒー' => array(15), - 'ヒーキ' => array(), - ); - foreach ($queries as $query => $results) { - $result = db_select('search_index', 'i') - ->extend('SearchQuery') - ->searchExpression($query, SEARCH_TYPE_JPN) - ->execute(); - - $set = $result ? $result->fetchAll() : array(); - $this->_testQueryMatching($query, $set, $results); - $this->_testQueryScores($query, $set, $results); - } - } - - /** - * Test the matching abilities of the engine. - * - * Verify if a query produces the correct results. - */ - function _testQueryMatching($query, $set, $results) { - // Get result IDs. - $found = array(); - foreach ($set as $item) { - $found[] = $item->sid; - } - - // Compare $results and $found. - sort($found); - sort($results); - $this->assertEqual($found, $results, "Query matching '$query'"); - } - - /** - * Test the scoring abilities of the engine. - * - * Verify if a query produces normalized, monotonous scores. - */ - function _testQueryScores($query, $set, $results) { - // Get result scores. - $scores = array(); - foreach ($set as $item) { - $scores[] = $item->calculated_score; - } - - // Check order. - $sorted = $scores; - sort($sorted); - $this->assertEqual($scores, array_reverse($sorted), "Query order '$query'"); - - // Check range. - $this->assertEqual(!count($scores) || (min($scores) > 0.0 && max($scores) <= 1.0001), TRUE, "Query scoring '$query'"); - } -} - -/** - * Tests the bike shed text on no results page, and text on the search page. - */ -class SearchPageText extends DrupalWebTestCase { - protected $searching_user; - - public static function getInfo() { - return array( - 'name' => 'Search page text', - 'description' => 'Tests the bike shed text on the no results page, and various other text on search pages.', - 'group' => 'Search' - ); - } - - function setUp() { - parent::setUp('search'); - - // Create user. - $this->searching_user = $this->drupalCreateUser(array('search content', 'access user profiles')); - } - - /** - * Tests the failed search text, and various other text on the search page. - */ - function testSearchText() { - $this->drupalLogin($this->searching_user); - $this->drupalGet('search/node'); - $this->assertText(t('Enter your keywords')); - $this->assertText(t('Search')); - $title = t('Search') . ' | Drupal'; - $this->assertTitle($title, 'Search page title is correct'); - - $edit = array(); - $edit['keys'] = 'bike shed ' . $this->randomName(); - $this->drupalPost('search/node', $edit, t('Search')); - $this->assertText(t('Consider loosening your query with OR. bike OR shed will often show more results than bike shed.'), t('Help text is displayed when search returns no results.')); - $this->assertText(t('Search')); - $this->assertTitle($title, 'Search page title is correct'); - - $edit['keys'] = $this->searching_user->name; - $this->drupalPost('search/user', $edit, t('Search')); - $this->assertText(t('Search')); - $this->assertTitle($title, 'Search page title is correct'); - - // Test that search keywords containing slashes are correctly loaded - // from the path and displayed in the search form. - $arg = $this->randomName() . '/' . $this->randomName(); - $this->drupalGet('search/node/' . $arg); - $input = $this->xpath("//input[@id='edit-keys' and @value='{$arg}']"); - $this->assertFalse(empty($input), 'Search keys with a / are correctly set as the default value in the search box.'); - } -} - -class SearchAdvancedSearchForm extends DrupalWebTestCase { - protected $node; - - public static function getInfo() { - return array( - 'name' => 'Advanced search form', - 'description' => 'Indexes content and tests the advanced search form.', - 'group' => 'Search', - ); - } - - function setUp() { - parent::setUp('search'); - // Create and login user. - $test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'administer nodes')); - $this->drupalLogin($test_user); - - // Create initial node. - $node = $this->drupalCreateNode(); - $this->node = $this->drupalCreateNode(); - - // First update the index. This does the initial processing. - node_update_index(); - - // Then, run the shutdown function. Testing is a unique case where indexing - // and searching has to happen in the same request, so running the shutdown - // function manually is needed to finish the indexing process. - search_update_totals(); - } - - /** - * Test using the search form with GET and POST queries. - * Test using the advanced search form to limit search to nodes of type "Basic page". - */ - function testNodeType() { - $this->assertTrue($this->node->type == 'page', t('Node type is Basic page.')); - - // Assert that the dummy title doesn't equal the real title. - $dummy_title = 'Lorem ipsum'; - $this->assertNotEqual($dummy_title, $this->node->title, t("Dummy title doens't equal node title")); - - // Search for the dummy title with a GET query. - $this->drupalGet('search/node/' . $dummy_title); - $this->assertNoText($this->node->title, t('Basic page node is not found with dummy title.')); - - // Search for the title of the node with a GET query. - $this->drupalGet('search/node/' . $this->node->title); - $this->assertText($this->node->title, t('Basic page node is found with GET query.')); - - // Search for the title of the node with a POST query. - $edit = array('or' => $this->node->title); - $this->drupalPost('search/node', $edit, t('Advanced search')); - $this->assertText($this->node->title, t('Basic page node is found with POST query.')); - - // Advanced search type option. - $this->drupalPost('search/node', array_merge($edit, array('type[page]' => 'page')), t('Advanced search')); - $this->assertText($this->node->title, t('Basic page node is found with POST query and type:page.')); - - $this->drupalPost('search/node', array_merge($edit, array('type[article]' => 'article')), t('Advanced search')); - $this->assertText('bike shed', t('Article node is not found with POST query and type:article.')); - } -} - -class SearchRankingTestCase extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'Search engine ranking', - 'description' => 'Indexes content and tests ranking factors.', - 'group' => 'Search', - ); - } - - /** - * Implementation setUp(). - */ - function setUp() { - parent::setUp('search', 'statistics', 'comment'); - } - - function testRankings() { - // Login with sufficient privileges. - $this->drupalLogin($this->drupalCreateUser(array('skip comment approval', 'create page content'))); - - // Build a list of the rankings to test. - $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views'); - - // Create nodes for testing. - foreach ($node_ranks as $node_rank) { - $settings = array( - 'type' => 'page', - 'title' => 'Drupal rocks', - 'body' => array(LANGUAGE_NONE => array(array('value' => "Drupal's search rocks"))), - ); - foreach (array(0, 1) as $num) { - if ($num == 1) { - switch ($node_rank) { - case 'sticky': - case 'promote': - $settings[$node_rank] = 1; - break; - case 'relevance': - $settings['body'][LANGUAGE_NONE][0]['value'] .= " really rocks"; - break; - case 'recent': - $settings['created'] = REQUEST_TIME + 3600; - break; - case 'comments': - $settings['comment'] = 2; - break; - } - } - $nodes[$node_rank][$num] = $this->drupalCreateNode($settings); - } - } - - // Update the search index. - module_invoke_all('update_index'); - search_update_totals(); - - // Refresh variables after the treatment. - $this->refreshVariables(); - - // Add a comment to one of the nodes. - $edit = array(); - $edit['subject'] = 'my comment title'; - $edit['comment_body[' . LANGUAGE_NONE . '][0][value]'] = 'some random comment'; - $this->drupalGet('comment/reply/' . $nodes['comments'][1]->nid); - $this->drupalPost(NULL, $edit, t('Preview')); - $this->drupalPost(NULL, $edit, t('Save')); - - // Enable counting of statistics. - variable_set('statistics_count_content_views', 1); - - // Then View one of the nodes a bunch of times. - for ($i = 0; $i < 5; $i ++) { - $this->drupalGet('node/' . $nodes['views'][1]->nid); - } - - // Test each of the possible rankings. - foreach ($node_ranks as $node_rank) { - // Disable all relevancy rankings except the one we are testing. - foreach ($node_ranks as $var) { - variable_set('node_rank_' . $var, $var == $node_rank ? 10 : 0); - } - - // Do the search and assert the results. - $set = node_search_execute('rocks'); - $this->assertEqual($set[0]['node']->nid, $nodes[$node_rank][1]->nid, 'Search ranking "' . $node_rank . '" order.'); - } - } - - /** - * Test rankings of HTML tags. - */ - function testHTMLRankings() { - // Login with sufficient privileges. - $this->drupalLogin($this->drupalCreateUser(array('create page content'))); - - // Test HTML tags with different weights. - $sorted_tags = array('h1', 'h2', 'h3', 'h4', 'a', 'h5', 'h6', 'notag'); - $shuffled_tags = $sorted_tags; - - // Shuffle tags to ensure HTML tags are ranked properly. - shuffle($shuffled_tags); - $settings = array( - 'type' => 'page', - 'title' => 'Simple node', - ); - foreach ($shuffled_tags as $tag) { - switch ($tag) { - case 'a': - $settings['body'] = array(LANGUAGE_NONE => array(array('value' => l('Drupal Rocks', 'node'), 'format' => 'full_html'))); - break; - case 'notag': - $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'Drupal Rocks'))); - break; - default: - $settings['body'] = array(LANGUAGE_NONE => array(array('value' => "<$tag>Drupal Rocks", 'format' => 'full_html'))); - break; - } - $nodes[$tag] = $this->drupalCreateNode($settings); - } - - // Update the search index. - module_invoke_all('update_index'); - search_update_totals(); - - // Refresh variables after the treatment. - $this->refreshVariables(); - - // Disable all other rankings. - $node_ranks = array('sticky', 'promote', 'recent', 'comments', 'views'); - foreach ($node_ranks as $node_rank) { - variable_set('node_rank_' . $node_rank, 0); - } - $set = node_search_execute('rocks'); - - // Test the ranking of each tag. - foreach ($sorted_tags as $tag_rank => $tag) { - // Assert the results. - if ($tag == 'notag') { - $this->assertEqual($set[$tag_rank]['node']->nid, $nodes[$tag]->nid, 'Search tag ranking for plain text order.'); - } else { - $this->assertEqual($set[$tag_rank]['node']->nid, $nodes[$tag]->nid, 'Search tag ranking for "<' . $sorted_tags[$tag_rank] . '>" order.'); - } - } - - // Test tags with the same weight against the sorted tags. - $unsorted_tags = array('u', 'b', 'i', 'strong', 'em'); - foreach ($unsorted_tags as $tag) { - $settings['body'] = array(LANGUAGE_NONE => array(array('value' => "<$tag>Drupal Rocks", 'format' => 'full_html'))); - $node = $this->drupalCreateNode($settings); - - // Update the search index. - module_invoke_all('update_index'); - search_update_totals(); - - // Refresh variables after the treatment. - $this->refreshVariables(); - - $set = node_search_execute('rocks'); - - // Ranking should always be second to last. - $set = array_slice($set, -2, 1); - - // Assert the results. - $this->assertEqual($set[0]['node']->nid, $node->nid, 'Search tag ranking for "<' . $tag . '>" order.'); - - // Delete node so it doesn't show up in subsequent search results. - node_delete($node->nid); - } - } - - /** - * Verifies that if we combine two rankings, search still works. - * - * See issue http://drupal.org/node/771596 - */ - function testDoubleRankings() { - // Login with sufficient privileges. - $this->drupalLogin($this->drupalCreateUser(array('skip comment approval', 'create page content'))); - - // See testRankings() above - build a node that will rank high for sticky. - $settings = array( - 'type' => 'page', - 'title' => 'Drupal rocks', - 'body' => array(LANGUAGE_NONE => array(array('value' => "Drupal's search rocks"))), - 'sticky' => 1, - ); - - $node = $this->drupalCreateNode($settings); - - // Update the search index. - module_invoke_all('update_index'); - search_update_totals(); - - // Refresh variables after the treatment. - $this->refreshVariables(); - - // Set up for ranking sticky and lots of comments; make sure others are - // disabled. - $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views'); - foreach ($node_ranks as $var) { - $value = ($var == 'sticky' || $var == 'comments') ? 10 : 0; - variable_set('node_rank_' . $var, $value); - } - - // Do the search and assert the results. - $set = node_search_execute('rocks'); - $this->assertEqual($set[0]['node']->nid, $node->nid, 'Search double ranking order.'); - } -} - -class SearchBlockTestCase extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'Block availability', - 'description' => 'Check if the search form block is available.', - 'group' => 'Search', - ); - } - - function setUp() { - parent::setUp('search'); - - // Create and login user - $admin_user = $this->drupalCreateUser(array('administer blocks', 'search content')); - $this->drupalLogin($admin_user); - } - - function testSearchFormBlock() { - // Set block title to confirm that the interface is availble. - $this->drupalPost('admin/structure/block/manage/search/form/configure', array('title' => $this->randomName(8)), t('Save block')); - $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.')); - - // Set the block to a region to confirm block is availble. - $edit = array(); - $edit['blocks[search_form][region]'] = 'footer'; - $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); - $this->assertText(t('The block settings have been updated.'), t('Block successfully move to footer region.')); - } - - /** - * Test that the search block form works correctly. - */ - function testBlock() { - // Enable the block, and place it in the 'content' region so that it isn't - // hidden on 404 pages. - $edit = array('blocks[search_form][region]' => 'content'); - $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); - - // Test a normal search via the block form, from the front page. - $terms = array('search_block_form' => 'test'); - $this->drupalPost('node', $terms, t('Search')); - $this->assertText('Your search yielded no results'); - - // Test a search from the block on a 404 page. - $this->drupalGet('foo'); - $this->assertResponse(404); - $this->drupalPost(NULL, $terms, t('Search')); - $this->assertResponse(200); - $this->assertText('Your search yielded no results'); - - // Test a search from the block when it doesn't appear on the search page. - $edit = array('pages' => 'search'); - $this->drupalPost('admin/structure/block/manage/search/form/configure', $edit, t('Save block')); - $this->drupalPost('node', $terms, t('Search')); - $this->assertText('Your search yielded no results'); - - // Confirm that the user is redirected to the search page. - $this->assertEqual( - $this->getUrl(), - url('search/node/' . $terms['search_block_form'], array('absolute' => TRUE)), - t('Redirected to correct url.') - ); - - // Test an empty search via the block form, from the front page. - $terms = array('search_block_form' => ''); - $this->drupalPost('node', $terms, t('Search')); - $this->assertText('Please enter some keywords'); - - // Confirm that the user is redirected to the search page, when form is submitted empty. - $this->assertEqual( - $this->getUrl(), - url('search/node/', array('absolute' => TRUE)), - t('Redirected to correct url.') - ); - } -} - -/** - * Tests that searching for a phrase gets the correct page count. - */ -class SearchExactTestCase extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'Search engine phrase queries', - 'description' => 'Tests that searching for a phrase gets the correct page count.', - 'group' => 'Search', - ); - } - - function setUp() { - parent::setUp('search'); - } - - /** - * Tests that the correct number of pager links are found for both keywords and phrases. - */ - function testExactQuery() { - // Login with sufficient privileges. - $this->drupalLogin($this->drupalCreateUser(array('create page content', 'search content'))); - - $settings = array( - 'type' => 'page', - 'title' => 'Simple Node', - ); - // Create nodes with exact phrase. - for ($i = 0; $i <= 17; $i++) { - $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love pizza'))); - $this->drupalCreateNode($settings); - } - // Create nodes containing keywords. - for ($i = 0; $i <= 17; $i++) { - $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love cheesy pizza'))); - $this->drupalCreateNode($settings); - } - - // Update the search index. - module_invoke_all('update_index'); - search_update_totals(); - - // Refresh variables after the treatment. - $this->refreshVariables(); - - // Test that the correct number of pager links are found for keyword search. - $edit = array('keys' => 'love pizza'); - $this->drupalPost('search/node', $edit, t('Search')); - $this->assertLinkByHref('page=1', 0, '2nd page link is found for keyword search.'); - $this->assertLinkByHref('page=2', 0, '3rd page link is found for keyword search.'); - $this->assertLinkByHref('page=3', 0, '4th page link is found for keyword search.'); - $this->assertNoLinkByHref('page=4', '5th page link is not found for keyword search.'); - - // Test that the correct number of pager links are found for exact phrase search. - $edit = array('keys' => '"love pizza"'); - $this->drupalPost('search/node', $edit, t('Search')); - $this->assertLinkByHref('page=1', 0, '2nd page link is found for exact phrase search.'); - $this->assertNoLinkByHref('page=2', '3rd page link is not found for exact phrase search.'); - } -} - -/** - * Test integration searching comments. - */ -class SearchCommentTestCase extends DrupalWebTestCase { - protected $admin_user; - - public static function getInfo() { - return array( - 'name' => 'Comment Search tests', - 'description' => 'Verify text formats and filters used elsewhere.', - 'group' => 'Search', - ); - } - - function setUp() { - parent::setUp('comment', 'search'); - - // Create and log in an administrative user having access to the Full HTML - // text format. - $full_html_format = filter_format_load('full_html'); - $permissions = array( - 'administer filters', - filter_permission_name($full_html_format), - 'administer permissions', - 'create page content', - 'skip comment approval', - 'access comments', - ); - $this->admin_user = $this->drupalCreateUser($permissions); - $this->drupalLogin($this->admin_user); - } - - /** - * Verify that comments are rendered using proper format in search results. - */ - function testSearchResultsComment() { - $comment_body = 'Test comment body'; - - variable_set('comment_preview_article', DRUPAL_OPTIONAL); - // Enable check_plain() for 'Filtered HTML' text format. - $filtered_html_format_id = 'filtered_html'; - $edit = array( - 'filters[filter_html_escape][status]' => TRUE, - ); - $this->drupalPost('admin/config/content/formats/' . $filtered_html_format_id, $edit, t('Save configuration')); - // Allow anonymous users to search content. - $edit = array( - DRUPAL_ANONYMOUS_RID . '[search content]' => 1, - DRUPAL_ANONYMOUS_RID . '[access comments]' => 1, - DRUPAL_ANONYMOUS_RID . '[post comments]' => 1, - ); - $this->drupalPost('admin/people/permissions', $edit, t('Save permissions')); - - // Create a node. - $node = $this->drupalCreateNode(array('type' => 'article')); - // Post a comment using 'Full HTML' text format. - $edit_comment = array(); - $edit_comment['subject'] = 'Test comment subject'; - $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '

    ' . $comment_body . '

    '; - $full_html_format_id = 'full_html'; - $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $full_html_format_id; - $this->drupalPost('comment/reply/' . $node->nid, $edit_comment, t('Save')); - - // Invoke search index update. - $this->drupalLogout(); - $this->cronRun(); - - // Search for the comment subject. - $edit = array( - 'search_block_form' => "'" . $edit_comment['subject'] . "'", - ); - $this->drupalPost('', $edit, t('Search')); - $this->assertText($node->title, t('Node found in search results.')); - $this->assertText($edit_comment['subject'], t('Comment subject found in search results.')); - - // Search for the comment body. - $edit = array( - 'search_block_form' => "'" . $comment_body . "'", - ); - $this->drupalPost('', $edit, t('Search')); - $this->assertText($node->title, t('Node found in search results.')); - - // Verify that comment is rendered using proper format. - $this->assertText($comment_body, t('Comment body text found in search results.')); - $this->assertNoRaw(t('n/a'), t('HTML in comment body is not hidden.')); - $this->assertNoRaw(check_plain($edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]']), t('HTML in comment body is not escaped.')); - - // Hide comments. - $this->drupalLogin($this->admin_user); - $node->comment = 0; - node_save($node); - - // Invoke search index update. - $this->drupalLogout(); - $this->cronRun(); - - // Search for $title. - $this->drupalPost('', $edit, t('Search')); - $this->assertNoText($comment_body, t('Comment body text not found in search results.')); - } - - /** - * Verify access rules for comment indexing with different permissions. - */ - function testSearchResultsCommentAccess() { - $comment_body = 'Test comment body'; - $this->comment_subject = 'Test comment subject'; - $this->admin_role = $this->admin_user->roles; - unset($this->admin_role[DRUPAL_AUTHENTICATED_RID]); - $this->admin_role = key($this->admin_role); - - // Create a node. - variable_set('comment_preview_article', DRUPAL_OPTIONAL); - $this->node = $this->drupalCreateNode(array('type' => 'article')); - - // Post a comment using 'Full HTML' text format. - $edit_comment = array(); - $edit_comment['subject'] = $this->comment_subject; - $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '

    ' . $comment_body . '

    '; - $this->drupalPost('comment/reply/' . $this->node->nid, $edit_comment, t('Save')); - - $this->drupalLogout(); - $this->setRolePermissions(DRUPAL_ANONYMOUS_RID); - $this->checkCommentAccess('Anon user has search permission but no access comments permission, comments should not be indexed'); - - $this->setRolePermissions(DRUPAL_ANONYMOUS_RID, TRUE); - $this->checkCommentAccess('Anon user has search permission and access comments permission, comments should be indexed', TRUE); - - $this->drupalLogin($this->admin_user); - $this->drupalGet('admin/people/permissions'); - - // Disable search access for authenticated user to test admin user. - $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, FALSE, FALSE); - - $this->setRolePermissions($this->admin_role); - $this->checkCommentAccess('Admin user has search permission but no access comments permission, comments should not be indexed'); - - $this->setRolePermissions($this->admin_role, TRUE); - $this->checkCommentAccess('Admin user has search permission and access comments permission, comments should be indexed', TRUE); - - $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID); - $this->checkCommentAccess('Authenticated user has search permission but no access comments permission, comments should not be indexed'); - - $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE); - $this->checkCommentAccess('Authenticated user has search permission and access comments permission, comments should be indexed', TRUE); - - // Verify that access comments permission is inherited from the - // authenticated role. - $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, FALSE); - $this->setRolePermissions($this->admin_role); - $this->checkCommentAccess('Admin user has search permission and no access comments permission, but comments should be indexed because admin user inherits authenticated user\'s permission to access comments', TRUE); - - // Verify that search content permission is inherited from the authenticated - // role. - $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, TRUE); - $this->setRolePermissions($this->admin_role, TRUE, FALSE); - $this->checkCommentAccess('Admin user has access comments permission and no search permission, but comments should be indexed because admin user inherits authenticated user\'s permission to search', TRUE); - - } - - /** - * Set permissions for role. - */ - function setRolePermissions($rid, $access_comments = FALSE, $search_content = TRUE) { - $permissions = array( - 'access comments' => $access_comments, - 'search content' => $search_content, - ); - user_role_change_permissions($rid, $permissions); - } - - /** - * Update search index and search for comment. - */ - function checkCommentAccess($message, $assume_access = FALSE) { - // Invoke search index update. - search_touch_node($this->node->nid); - $this->cronRun(); - - // Search for the comment subject. - $edit = array( - 'search_block_form' => "'" . $this->comment_subject . "'", - ); - $this->drupalPost('', $edit, t('Search')); - $method = $assume_access ? 'assertText' : 'assertNoText'; - $verb = $assume_access ? 'found' : 'not found'; - $this->{$method}($this->node->title, "Node $verb in search results: " . $message); - $this->{$method}($this->comment_subject, "Comment subject $verb in search results: " . $message); - } - - /** - * Verify that 'add new comment' does not appear in search results or index. - */ - function testAddNewComment() { - // Create a node with a short body. - $settings = array( - 'type' => 'article', - 'title' => 'short title', - 'body' => array(LANGUAGE_NONE => array(array('value' => 'short body text'))), - ); - - $user = $this->drupalCreateUser(array('search content', 'create article content', 'access content')); - $this->drupalLogin($user); - - $node = $this->drupalCreateNode($settings); - // Verify that if you view the node on its own page, 'add new comment' - // is there. - $this->drupalGet('node/' . $node->nid); - $this->assertText(t('Add new comment'), t('Add new comment appears on node page')); - - // Run cron to index this page. - $this->drupalLogout(); - $this->cronRun(); - - // Search for 'comment'. Should be no results. - $this->drupalLogin($user); - $this->drupalPost('search/node', array('keys' => 'comment'), t('Search')); - $this->assertText(t('Your search yielded no results'), t('No results searching for the word comment')); - - // Search for the node title. Should be found, and 'Add new comment' should - // not be part of the search snippet. - $this->drupalPost('search/node', array('keys' => 'short'), t('Search')); - $this->assertText($node->title, t('Search for keyword worked')); - $this->assertNoText(t('Add new comment'), t('Add new comment does not appear on search results page')); - } - -} - -/** - * Tests search_expression_insert() and search_expression_extract(). - * - * @see http://drupal.org/node/419388 (issue) - */ -class SearchExpressionInsertExtractTestCase extends DrupalUnitTestCase { - public static function getInfo() { - return array( - 'name' => 'Search expression insert/extract', - 'description' => 'Tests the functions search_expression_insert() and search_expression_extract()', - 'group' => 'Search', - ); - } - - function setUp() { - drupal_load('module', 'search'); - parent::setUp(); - } - - /** - * Tests search_expression_insert() and search_expression_extract(). - */ - function testInsertExtract() { - $base_expression = "mykeyword"; - // Build an array of option, value, what should be in the expression, what - // should be retrieved from expression. - $cases = array( - array('foo', 'bar', 'foo:bar', 'bar'), // Normal case. - array('foo', NULL, '', NULL), // Empty value: shouldn't insert. - array('foo', ' ', 'foo:', ''), // Space as value: should insert but retrieve empty string. - array('foo', '', 'foo:', ''), // Empty string as value: should insert but retrieve empty string. - array('foo', '0', 'foo:0', '0'), // String zero as value: should insert. - array('foo', 0, 'foo:0', '0'), // Numeric zero as value: should insert. - ); - - foreach ($cases as $index => $case) { - $after_insert = search_expression_insert($base_expression, $case[0], $case[1]); - if (empty($case[2])) { - $this->assertEqual($after_insert, $base_expression, "Empty insert does not change expression in case $index"); - } - else { - $this->assertEqual($after_insert, $base_expression . ' ' . $case[2], "Insert added correct expression for case $index"); - } - - $retrieved = search_expression_extract($after_insert, $case[0]); - if (!isset($case[3])) { - $this->assertFalse(isset($retrieved), "Empty retrieval results in unset value in case $index"); - } - else { - $this->assertEqual($retrieved, $case[3], "Value is retrieved for case $index"); - } - - $after_clear = search_expression_insert($after_insert, $case[0]); - $this->assertEqual(trim($after_clear), $base_expression, "After clearing, base expression is restored for case $index"); - - $cleared = search_expression_extract($after_clear, $case[0]); - $this->assertFalse(isset($cleared), "After clearing, value could not be retrieved for case $index"); - } - } -} - -/** - * Tests that comment count display toggles properly on comment status of node - * - * Issue 537278 - * - * - Nodes with comment status set to Open should always how comment counts - * - Nodes with comment status set to Closed should show comment counts - * only when there are comments - * - Nodes with comment status set to Hidden should never show comment counts - */ -class SearchCommentCountToggleTestCase extends DrupalWebTestCase { - protected $searching_user; - protected $searchable_nodes; - - public static function getInfo() { - return array( - 'name' => 'Comment count toggle', - 'description' => 'Verify that comment count display toggles properly on comment status of node.', - 'group' => 'Search', - ); - } - - function setUp() { - parent::setUp('search'); - - // Create searching user. - $this->searching_user = $this->drupalCreateUser(array('search content', 'access content', 'access comments', 'skip comment approval')); - - // Create initial nodes. - $node_params = array('type' => 'article', 'body' => array(LANGUAGE_NONE => array(array('value' => 'SearchCommentToggleTestCase')))); - - $this->searchable_nodes['1 comment'] = $this->drupalCreateNode($node_params); - $this->searchable_nodes['0 comments'] = $this->drupalCreateNode($node_params); - - // Login with sufficient privileges. - $this->drupalLogin($this->searching_user); - - // Create a comment array - $edit_comment = array(); - $edit_comment['subject'] = $this->randomName(); - $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = $this->randomName(); - $filtered_html_format_id = 'filtered_html'; - $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $filtered_html_format_id; - - // Post comment to the test node with comment - $this->drupalPost('comment/reply/' . $this->searchable_nodes['1 comment']->nid, $edit_comment, t('Save')); - - // First update the index. This does the initial processing. - node_update_index(); - - // Then, run the shutdown function. Testing is a unique case where indexing - // and searching has to happen in the same request, so running the shutdown - // function manually is needed to finish the indexing process. - search_update_totals(); - } - - /** - * Verify that comment count display toggles properly on comment status of node - */ - function testSearchCommentCountToggle() { - // Search for the nodes by string in the node body. - $edit = array( - 'search_block_form' => "'SearchCommentToggleTestCase'", - ); - - // Test comment count display for nodes with comment status set to Open - $this->drupalPost('', $edit, t('Search')); - $this->assertText(t('0 comments'), t('Empty comment count displays for nodes with comment status set to Open')); - $this->assertText(t('1 comment'), t('Non-empty comment count displays for nodes with comment status set to Open')); - - // Test comment count display for nodes with comment status set to Closed - $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_CLOSED; - node_save($this->searchable_nodes['0 comments']); - $this->searchable_nodes['1 comment']->comment = COMMENT_NODE_CLOSED; - node_save($this->searchable_nodes['1 comment']); - - $this->drupalPost('', $edit, t('Search')); - $this->assertNoText(t('0 comments'), t('Empty comment count does not display for nodes with comment status set to Closed')); - $this->assertText(t('1 comment'), t('Non-empty comment count displays for nodes with comment status set to Closed')); - - // Test comment count display for nodes with comment status set to Hidden - $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_HIDDEN; - node_save($this->searchable_nodes['0 comments']); - $this->searchable_nodes['1 comment']->comment = COMMENT_NODE_HIDDEN; - node_save($this->searchable_nodes['1 comment']); - - $this->drupalPost('', $edit, t('Search')); - $this->assertNoText(t('0 comments'), t('Empty comment count does not display for nodes with comment status set to Hidden')); - $this->assertNoText(t('1 comment'), t('Non-empty comment count does not display for nodes with comment status set to Hidden')); - } -} - -/** - * Test search_simplify() on every Unicode character, and some other cases. - */ -class SearchSimplifyTestCase extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'Search simplify', - 'description' => 'Check that the search_simply() function works as intended.', - 'group' => 'Search', - ); - } - - /** - * Tests that all Unicode characters simplify correctly. - */ - function testSearchSimplifyUnicode() { - // This test uses a file that was constructed so that the even lines are - // boundary characters, and the odd lines are valid word characters. (It - // was generated as a sequence of all the Unicode characters, and then the - // boundary chararacters (punctuation, spaces, etc.) were split off into - // their own lines). So the even-numbered lines should simplify to nothing, - // and the odd-numbered lines we need to split into shorter chunks and - // verify that simplification doesn't lose any characters. - $input = file_get_contents(DRUPAL_ROOT . '/modules/search/tests/UnicodeTest.txt'); - $basestrings = explode(chr(10), $input); - $strings = array(); - foreach ($basestrings as $key => $string) { - if ($key %2) { - // Even line - should simplify down to a space. - $simplified = search_simplify($string); - $this->assertIdentical($simplified, ' ', "Line $key is excluded from the index"); - } - else { - // Odd line, should be word characters. - // Split this into 30-character chunks, so we don't run into limits - // of truncation in search_simplify(). - $start = 0; - while ($start < drupal_strlen($string)) { - $newstr = drupal_substr($string, $start, 30); - // Special case: leading zeros are removed from numeric strings, - // and there's one string in this file that is numbers starting with - // zero, so prepend a 1 on that string. - if (preg_match('/^[0-9]+$/', $newstr)) { - $newstr = '1' . $newstr; - } - $strings[] = $newstr; - $start += 30; - } - } - } - foreach ($strings as $key => $string) { - $simplified = search_simplify($string); - $this->assertTrue(drupal_strlen($simplified) >= drupal_strlen($string), "Nothing is removed from string $key."); - } - - // Test the low-numbered ASCII control characters separately. They are not - // in the text file because they are problematic for diff, especially \0. - $string = ''; - for ($i = 0; $i < 32; $i++) { - $string .= chr($i); - } - $this->assertIdentical(' ', search_simplify($string), t('Search simplify works for ASCII control characters.')); - } - - /** - * Tests that search_simplify() does the right thing with punctuation. - */ - function testSearchSimplifyPunctuation() { - $cases = array( - array('20.03/94-28,876', '20039428876', 'Punctuation removed from numbers'), - array('great...drupal--module', 'great drupal module', 'Multiple dot and dashes are word boundaries'), - array('very_great-drupal.module', 'verygreatdrupalmodule', 'Single dot, dash, underscore are removed'), - array('regular,punctuation;word', 'regular punctuation word', 'Punctuation is a word boundary'), - ); - - foreach ($cases as $case) { - $out = trim(search_simplify($case[0])); - $this->assertEqual($out, $case[1], $case[2]); - } - } -} - - -/** - * Tests keywords and conditions. - */ -class SearchKeywordsConditions extends DrupalWebTestCase { - - public static function getInfo() { - return array( - 'name' => 'Keywords and conditions', - 'description' => 'Verify the search pulls in keywords and extra conditions.', - 'group' => 'Search', - ); - } - - function setUp() { - parent::setUp('search', 'search_extra_type'); - // Create searching user. - $this->searching_user = $this->drupalCreateUser(array('search content', 'access content', 'access comments', 'skip comment approval')); - // Login with sufficient privileges. - $this->drupalLogin($this->searching_user); - // Test with all search modules enabled. - variable_set('search_active_modules', array('node' => 'node', 'user' => 'user', 'search_extra_type' => 'search_extra_type')); - menu_rebuild(); - } - - /** - * Verify the kewords are captured and conditions respected. - */ - function testSearchKeyswordsConditions() { - // No keys, not conditions - no results. - $this->drupalGet('search/dummy_path'); - $this->assertNoText('Dummy search snippet to display'); - // With keys - get results. - $keys = 'bike shed ' . $this->randomName(); - $this->drupalGet("search/dummy_path/{$keys}"); - $this->assertText("Dummy search snippet to display. Keywords: {$keys}"); - $keys = 'blue drop ' . $this->randomName(); - $this->drupalGet("search/dummy_path", array('query' => array('keys' => $keys))); - $this->assertText("Dummy search snippet to display. Keywords: {$keys}"); - // Add some conditions and keys. - $keys = 'moving drop ' . $this->randomName(); - $this->drupalGet("search/dummy_path/bike", array('query' => array('search_conditions' => $keys))); - $this->assertText("Dummy search snippet to display."); - $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE)); - // Add some conditions and no keys. - $keys = 'drop kick ' . $this->randomName(); - $this->drupalGet("search/dummy_path", array('query' => array('search_conditions' => $keys))); - $this->assertText("Dummy search snippet to display."); - $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE)); - } -} - -/** - * Tests that numbers can be searched. - */ -class SearchNumbersTestCase extends DrupalWebTestCase { - protected $test_user; - protected $numbers; - protected $nodes; - - public static function getInfo() { - return array( - 'name' => 'Search numbers', - 'description' => 'Check that numbers can be searched', - 'group' => 'Search', - ); - } - - function setUp() { - parent::setUp('search'); - - $this->test_user = $this->drupalCreateUser(array('search content', 'access content', 'administer nodes', 'access site reports')); - $this->drupalLogin($this->test_user); - - // Create content with various numbers in it. - // Note: 50 characters is the current limit of the search index's word - // field. - $this->numbers = array( - 'ISBN' => '978-0446365383', - 'UPC' => '036000 291452', - 'EAN bar code' => '5901234123457', - 'negative' => '-123456.7890', - 'quoted negative' => '"-123456.7890"', - 'leading zero' => '0777777777', - 'tiny' => '111', - 'small' => '22222222222222', - 'medium' => '333333333333333333333333333', - 'large' => '444444444444444444444444444444444444444', - 'gigantic' => '5555555555555555555555555555555555555555555555555', - 'over fifty characters' => '666666666666666666666666666666666666666666666666666666666666', - 'date', '01/02/2009', - 'commas', '987,654,321', - ); - - foreach ($this->numbers as $doc => $num) { - $info = array( - 'body' => array(LANGUAGE_NONE => array(array('value' => $num))), - 'type' => 'page', - 'language' => LANGUAGE_NONE, - 'title' => $doc . ' number', - ); - $this->nodes[$doc] = $this->drupalCreateNode($info); - } - - // Run cron to ensure the content is indexed. - $this->cronRun(); - $this->drupalGet('admin/reports/dblog'); - $this->assertText(t('Cron run completed'), 'Log shows cron run completed'); - } - - /** - * Tests that all the numbers can be searched. - */ - function testNumberSearching() { - $types = array_keys($this->numbers); - - foreach ($types as $type) { - $number = $this->numbers[$type]; - // If the number is negative, remove the - sign, because - indicates - // "not keyword" when searching. - $number = ltrim($number, '-'); - $node = $this->nodes[$type]; - - // Verify that the node title does not appear on the search page - // with a dummy search. - $this->drupalPost('search/node', - array('keys' => 'foo'), - t('Search')); - $this->assertNoText($node->title, $type . ': node title not shown in dummy search'); - - // Verify that the node title does appear as a link on the search page - // when searching for the number. - $this->drupalPost('search/node', - array('keys' => $number), - t('Search')); - $this->assertText($node->title, $type . ': node title shown (search found the node) in search for number ' . $number); - } - } -} - -/** - * Tests that numbers can be searched, with more complex matching. - */ -class SearchNumberMatchingTestCase extends DrupalWebTestCase { - protected $test_user; - protected $numbers; - protected $nodes; - - public static function getInfo() { - return array( - 'name' => 'Search number matching', - 'description' => 'Check that numbers can be searched with more complex matching', - 'group' => 'Search', - ); - } - - function setUp() { - parent::setUp('search'); - - $this->test_user = $this->drupalCreateUser(array('search content', 'access content', 'administer nodes', 'access site reports')); - $this->drupalLogin($this->test_user); - - // Define a group of numbers that should all match each other -- - // numbers with internal punctuation should match each other, as well - // as numbers with and without leading zeros and leading/trailing - // . and -. - $this->numbers = array( - '123456789', - '12/34/56789', - '12.3456789', - '12-34-56789', - '123,456,789', - '-123456789', - '0123456789', - ); - - foreach ($this->numbers as $num) { - $info = array( - 'body' => array(LANGUAGE_NONE => array(array('value' => $num))), - 'type' => 'page', - 'language' => LANGUAGE_NONE, - ); - $this->nodes[] = $this->drupalCreateNode($info); - } - - // Run cron to ensure the content is indexed. - $this->cronRun(); - $this->drupalGet('admin/reports/dblog'); - $this->assertText(t('Cron run completed'), 'Log shows cron run completed'); - } - - /** - * Tests that all the numbers can be searched. - */ - function testNumberSearching() { - for ($i = 0; $i < count($this->numbers); $i++) { - $node = $this->nodes[$i]; - - // Verify that the node title does not appear on the search page - // with a dummy search. - $this->drupalPost('search/node', - array('keys' => 'foo'), - t('Search')); - $this->assertNoText($node->title, $i . ': node title not shown in dummy search'); - - // Now verify that we can find node i by searching for any of the - // numbers. - for ($j = 0; $j < count($this->numbers); $j++) { - $number = $this->numbers[$j]; - // If the number is negative, remove the - sign, because - indicates - // "not keyword" when searching. - $number = ltrim($number, '-'); - - $this->drupalPost('search/node', - array('keys' => $number), - t('Search')); - $this->assertText($node->title, $i . ': node title shown (search found the node) in search for number ' . $number); - } - } - - } -} - -/** - * Test config page. - */ -class SearchConfigSettingsForm extends DrupalWebTestCase { - public $search_user; - public $search_node; - - public static function getInfo() { - return array( - 'name' => 'Config settings form', - 'description' => 'Verify the search config settings form.', - 'group' => 'Search', - ); - } - - function setUp() { - parent::setUp('search', 'search_extra_type'); - - // Login as a user that can create and search content. - $this->search_user = $this->drupalCreateUser(array('search content', 'administer search', 'administer nodes', 'bypass node access', 'access user profiles', 'administer users', 'administer blocks')); - $this->drupalLogin($this->search_user); - - // Add a single piece of content and index it. - $node = $this->drupalCreateNode(); - $this->search_node = $node; - // Link the node to itself to test that it's only indexed once. The content - // also needs the word "pizza" so we can use it as the search keyword. - $langcode = LANGUAGE_NONE; - $body_key = "body[$langcode][0][value]"; - $edit[$body_key] = l($node->title, 'node/' . $node->nid) . ' pizza sandwich'; - $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); - - node_update_index(); - search_update_totals(); - - // Enable the search block. - $edit = array(); - $edit['blocks[search_form][region]'] = 'content'; - $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); - } - - /** - * Verify the search settings form. - */ - function testSearchSettingsPage() { - - // Test that the settings form displays the correct count of items left to index. - $this->drupalGet('admin/config/search/settings'); - $this->assertText(t('There are @count items left to index.', array('@count' => 0))); - - // Test the re-index button. - $this->drupalPost('admin/config/search/settings', array(), t('Re-index site')); - $this->assertText(t('Are you sure you want to re-index the site')); - $this->drupalPost('admin/config/search/settings/reindex', array(), t('Re-index site')); - $this->assertText(t('The index will be rebuilt')); - $this->drupalGet('admin/config/search/settings'); - $this->assertText(t('There is 1 item left to index.')); - } - - /** - * Verify that you can disable individual search modules. - */ - function testSearchModuleDisabling() { - // Array of search modules to test: 'path' is the search path, 'title' is - // the tab title, 'keys' are the keywords to search for, and 'text' is - // the text to assert is on the results page. - $module_info = array( - 'node' => array( - 'path' => 'node', - 'title' => 'Content', - 'keys' => 'pizza', - 'text' => $this->search_node->title, - ), - 'user' => array( - 'path' => 'user', - 'title' => 'User', - 'keys' => $this->search_user->name, - 'text' => $this->search_user->mail, - ), - 'search_extra_type' => array( - 'path' => 'dummy_path', - 'title' => 'Dummy search type', - 'keys' => 'foo', - 'text' => 'Dummy search snippet to display', - ), - ); - $modules = array_keys($module_info); - - // Test each module if it's enabled as the only search module. - foreach ($modules as $module) { - // Enable the one module and disable other ones. - $info = $module_info[$module]; - $edit = array(); - foreach ($modules as $other) { - $edit['search_active_modules[' . $other . ']'] = (($other == $module) ? $module : FALSE); - } - $edit['search_default_module'] = $module; - $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration')); - - // Run a search from the correct search URL. - $this->drupalGet('search/' . $info['path'] . '/' . $info['keys']); - $this->assertNoText('no results', $info['title'] . ' search found results'); - $this->assertText($info['text'], 'Correct search text found'); - - // Verify that other module search tab titles are not visible. - foreach ($modules as $other) { - if ($other != $module) { - $title = $module_info[$other]['title']; - $this->assertNoText($title, $title . ' search tab is not shown'); - } - } - - // Run a search from the search block on the node page. Verify you get - // to this module's search results page. - $terms = array('search_block_form' => $info['keys']); - $this->drupalPost('node', $terms, t('Search')); - $this->assertEqual( - $this->getURL(), - url('search/' . $info['path'] . '/' . $info['keys'], array('absolute' => TRUE)), - 'Block redirected to right search page'); - - // Try an invalid search path. Should redirect to our active module. - $this->drupalGet('search/not_a_module_path'); - $this->assertEqual( - $this->getURL(), - url('search/' . $info['path'], array('absolute' => TRUE)), - 'Invalid search path redirected to default search page'); - } - - // Test with all search modules enabled. When you go to the search - // page or run search, all modules should be shown. - $edit = array(); - foreach ($modules as $module) { - $edit['search_active_modules[' . $module . ']'] = $module; - } - $edit['search_default_module'] = 'node'; - - $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration')); - - foreach (array('search/node/pizza', 'search/node') as $path) { - $this->drupalGet($path); - foreach ($modules as $module) { - $title = $module_info[$module]['title']; - $this->assertText($title, $title . ' search tab is shown'); - } - } - } -} - -/** - * Tests the search_excerpt() function. - */ -class SearchExcerptTestCase extends DrupalUnitTestCase { - public static function getInfo() { - return array( - 'name' => 'Search excerpt extraction', - 'description' => 'Tests that the search_excerpt() function works.', - 'group' => 'Search', - ); - } - - function setUp() { - drupal_load('module', 'search'); - parent::setUp(); - } - - /** - * Tests search_excerpt() with several simulated search keywords. - * - * Passes keywords and a sample marked up string, "The quick - * brown fox jumps over the lazy dog", and compares it to the - * correctly marked up string. The correctly marked up string - * contains either highlighted keywords or the original marked - * up string if no keywords matched the string. - */ - function testSearchExcerpt() { - // Make some text with entities and tags. - $text = 'The quick brown fox & jumps

    over

    the lazy dog'; - // Note: The search_excerpt() function adds some extra spaces -- not - // important for HTML formatting. Remove these for comparison. - $expected = 'The quick brown fox & jumps over the lazy dog'; - $result = preg_replace('| +|', ' ', search_excerpt('nothing', $text)); - $this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Entire string is returned when keyword is not found in short string'); - - $result = preg_replace('| +|', ' ', search_excerpt('fox', $text)); - $this->assertEqual($result, 'The quick brown fox & jumps over the lazy dog ...', 'Found keyword is highlighted'); - - $longtext = str_repeat($text . ' ', 10); - $result = preg_replace('| +|', ' ', search_excerpt('nothing', $text)); - $this->assertTrue(strpos($result, $expected) === 0, 'When keyword is not found in long string, return value starts as expected'); - - $entities = str_repeat('készítése ', 20); - $result = preg_replace('| +|', ' ', search_excerpt('nothing', $entities)); - $this->assertFalse(strpos($result, '&'), 'Entities are not present in excerpt'); - $this->assertTrue(strpos($result, 'í') > 0, 'Entities are converted in excerpt'); - } - - /** - * Tests search_excerpt() with search keywords matching simplified words. - * - * Excerpting should handle keywords that are matched only after going through - * search_simplify(). This test passes keywords that match simplified words - * and compares them with strings that contain the original unsimplified word. - */ - function testSearchExcerptSimplified() { - $lorem1 = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae arcu at leo cursus laoreet. Curabitur dui tortor, adipiscing malesuada tempor in, bibendum ac diam. Cras non tellus a libero pellentesque condimentum. What is a Drupalism? Suspendisse ac lacus libero. Ut non est vel nisl faucibus interdum nec sed leo. Pellentesque sem risus, vulputate eu semper eget, auctor in libero.'; - $lorem2 = 'Ut fermentum est vitae metus convallis scelerisque. Phasellus pellentesque rhoncus tellus, eu dignissim purus posuere id. Quisque eu fringilla ligula. Morbi ullamcorper, lorem et mattis egestas, tortor neque pretium velit, eget eleifend odio turpis eu purus. Donec vitae metus quis leo pretium tincidunt a pulvinar sem. Morbi adipiscing laoreet mauris vel placerat. Nullam elementum, nisl sit amet scelerisque malesuada, dolor nunc hendrerit quam, eu ultrices erat est in orci.'; - - // Make some text with some keywords that will get simplified. - $text = $lorem1 . ' Number: 123456.7890 Hyphenated: one-two abc,def ' . $lorem2; - // Note: The search_excerpt() function adds some extra spaces -- not - // important for HTML formatting. Remove these for comparison. - $result = preg_replace('| +|', ' ', search_excerpt('123456.7890', $text)); - $this->assertTrue(strpos($result, 'Number: 123456.7890') !== FALSE, 'Numeric keyword is highlighted with exact match'); - - $result = preg_replace('| +|', ' ', search_excerpt('1234567890', $text)); - $this->assertTrue(strpos($result, 'Number: 123456.7890') !== FALSE, 'Numeric keyword is highlighted with simplified match'); - - $result = preg_replace('| +|', ' ', search_excerpt('Number 1234567890', $text)); - $this->assertTrue(strpos($result, 'Number: 123456.7890') !== FALSE, 'Punctuated and numeric keyword is highlighted with simplified match'); - - $result = preg_replace('| +|', ' ', search_excerpt('"Number 1234567890"', $text)); - $this->assertTrue(strpos($result, 'Number: 123456.7890') !== FALSE, 'Phrase with punctuated and numeric keyword is highlighted with simplified match'); - - $result = preg_replace('| +|', ' ', search_excerpt('"Hyphenated onetwo"', $text)); - $this->assertTrue(strpos($result, 'Hyphenated: one-two') !== FALSE, 'Phrase with punctuated and hyphenated keyword is highlighted with simplified match'); - - $result = preg_replace('| +|', ' ', search_excerpt('"abc def"', $text)); - $this->assertTrue(strpos($result, 'abc,def') !== FALSE, 'Phrase with keyword simplified into two separate words is highlighted with simplified match'); - } -} - -/** - * Test the CJK tokenizer. - */ -class SearchTokenizerTestCase extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'CJK tokenizer', - 'description' => 'Check that CJK tokenizer works as intended.', - 'group' => 'Search', - ); - } - - function setUp() { - parent::setUp('search'); - } - - /** - * Verifies that strings of CJK characters are tokenized. - * - * The search_simplify() function does special things with numbers, symbols, - * and punctuation. So we only test that CJK characters that are not in these - * character classes are tokenized properly. See PREG_CLASS_CKJ for more - * information. - */ - function testTokenizer() { - // Set the minimum word size to 1 (to split all CJK characters) and make - // sure CJK tokenizing is turned on. - variable_set('minimum_word_size', 1); - variable_set('overlap_cjk', TRUE); - $this->refreshVariables(); - - // Create a string of CJK characters from various character ranges in - // the Unicode tables. - - // Beginnings of the character ranges. - $starts = array( - 'CJK unified' => 0x4e00, - 'CJK Ext A' => 0x3400, - 'CJK Compat' => 0xf900, - 'Hangul Jamo' => 0x1100, - 'Hangul Ext A' => 0xa960, - 'Hangul Ext B' => 0xd7b0, - 'Hangul Compat' => 0x3131, - 'Half non-punct 1' => 0xff21, - 'Half non-punct 2' => 0xff41, - 'Half non-punct 3' => 0xff66, - 'Hangul Syllables' => 0xac00, - 'Hiragana' => 0x3040, - 'Katakana' => 0x30a1, - 'Katakana Ext' => 0x31f0, - 'CJK Reserve 1' => 0x20000, - 'CJK Reserve 2' => 0x30000, - 'Bomofo' => 0x3100, - 'Bomofo Ext' => 0x31a0, - 'Lisu' => 0xa4d0, - 'Yi' => 0xa000, - ); - - // Ends of the character ranges. - $ends = array( - 'CJK unified' => 0x9fcf, - 'CJK Ext A' => 0x4dbf, - 'CJK Compat' => 0xfaff, - 'Hangul Jamo' => 0x11ff, - 'Hangul Ext A' => 0xa97f, - 'Hangul Ext B' => 0xd7ff, - 'Hangul Compat' => 0x318e, - 'Half non-punct 1' => 0xff3a, - 'Half non-punct 2' => 0xff5a, - 'Half non-punct 3' => 0xffdc, - 'Hangul Syllables' => 0xd7af, - 'Hiragana' => 0x309f, - 'Katakana' => 0x30ff, - 'Katakana Ext' => 0x31ff, - 'CJK Reserve 1' => 0x2fffd, - 'CJK Reserve 2' => 0x3fffd, - 'Bomofo' => 0x312f, - 'Bomofo Ext' => 0x31b7, - 'Lisu' => 0xa4fd, - 'Yi' => 0xa48f, - ); - - // Generate characters consisting of starts, midpoints, and ends. - $chars = array(); - $charcodes = array(); - foreach ($starts as $key => $value) { - $charcodes[] = $starts[$key]; - $chars[] = $this->code2utf($starts[$key]); - $mid = round(0.5 * ($starts[$key] + $ends[$key])); - $charcodes[] = $mid; - $chars[] = $this->code2utf($mid); - $charcodes[] = $ends[$key]; - $chars[] = $this->code2utf($ends[$key]); - } - - // Merge into a string and tokenize. - $string = implode('', $chars); - $out = trim(search_simplify($string)); - $expected = drupal_strtolower(implode(' ', $chars)); - - // Verify that the output matches what we expect. - $this->assertEqual($out, $expected, 'CJK tokenizer worked on all supplied CJK characters'); - } - - /** - * Verifies that strings of non-CJK characters are not tokenized. - * - * This is just a sanity check - it verifies that strings of letters are - * not tokenized. - */ - function testNoTokenizer() { - // Set the minimum word size to 1 (to split all CJK characters) and make - // sure CJK tokenizing is turned on. - variable_set('minimum_word_size', 1); - variable_set('overlap_cjk', TRUE); - $this->refreshVariables(); - - $letters = 'abcdefghijklmnopqrstuvwxyz'; - $out = trim(search_simplify($letters)); - - $this->assertEqual($letters, $out, 'Letters are not CJK tokenized'); - } - - /** - * Like PHP chr() function, but for unicode characters. - * - * chr() only works for ASCII characters up to character 255. This function - * converts a number to the corresponding unicode character. Adapted from - * functions supplied in comments on several functions on php.net. - */ - function code2utf($num) { - if ($num < 128) { - return chr($num); - } - - if ($num < 2048) { - return chr(($num >> 6) + 192) . chr(($num & 63) + 128); - } - - if ($num < 65536) { - return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128); - } - - if ($num < 2097152) { - return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128); - } - - return ''; - } -} - -/** - * Tests that we can embed a form in search results and submit it. - */ -class SearchEmbedForm extends DrupalWebTestCase { - /** - * Node used for testing. - */ - public $node; - - /** - * Count of how many times the form has been submitted. - */ - public $submit_count = 0; - - public static function getInfo() { - return array( - 'name' => 'Embedded forms', - 'description' => 'Verifies that a form embedded in search results works', - 'group' => 'Search', - ); - } - - function setUp() { - parent::setUp('search', 'search_embedded_form'); - - // Create a user and a node, and update the search index. - $test_user = $this->drupalCreateUser(array('access content', 'search content', 'administer nodes')); - $this->drupalLogin($test_user); - - $this->node = $this->drupalCreateNode(); - - node_update_index(); - search_update_totals(); - - // Set up a dummy initial count of times the form has been submitted. - $this->submit_count = 12; - variable_set('search_embedded_form_submitted', $this->submit_count); - $this->refreshVariables(); - } - - /** - * Tests that the embedded form appears and can be submitted. - */ - function testEmbeddedForm() { - // First verify we can submit the form from the module's page. - $this->drupalPost('search_embedded_form', - array('name' => 'John'), - t('Send away')); - $this->assertText(t('Test form was submitted'), 'Form message appears'); - $count = variable_get('search_embedded_form_submitted', 0); - $this->assertEqual($this->submit_count + 1, $count, 'Form submission count is correct'); - $this->submit_count = $count; - - // Now verify that we can see and submit the form from the search results. - $this->drupalGet('search/node/' . $this->node->title); - $this->assertText(t('Your name'), 'Form is visible'); - $this->drupalPost('search/node/' . $this->node->title, - array('name' => 'John'), - t('Send away')); - $this->assertText(t('Test form was submitted'), 'Form message appears'); - $count = variable_get('search_embedded_form_submitted', 0); - $this->assertEqual($this->submit_count + 1, $count, 'Form submission count is correct'); - $this->submit_count = $count; - - // Now verify that if we submit the search form, it doesn't count as - // our form being submitted. - $this->drupalPost('search', - array('keys' => 'foo'), - t('Search')); - $this->assertNoText(t('Test form was submitted'), 'Form message does not appear'); - $count = variable_get('search_embedded_form_submitted', 0); - $this->assertEqual($this->submit_count, $count, 'Form submission count is correct'); - $this->submit_count = $count; - } -} - -/** - * Tests that hook_search_page runs. - */ -class SearchPageOverride extends DrupalWebTestCase { - public $search_user; - - public static function getInfo() { - return array( - 'name' => 'Search page override', - 'description' => 'Verify that hook_search_page can override search page display.', - 'group' => 'Search', - ); - } - - function setUp() { - parent::setUp('search', 'search_extra_type'); - - // Login as a user that can create and search content. - $this->search_user = $this->drupalCreateUser(array('search content', 'administer search')); - $this->drupalLogin($this->search_user); - - // Enable the extra type module for searching. - variable_set('search_active_modules', array('node' => 'node', 'user' => 'user', 'search_extra_type' => 'search_extra_type')); - menu_rebuild(); - } - - function testSearchPageHook() { - $keys = 'bike shed ' . $this->randomName(); - $this->drupalGet("search/dummy_path/{$keys}"); - $this->assertText('Dummy search snippet', 'Dummy search snippet is shown'); - $this->assertText('Test page text is here', 'Page override is working'); - } -} - -/** - * Test node search with multiple languages. - */ -class SearchLanguageTestCase extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'Search language selection', - 'description' => 'Tests advanced search with different languages enabled.', - 'group' => 'Search', - ); - } - - /** - * Implementation setUp(). - */ - function setUp() { - parent::setUp('search', 'locale'); - - // Create and login user. - $test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'administer nodes', 'administer languages', 'access administration pages')); - $this->drupalLogin($test_user); - } - - function testLanguages() { - // Check that there are initially no languages displayed. - $this->drupalGet('search/node'); - $this->assertNoText(t('Languages'), t('No languages to choose from.')); - - // Add predefined language. - $edit = array('langcode' => 'fr'); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); - $this->assertText('fr', t('Language added successfully.')); - - // Now we should have languages displayed. - $this->drupalGet('search/node'); - $this->assertText(t('Languages'), t('Languages displayed to choose from.')); - $this->assertText(t('English'), t('English is a possible choice.')); - $this->assertText(t('French'), t('French is a possible choice.')); - - // Ensure selecting no language does not make the query different. - $this->drupalPost('search/node', array(), t('Advanced search')); - $this->assertEqual($this->getUrl(), url('search/node/', array('absolute' => TRUE)), t('Correct page redirection, no language filtering.')); - - // Pick French and ensure it is selected. - $edit = array('language[fr]' => TRUE); - $this->drupalPost('search/node', $edit, t('Advanced search')); - $this->assertFieldByXPath('//input[@name="keys"]', 'language:fr', t('Language filter added to query.')); - - // Change the default language and disable English. - $path = 'admin/config/regional/language'; - $this->drupalGet($path); - $this->assertFieldChecked('edit-site-default-en', t('English is the default language.')); - $edit = array('site_default' => 'fr'); - $this->drupalPost(NULL, $edit, t('Save configuration')); - $this->assertNoFieldChecked('edit-site-default-en', t('Default language updated.')); - $edit = array('enabled[en]' => FALSE); - $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration')); - $this->assertNoFieldChecked('edit-enabled-en', t('Language disabled.')); - - // Check that there are again no languages displayed. - $this->drupalGet('search/node'); - $this->assertNoText(t('Languages'), t('No languages to choose from.')); - } -} diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php deleted file mode 100644 index b60c682..0000000 --- a/modules/simpletest/drupal_web_test_case.php +++ /dev/null @@ -1,3422 +0,0 @@ - 0, - '#fail' => 0, - '#exception' => 0, - '#debug' => 0, - ); - - /** - * Assertions thrown in that test case. - * - * @var Array - */ - protected $assertions = array(); - - /** - * This class is skipped when looking for the source of an assertion. - * - * When displaying which function an assert comes from, it's not too useful - * to see "drupalWebTestCase->drupalLogin()', we would like to see the test - * that called it. So we need to skip the classes defining these helper - * methods. - */ - protected $skipClasses = array(__CLASS__ => TRUE); - - /** - * Constructor for DrupalTestCase. - * - * @param $test_id - * Tests with the same id are reported together. - */ - public function __construct($test_id = NULL) { - $this->testId = $test_id; - } - - /** - * Internal helper: stores the assert. - * - * @param $status - * Can be 'pass', 'fail', 'exception'. - * TRUE is a synonym for 'pass', FALSE for 'fail'. - * @param $message - * The message string. - * @param $group - * Which group this assert belongs to. - * @param $caller - * By default, the assert comes from a function whose name starts with - * 'test'. Instead, you can specify where this assert originates from - * by passing in an associative array as $caller. Key 'file' is - * the name of the source file, 'line' is the line number and 'function' - * is the caller function itself. - */ - protected function assert($status, $message = '', $group = 'Other', array $caller = NULL) { - // Convert boolean status to string status. - if (is_bool($status)) { - $status = $status ? 'pass' : 'fail'; - } - - // Increment summary result counter. - $this->results['#' . $status]++; - - // Get the function information about the call to the assertion method. - if (!$caller) { - $caller = $this->getAssertionCall(); - } - - // Creation assertion array that can be displayed while tests are running. - $this->assertions[] = $assertion = array( - 'test_id' => $this->testId, - 'test_class' => get_class($this), - 'status' => $status, - 'message' => $message, - 'message_group' => $group, - 'function' => $caller['function'], - 'line' => $caller['line'], - 'file' => $caller['file'], - ); - - // Store assertion for display after the test has completed. - Database::getConnection('default', 'simpletest_original_default') - ->insert('simpletest') - ->fields($assertion) - ->execute(); - - // We do not use a ternary operator here to allow a breakpoint on - // test failure. - if ($status == 'pass') { - return TRUE; - } - else { - return FALSE; - } - } - - /** - * Store an assertion from outside the testing context. - * - * This is useful for inserting assertions that can only be recorded after - * the test case has been destroyed, such as PHP fatal errors. The caller - * information is not automatically gathered since the caller is most likely - * inserting the assertion on behalf of other code. In all other respects - * the method behaves just like DrupalTestCase::assert() in terms of storing - * the assertion. - * - * @return - * Message ID of the stored assertion. - * - * @see DrupalTestCase::assert() - * @see DrupalTestCase::deleteAssert() - */ - public static function insertAssert($test_id, $test_class, $status, $message = '', $group = 'Other', array $caller = array()) { - // Convert boolean status to string status. - if (is_bool($status)) { - $status = $status ? 'pass' : 'fail'; - } - - $caller += array( - 'function' => t('Unknown'), - 'line' => 0, - 'file' => t('Unknown'), - ); - - $assertion = array( - 'test_id' => $test_id, - 'test_class' => $test_class, - 'status' => $status, - 'message' => $message, - 'message_group' => $group, - 'function' => $caller['function'], - 'line' => $caller['line'], - 'file' => $caller['file'], - ); - - return db_insert('simpletest') - ->fields($assertion) - ->execute(); - } - - /** - * Delete an assertion record by message ID. - * - * @param $message_id - * Message ID of the assertion to delete. - * @return - * TRUE if the assertion was deleted, FALSE otherwise. - * - * @see DrupalTestCase::insertAssert() - */ - public static function deleteAssert($message_id) { - return (bool) db_delete('simpletest') - ->condition('message_id', $message_id) - ->execute(); - } - - /** - * Cycles through backtrace until the first non-assertion method is found. - * - * @return - * Array representing the true caller. - */ - protected function getAssertionCall() { - $backtrace = debug_backtrace(); - - // The first element is the call. The second element is the caller. - // We skip calls that occurred in one of the methods of our base classes - // or in an assertion function. - while (($caller = $backtrace[1]) && - ((isset($caller['class']) && isset($this->skipClasses[$caller['class']])) || - substr($caller['function'], 0, 6) == 'assert')) { - // We remove that call. - array_shift($backtrace); - } - - return _drupal_get_last_caller($backtrace); - } - - /** - * Check to see if a value is not false (not an empty string, 0, NULL, or FALSE). - * - * @param $value - * The value on which the assertion is to be done. - * @param $message - * The message to display along with the assertion. - * @param $group - * The type of assertion - examples are "Browser", "PHP". - * @return - * TRUE if the assertion succeeded, FALSE otherwise. - */ - protected function assertTrue($value, $message = '', $group = 'Other') { - return $this->assert((bool) $value, $message ? $message : t('Value @value is TRUE.', array('@value' => var_export($value, TRUE))), $group); - } - - /** - * Check to see if a value is false (an empty string, 0, NULL, or FALSE). - * - * @param $value - * The value on which the assertion is to be done. - * @param $message - * The message to display along with the assertion. - * @param $group - * The type of assertion - examples are "Browser", "PHP". - * @return - * TRUE if the assertion succeeded, FALSE otherwise. - */ - protected function assertFalse($value, $message = '', $group = 'Other') { - return $this->assert(!$value, $message ? $message : t('Value @value is FALSE.', array('@value' => var_export($value, TRUE))), $group); - } - - /** - * Check to see if a value is NULL. - * - * @param $value - * The value on which the assertion is to be done. - * @param $message - * The message to display along with the assertion. - * @param $group - * The type of assertion - examples are "Browser", "PHP". - * @return - * TRUE if the assertion succeeded, FALSE otherwise. - */ - protected function assertNull($value, $message = '', $group = 'Other') { - return $this->assert(!isset($value), $message ? $message : t('Value @value is NULL.', array('@value' => var_export($value, TRUE))), $group); - } - - /** - * Check to see if a value is not NULL. - * - * @param $value - * The value on which the assertion is to be done. - * @param $message - * The message to display along with the assertion. - * @param $group - * The type of assertion - examples are "Browser", "PHP". - * @return - * TRUE if the assertion succeeded, FALSE otherwise. - */ - protected function assertNotNull($value, $message = '', $group = 'Other') { - return $this->assert(isset($value), $message ? $message : t('Value @value is not NULL.', array('@value' => var_export($value, TRUE))), $group); - } - - /** - * Check to see if two values are equal. - * - * @param $first - * The first value to check. - * @param $second - * The second value to check. - * @param $message - * The message to display along with the assertion. - * @param $group - * The type of assertion - examples are "Browser", "PHP". - * @return - * TRUE if the assertion succeeded, FALSE otherwise. - */ - protected function assertEqual($first, $second, $message = '', $group = 'Other') { - return $this->assert($first == $second, $message ? $message : t('Value @first is equal to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group); - } - - /** - * Check to see if two values are not equal. - * - * @param $first - * The first value to check. - * @param $second - * The second value to check. - * @param $message - * The message to display along with the assertion. - * @param $group - * The type of assertion - examples are "Browser", "PHP". - * @return - * TRUE if the assertion succeeded, FALSE otherwise. - */ - protected function assertNotEqual($first, $second, $message = '', $group = 'Other') { - return $this->assert($first != $second, $message ? $message : t('Value @first is not equal to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group); - } - - /** - * Check to see if two values are identical. - * - * @param $first - * The first value to check. - * @param $second - * The second value to check. - * @param $message - * The message to display along with the assertion. - * @param $group - * The type of assertion - examples are "Browser", "PHP". - * @return - * TRUE if the assertion succeeded, FALSE otherwise. - */ - protected function assertIdentical($first, $second, $message = '', $group = 'Other') { - return $this->assert($first === $second, $message ? $message : t('Value @first is identical to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group); - } - - /** - * Check to see if two values are not identical. - * - * @param $first - * The first value to check. - * @param $second - * The second value to check. - * @param $message - * The message to display along with the assertion. - * @param $group - * The type of assertion - examples are "Browser", "PHP". - * @return - * TRUE if the assertion succeeded, FALSE otherwise. - */ - protected function assertNotIdentical($first, $second, $message = '', $group = 'Other') { - return $this->assert($first !== $second, $message ? $message : t('Value @first is not identical to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group); - } - - /** - * Fire an assertion that is always positive. - * - * @param $message - * The message to display along with the assertion. - * @param $group - * The type of assertion - examples are "Browser", "PHP". - * @return - * TRUE. - */ - protected function pass($message = NULL, $group = 'Other') { - return $this->assert(TRUE, $message, $group); - } - - /** - * Fire an assertion that is always negative. - * - * @param $message - * The message to display along with the assertion. - * @param $group - * The type of assertion - examples are "Browser", "PHP". - * @return - * FALSE. - */ - protected function fail($message = NULL, $group = 'Other') { - return $this->assert(FALSE, $message, $group); - } - - /** - * Fire an error assertion. - * - * @param $message - * The message to display along with the assertion. - * @param $group - * The type of assertion - examples are "Browser", "PHP". - * @param $caller - * The caller of the error. - * @return - * FALSE. - */ - protected function error($message = '', $group = 'Other', array $caller = NULL) { - if ($group == 'User notice') { - // Since 'User notice' is set by trigger_error() which is used for debug - // set the message to a status of 'debug'. - return $this->assert('debug', $message, 'Debug', $caller); - } - - return $this->assert('exception', $message, $group, $caller); - } - - /** - * Logs verbose message in a text file. - * - * The a link to the vebose message will be placed in the test results via - * as a passing assertion with the text '[verbose message]'. - * - * @param $message - * The verbose message to be stored. - * - * @see simpletest_verbose() - */ - protected function verbose($message) { - if ($id = simpletest_verbose($message)) { - $url = file_create_url($this->originalFileDirectory . '/simpletest/verbose/' . get_class($this) . '-' . $id . '.html'); - $this->error(l(t('Verbose message'), $url, array('attributes' => array('target' => '_blank'))), 'User notice'); - } - } - - /** - * Run all tests in this class. - * - * Regardless of whether $methods are passed or not, only method names - * starting with "test" are executed. - * - * @param $methods - * (optional) A list of method names in the test case class to run; e.g., - * array('testFoo', 'testBar'). By default, all methods of the class are - * taken into account, but it can be useful to only run a few selected test - * methods during debugging. - */ - public function run(array $methods = array()) { - // Initialize verbose debugging. - simpletest_verbose(NULL, variable_get('file_public_path', conf_path() . '/files'), get_class($this)); - - // HTTP auth settings (:) for the simpletest browser - // when sending requests to the test site. - $this->httpauth_method = variable_get('simpletest_httpauth_method', CURLAUTH_BASIC); - $username = variable_get('simpletest_httpauth_username', NULL); - $password = variable_get('simpletest_httpauth_password', NULL); - if ($username && $password) { - $this->httpauth_credentials = $username . ':' . $password; - } - - set_error_handler(array($this, 'errorHandler')); - $class = get_class($this); - // Iterate through all the methods in this class, unless a specific list of - // methods to run was passed. - $class_methods = get_class_methods($class); - if ($methods) { - $class_methods = array_intersect($class_methods, $methods); - } - foreach ($class_methods as $method) { - // If the current method starts with "test", run it - it's a test. - if (strtolower(substr($method, 0, 4)) == 'test') { - // Insert a fail record. This will be deleted on completion to ensure - // that testing completed. - $method_info = new ReflectionMethod($class, $method); - $caller = array( - 'file' => $method_info->getFileName(), - 'line' => $method_info->getStartLine(), - 'function' => $class . '->' . $method . '()', - ); - $completion_check_id = DrupalTestCase::insertAssert($this->testId, $class, FALSE, t('The test did not complete due to a fatal error.'), 'Completion check', $caller); - $this->setUp(); - try { - $this->$method(); - // Finish up. - } - catch (Exception $e) { - $this->exceptionHandler($e); - } - $this->tearDown(); - // Remove the completion check record. - DrupalTestCase::deleteAssert($completion_check_id); - } - } - // Clear out the error messages and restore error handler. - drupal_get_messages(); - restore_error_handler(); - } - - /** - * Handle errors during test runs. - * - * Because this is registered in set_error_handler(), it has to be public. - * @see set_error_handler - */ - public function errorHandler($severity, $message, $file = NULL, $line = NULL) { - if ($severity & error_reporting()) { - $error_map = array( - E_STRICT => 'Run-time notice', - E_WARNING => 'Warning', - E_NOTICE => 'Notice', - E_CORE_ERROR => 'Core error', - E_CORE_WARNING => 'Core warning', - E_USER_ERROR => 'User error', - E_USER_WARNING => 'User warning', - E_USER_NOTICE => 'User notice', - E_RECOVERABLE_ERROR => 'Recoverable error', - ); - - $backtrace = debug_backtrace(); - $this->error($message, $error_map[$severity], _drupal_get_last_caller($backtrace)); - } - return TRUE; - } - - /** - * Handle exceptions. - * - * @see set_exception_handler - */ - protected function exceptionHandler($exception) { - $backtrace = $exception->getTrace(); - // Push on top of the backtrace the call that generated the exception. - array_unshift($backtrace, array( - 'line' => $exception->getLine(), - 'file' => $exception->getFile(), - )); - require_once DRUPAL_ROOT . '/includes/errors.inc'; - // The exception message is run through check_plain() by _drupal_decode_exception(). - $this->error(t('%type: !message in %function (line %line of %file).', _drupal_decode_exception($exception)), 'Uncaught exception', _drupal_get_last_caller($backtrace)); - } - - /** - * Generates a random string of ASCII characters of codes 32 to 126. - * - * The generated string includes alpha-numeric characters and common misc - * characters. Use this method when testing general input where the content - * is not restricted. - * - * @param $length - * Length of random string to generate. - * @return - * Randomly generated string. - */ - public static function randomString($length = 8) { - $str = ''; - for ($i = 0; $i < $length; $i++) { - $str .= chr(mt_rand(32, 126)); - } - return $str; - } - - /** - * Generates a random string containing letters and numbers. - * - * The string will always start with a letter. The letters may be upper or - * lower case. This method is better for restricted inputs that do not - * accept certain characters. For example, when testing input fields that - * require machine readable values (i.e. without spaces and non-standard - * characters) this method is best. - * - * @param $length - * Length of random string to generate. - * @return - * Randomly generated string. - */ - public static function randomName($length = 8) { - $values = array_merge(range(65, 90), range(97, 122), range(48, 57)); - $max = count($values) - 1; - $str = chr(mt_rand(97, 122)); - for ($i = 1; $i < $length; $i++) { - $str .= chr($values[mt_rand(0, $max)]); - } - return $str; - } - - /** - * Converts a list of possible parameters into a stack of permutations. - * - * Takes a list of parameters containing possible values, and converts all of - * them into a list of items containing every possible permutation. - * - * Example: - * @code - * $parameters = array( - * 'one' => array(0, 1), - * 'two' => array(2, 3), - * ); - * $permutations = $this->permute($parameters); - * // Result: - * $permutations == array( - * array('one' => 0, 'two' => 2), - * array('one' => 1, 'two' => 2), - * array('one' => 0, 'two' => 3), - * array('one' => 1, 'two' => 3), - * ) - * @endcode - * - * @param $parameters - * An associative array of parameters, keyed by parameter name, and whose - * values are arrays of parameter values. - * - * @return - * A list of permutations, which is an array of arrays. Each inner array - * contains the full list of parameters that have been passed, but with a - * single value only. - */ - public static function generatePermutations($parameters) { - $all_permutations = array(array()); - foreach ($parameters as $parameter => $values) { - $new_permutations = array(); - // Iterate over all values of the parameter. - foreach ($values as $value) { - // Iterate over all existing permutations. - foreach ($all_permutations as $permutation) { - // Add the new parameter value to existing permutations. - $new_permutations[] = $permutation + array($parameter => $value); - } - } - // Replace the old permutations with the new permutations. - $all_permutations = $new_permutations; - } - return $all_permutations; - } -} - -/** - * Test case for Drupal unit tests. - * - * These tests can not access the database nor files. Calling any Drupal - * function that needs the database will throw exceptions. These include - * watchdog(), module_implements(), module_invoke_all() etc. - */ -class DrupalUnitTestCase extends DrupalTestCase { - - /** - * Constructor for DrupalUnitTestCase. - */ - function __construct($test_id = NULL) { - parent::__construct($test_id); - $this->skipClasses[__CLASS__] = TRUE; - } - - /** - * Sets up unit test environment. - * - * Unlike DrupalWebTestCase::setUp(), DrupalUnitTestCase::setUp() does not - * install modules because tests are performed without accessing the database. - * Any required files must be explicitly included by the child class setUp() - * method. - */ - protected function setUp() { - global $conf; - - // Store necessary current values before switching to the test environment. - $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); - - // Reset all statics so that test is performed with a clean environment. - drupal_static_reset(); - - // Generate temporary prefixed database to ensure that tests have a clean starting point. - $this->databasePrefix = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}'); - - // Create test directory. - $public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); - file_prepare_directory($public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); - $conf['file_public_path'] = $public_files_directory; - - // Clone the current connection and replace the current prefix. - $connection_info = Database::getConnectionInfo('default'); - Database::renameConnection('default', 'simpletest_original_default'); - foreach ($connection_info as $target => $value) { - $connection_info[$target]['prefix'] = array( - 'default' => $value['prefix']['default'] . $this->databasePrefix, - ); - } - Database::addConnectionInfo('default', 'default', $connection_info['default']); - - // Set user agent to be consistent with web test case. - $_SERVER['HTTP_USER_AGENT'] = $this->databasePrefix; - - // If locale is enabled then t() will try to access the database and - // subsequently will fail as the database is not accessible. - $module_list = module_list(); - if (isset($module_list['locale'])) { - $this->originalModuleList = $module_list; - unset($module_list['locale']); - module_list(TRUE, FALSE, FALSE, $module_list); - } - } - - protected function tearDown() { - global $conf; - - // Get back to the original connection. - Database::removeConnection('default'); - Database::renameConnection('simpletest_original_default', 'default'); - - $conf['file_public_path'] = $this->originalFileDirectory; - // Restore modules if necessary. - if (isset($this->originalModuleList)) { - module_list(TRUE, FALSE, FALSE, $this->originalModuleList); - } - } -} - -/** - * Test case for typical Drupal tests. - */ -class DrupalWebTestCase extends DrupalTestCase { - /** - * The profile to install as a basis for testing. - * - * @var string - */ - protected $profile = 'standard'; - - /** - * The URL currently loaded in the internal browser. - * - * @var string - */ - protected $url; - - /** - * The handle of the current cURL connection. - * - * @var resource - */ - protected $curlHandle; - - /** - * The headers of the page currently loaded in the internal browser. - * - * @var Array - */ - protected $headers; - - /** - * The content of the page currently loaded in the internal browser. - * - * @var string - */ - protected $content; - - /** - * The content of the page currently loaded in the internal browser (plain text version). - * - * @var string - */ - protected $plainTextContent; - - /** - * The value of the Drupal.settings JavaScript variable for the page currently loaded in the internal browser. - * - * @var Array - */ - protected $drupalSettings; - - /** - * The parsed version of the page. - * - * @var SimpleXMLElement - */ - protected $elements = NULL; - - /** - * The current user logged in using the internal browser. - * - * @var bool - */ - protected $loggedInUser = FALSE; - - /** - * The current cookie file used by cURL. - * - * We do not reuse the cookies in further runs, so we do not need a file - * but we still need cookie handling, so we set the jar to NULL. - */ - protected $cookieFile = NULL; - - /** - * Additional cURL options. - * - * DrupalWebTestCase itself never sets this but always obeys what is set. - */ - protected $additionalCurlOptions = array(); - - /** - * The original user, before it was changed to a clean uid = 1 for testing purposes. - * - * @var object - */ - protected $originalUser = NULL; - - /** - * The original shutdown handlers array, before it was cleaned for testing purposes. - * - * @var array - */ - protected $originalShutdownCallbacks = array(); - - /** - * HTTP authentication method - */ - protected $httpauth_method = CURLAUTH_BASIC; - - /** - * HTTP authentication credentials (:). - */ - protected $httpauth_credentials = NULL; - - /** - * The current session name, if available. - */ - protected $session_name = NULL; - - /** - * The current session ID, if available. - */ - protected $session_id = NULL; - - /** - * Whether the files were copied to the test files directory. - */ - protected $generatedTestFiles = FALSE; - - /** - * The number of redirects followed during the handling of a request. - */ - protected $redirect_count; - - /** - * Constructor for DrupalWebTestCase. - */ - function __construct($test_id = NULL) { - parent::__construct($test_id); - $this->skipClasses[__CLASS__] = TRUE; - } - - /** - * Get a node from the database based on its title. - * - * @param title - * A node title, usually generated by $this->randomName(). - * @param $reset - * (optional) Whether to reset the internal node_load() cache. - * - * @return - * A node object matching $title. - */ - function drupalGetNodeByTitle($title, $reset = FALSE) { - $nodes = node_load_multiple(array(), array('title' => $title), $reset); - // Load the first node returned from the database. - $returned_node = reset($nodes); - return $returned_node; - } - - /** - * Creates a node based on default settings. - * - * @param $settings - * An associative array of settings to change from the defaults, keys are - * node properties, for example 'title' => 'Hello, world!'. - * @return - * Created node object. - */ - protected function drupalCreateNode($settings = array()) { - // Populate defaults array. - $settings += array( - 'body' => array(LANGUAGE_NONE => array(array())), - 'title' => $this->randomName(8), - 'comment' => 2, - 'changed' => REQUEST_TIME, - 'moderate' => 0, - 'promote' => 0, - 'revision' => 1, - 'log' => '', - 'status' => 1, - 'sticky' => 0, - 'type' => 'page', - 'revisions' => NULL, - 'language' => LANGUAGE_NONE, - ); - - // Use the original node's created time for existing nodes. - if (isset($settings['created']) && !isset($settings['date'])) { - $settings['date'] = format_date($settings['created'], 'custom', 'Y-m-d H:i:s O'); - } - - // If the node's user uid is not specified manually, use the currently - // logged in user if available, or else the user running the test. - if (!isset($settings['uid'])) { - if ($this->loggedInUser) { - $settings['uid'] = $this->loggedInUser->uid; - } - else { - global $user; - $settings['uid'] = $user->uid; - } - } - - // Merge body field value and format separately. - $body = array( - 'value' => $this->randomName(32), - 'format' => filter_default_format(), - ); - $settings['body'][$settings['language']][0] += $body; - - $node = (object) $settings; - node_save($node); - - // Small hack to link revisions to our test user. - db_update('node_revision') - ->fields(array('uid' => $node->uid)) - ->condition('vid', $node->vid) - ->execute(); - return $node; - } - - /** - * Creates a custom content type based on default settings. - * - * @param $settings - * An array of settings to change from the defaults. - * Example: 'type' => 'foo'. - * @return - * Created content type. - */ - protected function drupalCreateContentType($settings = array()) { - // Find a non-existent random type name. - do { - $name = strtolower($this->randomName(8)); - } while (node_type_get_type($name)); - - // Populate defaults array. - $defaults = array( - 'type' => $name, - 'name' => $name, - 'base' => 'node_content', - 'description' => '', - 'help' => '', - 'title_label' => 'Title', - 'body_label' => 'Body', - 'has_title' => 1, - 'has_body' => 1, - ); - // Imposed values for a custom type. - $forced = array( - 'orig_type' => '', - 'old_type' => '', - 'module' => 'node', - 'custom' => 1, - 'modified' => 1, - 'locked' => 0, - ); - $type = $forced + $settings + $defaults; - $type = (object) $type; - - $saved_type = node_type_save($type); - node_types_rebuild(); - menu_rebuild(); - node_add_body_field($type); - - $this->assertEqual($saved_type, SAVED_NEW, t('Created content type %type.', array('%type' => $type->type))); - - // Reset permissions so that permissions for this content type are available. - $this->checkPermissions(array(), TRUE); - - return $type; - } - - /** - * Get a list files that can be used in tests. - * - * @param $type - * File type, possible values: 'binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'. - * @param $size - * File size in bytes to match. Please check the tests/files folder. - * @return - * List of files that match filter. - */ - protected function drupalGetTestFiles($type, $size = NULL) { - if (empty($this->generatedTestFiles)) { - // Generate binary test files. - $lines = array(64, 1024); - $count = 0; - foreach ($lines as $line) { - simpletest_generate_file('binary-' . $count++, 64, $line, 'binary'); - } - - // Generate text test files. - $lines = array(16, 256, 1024, 2048, 20480); - $count = 0; - foreach ($lines as $line) { - simpletest_generate_file('text-' . $count++, 64, $line); - } - - // Copy other test files from simpletest. - $original = drupal_get_path('module', 'simpletest') . '/files'; - $files = file_scan_directory($original, '/(html|image|javascript|php|sql)-.*/'); - foreach ($files as $file) { - file_unmanaged_copy($file->uri, variable_get('file_public_path', conf_path() . '/files')); - } - - $this->generatedTestFiles = TRUE; - } - - $files = array(); - // Make sure type is valid. - if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) { - $files = file_scan_directory('public://', '/' . $type . '\-.*/'); - - // If size is set then remove any files that are not of that size. - if ($size !== NULL) { - foreach ($files as $file) { - $stats = stat($file->uri); - if ($stats['size'] != $size) { - unset($files[$file->uri]); - } - } - } - } - usort($files, array($this, 'drupalCompareFiles')); - return $files; - } - - /** - * Compare two files based on size and file name. - */ - protected function drupalCompareFiles($file1, $file2) { - $compare_size = filesize($file1->uri) - filesize($file2->uri); - if ($compare_size) { - // Sort by file size. - return $compare_size; - } - else { - // The files were the same size, so sort alphabetically. - return strnatcmp($file1->name, $file2->name); - } - } - - /** - * Create a user with a given set of permissions. The permissions correspond to the - * names given on the privileges page. - * - * @param $permissions - * Array of permission names to assign to user. - * @return - * A fully loaded user object with pass_raw property, or FALSE if account - * creation fails. - */ - protected function drupalCreateUser($permissions = array('access comments', 'access content', 'post comments', 'skip comment approval')) { - // Create a role with the given permission set. - if (!($rid = $this->drupalCreateRole($permissions))) { - return FALSE; - } - - // Create a user assigned to that role. - $edit = array(); - $edit['name'] = $this->randomName(); - $edit['mail'] = $edit['name'] . '@example.com'; - $edit['roles'] = array($rid => $rid); - $edit['pass'] = user_password(); - $edit['status'] = 1; - - $account = user_save(drupal_anonymous_user(), $edit); - - $this->assertTrue(!empty($account->uid), t('User created with name %name and pass %pass', array('%name' => $edit['name'], '%pass' => $edit['pass'])), t('User login')); - if (empty($account->uid)) { - return FALSE; - } - - // Add the raw password so that we can log in as this user. - $account->pass_raw = $edit['pass']; - return $account; - } - - /** - * Internal helper function; Create a role with specified permissions. - * - * @param $permissions - * Array of permission names to assign to role. - * @param $name - * (optional) String for the name of the role. Defaults to a random string. - * @return - * Role ID of newly created role, or FALSE if role creation failed. - */ - protected function drupalCreateRole(array $permissions, $name = NULL) { - // Generate random name if it was not passed. - if (!$name) { - $name = $this->randomName(); - } - - // Check the all the permissions strings are valid. - if (!$this->checkPermissions($permissions)) { - return FALSE; - } - - // Create new role. - $role = new stdClass(); - $role->name = $name; - user_role_save($role); - user_role_grant_permissions($role->rid, $permissions); - - $this->assertTrue(isset($role->rid), t('Created role of name: @name, id: @rid', array('@name' => $name, '@rid' => (isset($role->rid) ? $role->rid : t('-n/a-')))), t('Role')); - if ($role && !empty($role->rid)) { - $count = db_query('SELECT COUNT(*) FROM {role_permission} WHERE rid = :rid', array(':rid' => $role->rid))->fetchField(); - $this->assertTrue($count == count($permissions), t('Created permissions: @perms', array('@perms' => implode(', ', $permissions))), t('Role')); - return $role->rid; - } - else { - return FALSE; - } - } - - /** - * Check to make sure that the array of permissions are valid. - * - * @param $permissions - * Permissions to check. - * @param $reset - * Reset cached available permissions. - * @return - * TRUE or FALSE depending on whether the permissions are valid. - */ - protected function checkPermissions(array $permissions, $reset = FALSE) { - $available = &drupal_static(__FUNCTION__); - - if (!isset($available) || $reset) { - $available = array_keys(module_invoke_all('permission')); - } - - $valid = TRUE; - foreach ($permissions as $permission) { - if (!in_array($permission, $available)) { - $this->fail(t('Invalid permission %permission.', array('%permission' => $permission)), t('Role')); - $valid = FALSE; - } - } - return $valid; - } - - /** - * Log in a user with the internal browser. - * - * If a user is already logged in, then the current user is logged out before - * logging in the specified user. - * - * Please note that neither the global $user nor the passed-in user object is - * populated with data of the logged in user. If you need full access to the - * user object after logging in, it must be updated manually. If you also need - * access to the plain-text password of the user (set by drupalCreateUser()), - * e.g. to log in the same user again, then it must be re-assigned manually. - * For example: - * @code - * // Create a user. - * $account = $this->drupalCreateUser(array()); - * $this->drupalLogin($account); - * // Load real user object. - * $pass_raw = $account->pass_raw; - * $account = user_load($account->uid); - * $account->pass_raw = $pass_raw; - * @endcode - * - * @param $user - * User object representing the user to log in. - * - * @see drupalCreateUser() - */ - protected function drupalLogin(stdClass $user) { - if ($this->loggedInUser) { - $this->drupalLogout(); - } - - $edit = array( - 'name' => $user->name, - 'pass' => $user->pass_raw - ); - $this->drupalPost('user', $edit, t('Log in')); - - // If a "log out" link appears on the page, it is almost certainly because - // the login was successful. - $pass = $this->assertLink(t('Log out'), 0, t('User %name successfully logged in.', array('%name' => $user->name)), t('User login')); - - if ($pass) { - $this->loggedInUser = $user; - } - } - - /** - * Generate a token for the currently logged in user. - */ - protected function drupalGetToken($value = '') { - $private_key = drupal_get_private_key(); - return drupal_hmac_base64($value, $this->session_id . $private_key); - } - - /* - * Logs a user out of the internal browser, then check the login page to confirm logout. - */ - protected function drupalLogout() { - // Make a request to the logout page, and redirect to the user page, the - // idea being if you were properly logged out you should be seeing a login - // screen. - $this->drupalGet('user/logout'); - $this->drupalGet('user'); - $pass = $this->assertField('name', t('Username field found.'), t('Logout')); - $pass = $pass && $this->assertField('pass', t('Password field found.'), t('Logout')); - - if ($pass) { - $this->loggedInUser = FALSE; - } - } - - /** - * Generates a random database prefix, runs the install scripts on the - * prefixed database and enable the specified modules. After installation - * many caches are flushed and the internal browser is setup so that the - * page requests will run on the new prefix. A temporary files directory - * is created with the same name as the database prefix. - * - * @param ... - * List of modules to enable for the duration of the test. This can be - * either a single array or a variable number of string arguments. - */ - protected function setUp() { - global $user, $language, $conf; - - // Generate a temporary prefixed database to ensure that tests have a clean starting point. - $this->databasePrefix = 'simpletest' . mt_rand(1000, 1000000); - db_update('simpletest_test_id') - ->fields(array('last_prefix' => $this->databasePrefix)) - ->condition('test_id', $this->testId) - ->execute(); - - // Clone the current connection and replace the current prefix. - $connection_info = Database::getConnectionInfo('default'); - Database::renameConnection('default', 'simpletest_original_default'); - foreach ($connection_info as $target => $value) { - $connection_info[$target]['prefix'] = array( - 'default' => $value['prefix']['default'] . $this->databasePrefix, - ); - } - Database::addConnectionInfo('default', 'default', $connection_info['default']); - - // Store necessary current values before switching to prefixed database. - $this->originalLanguage = $language; - $this->originalLanguageDefault = variable_get('language_default'); - $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); - $this->originalProfile = drupal_get_profile(); - $clean_url_original = variable_get('clean_url', 0); - - // Save and clean shutdown callbacks array because it static cached and - // will be changed by the test run. If we don't, then it will contain - // callbacks from both environments. So testing environment will try - // to call handlers from original environment. - $callbacks = &drupal_register_shutdown_function(); - $this->originalShutdownCallbacks = $callbacks; - $callbacks = array(); - - // Create test directory ahead of installation so fatal errors and debug - // information can be logged during installation process. - // Use temporary files directory with the same prefix as the database. - $public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); - $private_files_directory = $public_files_directory . '/private'; - $temp_files_directory = $private_files_directory . '/temp'; - - // Create the directories - file_prepare_directory($public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); - file_prepare_directory($private_files_directory, FILE_CREATE_DIRECTORY); - file_prepare_directory($temp_files_directory, FILE_CREATE_DIRECTORY); - $this->generatedTestFiles = FALSE; - - // Log fatal errors. - ini_set('log_errors', 1); - ini_set('error_log', $public_files_directory . '/error.log'); - - // Reset all statics and variables to perform tests in a clean environment. - $conf = array(); - drupal_static_reset(); - - // Set the test information for use in other parts of Drupal. - $test_info = &$GLOBALS['drupal_test_info']; - $test_info['test_run_id'] = $this->databasePrefix; - $test_info['in_child_site'] = FALSE; - - include_once DRUPAL_ROOT . '/includes/install.inc'; - drupal_install_system(); - - $this->preloadRegistry(); - - // Set path variables. - variable_set('file_public_path', $public_files_directory); - variable_set('file_private_path', $private_files_directory); - variable_set('file_temporary_path', $temp_files_directory); - - // Include the testing profile. - variable_set('install_profile', $this->profile); - $profile_details = install_profile_info($this->profile, 'en'); - - // Install the modules specified by the testing profile. - module_enable($profile_details['dependencies'], FALSE); - - // Install modules needed for this test. This could have been passed in as - // either a single array argument or a variable number of string arguments. - // @todo Remove this compatibility layer in Drupal 8, and only accept - // $modules as a single array argument. - $modules = func_get_args(); - if (isset($modules[0]) && is_array($modules[0])) { - $modules = $modules[0]; - } - if ($modules) { - module_enable($modules, TRUE); - } - - // Run the profile tasks. - $install_profile_module_exists = db_query("SELECT 1 FROM {system} WHERE type = 'module' AND name = :name", array( - ':name' => $this->profile, - ))->fetchField(); - if ($install_profile_module_exists) { - module_enable(array($this->profile), FALSE); - } - - // Reset/rebuild all data structures after enabling the modules. - $this->resetAll(); - - // Run cron once in that environment, as install.php does at the end of - // the installation process. - drupal_cron_run(); - - // Log in with a clean $user. - $this->originalUser = $user; - drupal_save_session(FALSE); - $user = user_load(1); - - // Restore necessary variables. - variable_set('install_task', 'done'); - variable_set('clean_url', $clean_url_original); - variable_set('site_mail', 'simpletest@example.com'); - variable_set('date_default_timezone', date_default_timezone_get()); - // Set up English language. - unset($GLOBALS['conf']['language_default']); - $language = language_default(); - - // Use the test mail class instead of the default mail handler class. - variable_set('mail_system', array('default-system' => 'TestingMailSystem')); - - drupal_set_time_limit($this->timeLimit); - } - - /** - * Preload the registry from the testing site. - * - * This method is called by DrupalWebTestCase::setUp(), and preloads the - * registry from the testing site to cut down on the time it takes to - * set up a clean environment for the current test run. - */ - protected function preloadRegistry() { - // Use two separate queries, each with their own connections: copy the - // {registry} and {registry_file} tables over from the parent installation - // to the child installation. - $original_connection = Database::getConnection('default', 'simpletest_original_default'); - $test_connection = Database::getConnection(); - - foreach (array('registry', 'registry_file') as $table) { - // Find the records from the parent database. - $source_query = $original_connection - ->select($table, array(), array('fetch' => PDO::FETCH_ASSOC)) - ->fields($table); - - $dest_query = $test_connection->insert($table); - - $first = TRUE; - foreach ($source_query->execute() as $row) { - if ($first) { - $dest_query->fields(array_keys($row)); - $first = FALSE; - } - // Insert the records into the child database. - $dest_query->values($row); - } - - $dest_query->execute(); - } - } - - /** - * Reset all data structures after having enabled new modules. - * - * This method is called by DrupalWebTestCase::setUp() after enabling - * the requested modules. It must be called again when additional modules - * are enabled later. - */ - protected function resetAll() { - // Reset all static variables. - drupal_static_reset(); - // Reset the list of enabled modules. - module_list(TRUE); - - // Reset cached schema for new database prefix. This must be done before - // drupal_flush_all_caches() so rebuilds can make use of the schema of - // modules enabled on the cURL side. - drupal_get_schema(NULL, TRUE); - - // Perform rebuilds and flush remaining caches. - drupal_flush_all_caches(); - - // Reload global $conf array and permissions. - $this->refreshVariables(); - $this->checkPermissions(array(), TRUE); - } - - /** - * Refresh the in-memory set of variables. Useful after a page request is made - * that changes a variable in a different thread. - * - * In other words calling a settings page with $this->drupalPost() with a changed - * value would update a variable to reflect that change, but in the thread that - * made the call (thread running the test) the changed variable would not be - * picked up. - * - * This method clears the variables cache and loads a fresh copy from the database - * to ensure that the most up-to-date set of variables is loaded. - */ - protected function refreshVariables() { - global $conf; - cache_clear_all('variables', 'cache_bootstrap'); - $conf = variable_initialize(); - } - - /** - * Delete created files and temporary files directory, delete the tables created by setUp(), - * and reset the database prefix. - */ - protected function tearDown() { - global $user, $language; - - // In case a fatal error occured that was not in the test process read the - // log to pick up any fatal errors. - simpletest_log_read($this->testId, $this->databasePrefix, get_class($this), TRUE); - - $emailCount = count(variable_get('drupal_test_email_collector', array())); - if ($emailCount) { - $message = format_plural($emailCount, '1 e-mail was sent during this test.', '@count e-mails were sent during this test.'); - $this->pass($message, t('E-mail')); - } - - // Delete temporary files directory. - file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10)); - - // Remove all prefixed tables (all the tables in the schema). - $schema = drupal_get_schema(NULL, TRUE); - foreach ($schema as $name => $table) { - db_drop_table($name); - } - - // Get back to the original connection. - Database::removeConnection('default'); - Database::renameConnection('simpletest_original_default', 'default'); - - // Restore original shutdown callbacks array to prevent original - // environment of calling handlers from test run. - $callbacks = &drupal_register_shutdown_function(); - $callbacks = $this->originalShutdownCallbacks; - - // Return the user to the original one. - $user = $this->originalUser; - drupal_save_session(TRUE); - - // Ensure that internal logged in variable and cURL options are reset. - $this->loggedInUser = FALSE; - $this->additionalCurlOptions = array(); - - // Reload module list and implementations to ensure that test module hooks - // aren't called after tests. - module_list(TRUE); - module_implements('', FALSE, TRUE); - - // Reset the Field API. - field_cache_clear(); - - // Rebuild caches. - $this->refreshVariables(); - - // Reset language. - $language = $this->originalLanguage; - if ($this->originalLanguageDefault) { - $GLOBALS['conf']['language_default'] = $this->originalLanguageDefault; - } - - // Close the CURL handler. - $this->curlClose(); - } - - /** - * Initializes the cURL connection. - * - * If the simpletest_httpauth_credentials variable is set, this function will - * add HTTP authentication headers. This is necessary for testing sites that - * are protected by login credentials from public access. - * See the description of $curl_options for other options. - */ - protected function curlInitialize() { - global $base_url; - - if (!isset($this->curlHandle)) { - $this->curlHandle = curl_init(); - $curl_options = array( - CURLOPT_COOKIEJAR => $this->cookieFile, - CURLOPT_URL => $base_url, - CURLOPT_FOLLOWLOCATION => FALSE, - CURLOPT_RETURNTRANSFER => TRUE, - CURLOPT_SSL_VERIFYPEER => FALSE, // Required to make the tests run on https. - CURLOPT_SSL_VERIFYHOST => FALSE, // Required to make the tests run on https. - CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'), - CURLOPT_USERAGENT => $this->databasePrefix, - ); - if (isset($this->httpauth_credentials)) { - $curl_options[CURLOPT_HTTPAUTH] = $this->httpauth_method; - $curl_options[CURLOPT_USERPWD] = $this->httpauth_credentials; - } - curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options); - - // By default, the child session name should be the same as the parent. - $this->session_name = session_name(); - } - // We set the user agent header on each request so as to use the current - // time and a new uniqid. - if (preg_match('/simpletest\d+/', $this->databasePrefix, $matches)) { - curl_setopt($this->curlHandle, CURLOPT_USERAGENT, drupal_generate_test_ua($matches[0])); - } - } - - /** - * Initializes and executes a cURL request. - * - * @param $curl_options - * An associative array of cURL options to set, where the keys are constants - * defined by the cURL library. For a list of valid options, see - * http://www.php.net/manual/function.curl-setopt.php - * @param $redirect - * FALSE if this is an initial request, TRUE if this request is the result - * of a redirect. - * - * @return - * The content returned from the call to curl_exec(). - * - * @see curlInitialize() - */ - protected function curlExec($curl_options, $redirect = FALSE) { - $this->curlInitialize(); - - // cURL incorrectly handles URLs with a fragment by including the - // fragment in the request to the server, causing some web servers - // to reject the request citing "400 - Bad Request". To prevent - // this, we strip the fragment from the request. - // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0. - if (!empty($curl_options[CURLOPT_URL]) && strpos($curl_options[CURLOPT_URL], '#')) { - $original_url = $curl_options[CURLOPT_URL]; - $curl_options[CURLOPT_URL] = strtok($curl_options[CURLOPT_URL], '#'); - } - - $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL]; - - if (!empty($curl_options[CURLOPT_POST])) { - // This is a fix for the Curl library to prevent Expect: 100-continue - // headers in POST requests, that may cause unexpected HTTP response - // codes from some webservers (like lighttpd that returns a 417 error - // code). It is done by setting an empty "Expect" header field that is - // not overwritten by Curl. - $curl_options[CURLOPT_HTTPHEADER][] = 'Expect:'; - } - curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options); - - if (!$redirect) { - // Reset headers, the session ID and the redirect counter. - $this->session_id = NULL; - $this->headers = array(); - $this->redirect_count = 0; - } - - $content = curl_exec($this->curlHandle); - $status = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE); - - // cURL incorrectly handles URLs with fragments, so instead of - // letting cURL handle redirects we take of them ourselves to - // to prevent fragments being sent to the web server as part - // of the request. - // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0. - if (in_array($status, array(300, 301, 302, 303, 305, 307)) && $this->redirect_count < variable_get('simpletest_maximum_redirects', 5)) { - if ($this->drupalGetHeader('location')) { - $this->redirect_count++; - $curl_options = array(); - $curl_options[CURLOPT_URL] = $this->drupalGetHeader('location'); - $curl_options[CURLOPT_HTTPGET] = TRUE; - return $this->curlExec($curl_options, TRUE); - } - } - - $this->drupalSetContent($content, isset($original_url) ? $original_url : curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL)); - $message_vars = array( - '!method' => !empty($curl_options[CURLOPT_NOBODY]) ? 'HEAD' : (empty($curl_options[CURLOPT_POSTFIELDS]) ? 'GET' : 'POST'), - '@url' => isset($original_url) ? $original_url : $url, - '@status' => $status, - '!length' => format_size(strlen($this->drupalGetContent())) - ); - $message = t('!method @url returned @status (!length).', $message_vars); - $this->assertTrue($this->drupalGetContent() !== FALSE, $message, t('Browser')); - return $this->drupalGetContent(); - } - - /** - * Reads headers and registers errors received from the tested site. - * - * @see _drupal_log_error(). - * - * @param $curlHandler - * The cURL handler. - * @param $header - * An header. - */ - protected function curlHeaderCallback($curlHandler, $header) { - $this->headers[] = $header; - - // Errors are being sent via X-Drupal-Assertion-* headers, - // generated by _drupal_log_error() in the exact form required - // by DrupalWebTestCase::error(). - if (preg_match('/^X-Drupal-Assertion-[0-9]+: (.*)$/', $header, $matches)) { - // Call DrupalWebTestCase::error() with the parameters from the header. - call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1]))); - } - - // Save cookies. - if (preg_match('/^Set-Cookie: ([^=]+)=(.+)/', $header, $matches)) { - $name = $matches[1]; - $parts = array_map('trim', explode(';', $matches[2])); - $value = array_shift($parts); - $this->cookies[$name] = array('value' => $value, 'secure' => in_array('secure', $parts)); - if ($name == $this->session_name) { - if ($value != 'deleted') { - $this->session_id = $value; - } - else { - $this->session_id = NULL; - } - } - } - - // This is required by cURL. - return strlen($header); - } - - /** - * Close the cURL handler and unset the handler. - */ - protected function curlClose() { - if (isset($this->curlHandle)) { - curl_close($this->curlHandle); - unset($this->curlHandle); - } - } - - /** - * Parse content returned from curlExec using DOM and SimpleXML. - * - * @return - * A SimpleXMLElement or FALSE on failure. - */ - protected function parse() { - if (!$this->elements) { - // DOM can load HTML soup. But, HTML soup can throw warnings, suppress - // them. - $htmlDom = new DOMDocument(); - @$htmlDom->loadHTML($this->drupalGetContent()); - if ($htmlDom) { - $this->pass(t('Valid HTML found on "@path"', array('@path' => $this->getUrl())), t('Browser')); - // It's much easier to work with simplexml than DOM, luckily enough - // we can just simply import our DOM tree. - $this->elements = simplexml_import_dom($htmlDom); - } - } - if (!$this->elements) { - $this->fail(t('Parsed page successfully.'), t('Browser')); - } - - return $this->elements; - } - - /** - * Retrieves a Drupal path or an absolute path. - * - * @param $path - * Drupal path or URL to load into internal browser - * @param $options - * Options to be forwarded to url(). - * @param $headers - * An array containing additional HTTP request headers, each formatted as - * "name: value". - * @return - * The retrieved HTML string, also available as $this->drupalGetContent() - */ - protected function drupalGet($path, array $options = array(), array $headers = array()) { - $options['absolute'] = TRUE; - - // We re-using a CURL connection here. If that connection still has certain - // options set, it might change the GET into a POST. Make sure we clear out - // previous options. - $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers)); - $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up. - - // Replace original page output with new output from redirected page(s). - if ($new = $this->checkForMetaRefresh()) { - $out = $new; - } - $this->verbose('GET request to: ' . $path . - '
    Ending URL: ' . $this->getUrl() . - '
    ' . $out); - return $out; - } - - /** - * Retrieve a Drupal path or an absolute path and JSON decode the result. - */ - protected function drupalGetAJAX($path, array $options = array(), array $headers = array()) { - return drupal_json_decode($this->drupalGet($path, $options, $headers)); - } - - /** - * Execute a POST request on a Drupal page. - * It will be done as usual POST request with SimpleBrowser. - * - * @param $path - * Location of the post form. Either a Drupal path or an absolute path or - * NULL to post to the current page. For multi-stage forms you can set the - * path to NULL and have it post to the last received page. Example: - * - * @code - * // First step in form. - * $edit = array(...); - * $this->drupalPost('some_url', $edit, t('Save')); - * - * // Second step in form. - * $edit = array(...); - * $this->drupalPost(NULL, $edit, t('Save')); - * @endcode - * @param $edit - * Field data in an associative array. Changes the current input fields - * (where possible) to the values indicated. A checkbox can be set to - * TRUE to be checked and FALSE to be unchecked. Note that when a form - * contains file upload fields, other fields cannot start with the '@' - * character. - * - * Multiple select fields can be set using name[] and setting each of the - * possible values. Example: - * @code - * $edit = array(); - * $edit['name[]'] = array('value1', 'value2'); - * @endcode - * @param $submit - * Value of the submit button whose click is to be emulated. For example, - * t('Save'). The processing of the request depends on this value. For - * example, a form may have one button with the value t('Save') and another - * button with the value t('Delete'), and execute different code depending - * on which one is clicked. - * - * This function can also be called to emulate an Ajax submission. In this - * case, this value needs to be an array with the following keys: - * - path: A path to submit the form values to for Ajax-specific processing, - * which is likely different than the $path parameter used for retrieving - * the initial form. Defaults to 'system/ajax'. - * - triggering_element: If the value for the 'path' key is 'system/ajax' or - * another generic Ajax processing path, this needs to be set to the name - * of the element. If the name doesn't identify the element uniquely, then - * this should instead be an array with a single key/value pair, - * corresponding to the element name and value. The callback for the - * generic Ajax processing path uses this to find the #ajax information - * for the element, including which specific callback to use for - * processing the request. - * - * This can also be set to NULL in order to emulate an Internet Explorer - * submission of a form with a single text field, and pressing ENTER in that - * textfield: under these conditions, no button information is added to the - * POST data. - * @param $options - * Options to be forwarded to url(). - * @param $headers - * An array containing additional HTTP request headers, each formatted as - * "name: value". - * @param $form_html_id - * (optional) HTML ID of the form to be submitted. On some pages - * there are many identical forms, so just using the value of the submit - * button is not enough. For example: 'trigger-node-presave-assign-form'. - * Note that this is not the Drupal $form_id, but rather the HTML ID of the - * form, which is typically the same thing but with hyphens replacing the - * underscores. - * @param $extra_post - * (optional) A string of additional data to append to the POST submission. - * This can be used to add POST data for which there are no HTML fields, as - * is done by drupalPostAJAX(). This string is literally appended to the - * POST data, so it must already be urlencoded and contain a leading "&" - * (e.g., "&extra_var1=hello+world&extra_var2=you%26me"). - */ - protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) { - $submit_matches = FALSE; - $ajax = is_array($submit); - if (isset($path)) { - $this->drupalGet($path, $options); - } - if ($this->parse()) { - $edit_save = $edit; - // Let's iterate over all the forms. - $xpath = "//form"; - if (!empty($form_html_id)) { - $xpath .= "[@id='" . $form_html_id . "']"; - } - $forms = $this->xpath($xpath); - foreach ($forms as $form) { - // We try to set the fields of this form as specified in $edit. - $edit = $edit_save; - $post = array(); - $upload = array(); - $submit_matches = $this->handleForm($post, $edit, $upload, $ajax ? NULL : $submit, $form); - $action = isset($form['action']) ? $this->getAbsoluteUrl((string) $form['action']) : $this->getUrl(); - if ($ajax) { - $action = $this->getAbsoluteUrl(!empty($submit['path']) ? $submit['path'] : 'system/ajax'); - // Ajax callbacks verify the triggering element if necessary, so while - // we may eventually want extra code that verifies it in the - // handleForm() function, it's not currently a requirement. - $submit_matches = TRUE; - } - - // We post only if we managed to handle every field in edit and the - // submit button matches. - if (!$edit && ($submit_matches || !isset($submit))) { - $post_array = $post; - if ($upload) { - // TODO: cURL handles file uploads for us, but the implementation - // is broken. This is a less than elegant workaround. Alternatives - // are being explored at #253506. - foreach ($upload as $key => $file) { - $file = drupal_realpath($file); - if ($file && is_file($file)) { - $post[$key] = '@' . $file; - } - } - } - else { - foreach ($post as $key => $value) { - // Encode according to application/x-www-form-urlencoded - // Both names and values needs to be urlencoded, according to - // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1 - $post[$key] = urlencode($key) . '=' . urlencode($value); - } - $post = implode('&', $post) . $extra_post; - } - $out = $this->curlExec(array(CURLOPT_URL => $action, CURLOPT_POST => TRUE, CURLOPT_POSTFIELDS => $post, CURLOPT_HTTPHEADER => $headers)); - // Ensure that any changes to variables in the other thread are picked up. - $this->refreshVariables(); - - // Replace original page output with new output from redirected page(s). - if ($new = $this->checkForMetaRefresh()) { - $out = $new; - } - $this->verbose('POST request to: ' . $path . - '
    Ending URL: ' . $this->getUrl() . - '
    Fields: ' . highlight_string('' . $out); - return $out; - } - } - // We have not found a form which contained all fields of $edit. - foreach ($edit as $name => $value) { - $this->fail(t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value))); - } - if (!$ajax && isset($submit)) { - $this->assertTrue($submit_matches, t('Found the @submit button', array('@submit' => $submit))); - } - $this->fail(t('Found the requested form fields at @path', array('@path' => $path))); - } - } - - /** - * Execute an Ajax submission. - * - * This executes a POST as ajax.js does. It uses the returned JSON data, an - * array of commands, to update $this->content using equivalent DOM - * manipulation as is used by ajax.js. It also returns the array of commands. - * - * @param $path - * Location of the form containing the Ajax enabled element to test. Can be - * either a Drupal path or an absolute path or NULL to use the current page. - * @param $edit - * Field data in an associative array. Changes the current input fields - * (where possible) to the values indicated. - * @param $triggering_element - * The name of the form element that is responsible for triggering the Ajax - * functionality to test. May be a string or, if the triggering element is - * a button, an associative array where the key is the name of the button - * and the value is the button label. i.e.) array('op' => t('Refresh')). - * @param $ajax_path - * (optional) Override the path set by the Ajax settings of the triggering - * element. In the absence of both the triggering element's Ajax path and - * $ajax_path 'system/ajax' will be used. - * @param $options - * (optional) Options to be forwarded to url(). - * @param $headers - * (optional) An array containing additional HTTP request headers, each - * formatted as "name: value". Forwarded to drupalPost(). - * @param $form_html_id - * (optional) HTML ID of the form to be submitted, use when there is more - * than one identical form on the same page and the value of the triggering - * element is not enough to identify the form. Note this is not the Drupal - * ID of the form but rather the HTML ID of the form. - * @param $ajax_settings - * (optional) An array of Ajax settings which if specified will be used in - * place of the Ajax settings of the triggering element. - * - * @return - * An array of Ajax commands. - * - * @see drupalPost() - * @see ajax.js - */ - protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path = NULL, array $options = array(), array $headers = array(), $form_html_id = NULL, $ajax_settings = NULL) { - // Get the content of the initial page prior to calling drupalPost(), since - // drupalPost() replaces $this->content. - if (isset($path)) { - $this->drupalGet($path, $options); - } - $content = $this->content; - $drupal_settings = $this->drupalSettings; - - // Get the Ajax settings bound to the triggering element. - if (!isset($ajax_settings)) { - if (is_array($triggering_element)) { - $xpath = '//*[@name="' . key($triggering_element) . '" and @value="' . current($triggering_element) . '"]'; - } - else { - $xpath = '//*[@name="' . $triggering_element . '"]'; - } - if (isset($form_html_id)) { - $xpath = '//form[@id="' . $form_html_id . '"]' . $xpath; - } - $element = $this->xpath($xpath); - $element_id = (string) $element[0]['id']; - $ajax_settings = $drupal_settings['ajax'][$element_id]; - } - - // Add extra information to the POST data as ajax.js does. - $extra_post = ''; - if (isset($ajax_settings['submit'])) { - foreach ($ajax_settings['submit'] as $key => $value) { - $extra_post .= '&' . urlencode($key) . '=' . urlencode($value); - } - } - foreach ($this->xpath('//*[@id]') as $element) { - $id = (string) $element['id']; - $extra_post .= '&' . urlencode('ajax_html_ids[]') . '=' . urlencode($id); - } - - // Unless a particular path is specified, use the one specified by the - // Ajax settings, or else 'system/ajax'. - if (!isset($ajax_path)) { - $ajax_path = isset($ajax_settings['url']) ? $ajax_settings['url'] : 'system/ajax'; - } - - // Submit the POST request. - $return = drupal_json_decode($this->drupalPost(NULL, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers, $form_html_id, $extra_post)); - - // Change the page content by applying the returned commands. - if (!empty($ajax_settings) && !empty($return)) { - // ajax.js applies some defaults to the settings object, so do the same - // for what's used by this function. - $ajax_settings += array( - 'method' => 'replaceWith', - ); - // DOM can load HTML soup. But, HTML soup can throw warnings, suppress - // them. - $dom = new DOMDocument(); - @$dom->loadHTML($content); - foreach ($return as $command) { - switch ($command['command']) { - case 'settings': - $drupal_settings = array_merge_recursive($drupal_settings, $command['settings']); - break; - - case 'insert': - // @todo ajax.js can process commands that include a 'selector', but - // these are hard to emulate with DOMDocument. For now, we only - // implement 'insert' commands that use $ajax_settings['wrapper']. - if (!isset($command['selector'])) { - // $dom->getElementById() doesn't work when drupalPostAJAX() is - // invoked multiple times for a page, so use XPath instead. This - // also sets us up for adding support for $command['selector'] in - // the future, once we figure out how to transform a jQuery - // selector to XPath. - $xpath = new DOMXPath($dom); - $wrapperNode = $xpath->query('//*[@id="' . $ajax_settings['wrapper'] . '"]')->item(0); - if ($wrapperNode) { - // ajax.js adds an enclosing DIV to work around a Safari bug. - $newDom = new DOMDocument(); - $newDom->loadHTML('
    ' . $command['data'] . '
    '); - $newNode = $dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE); - $method = isset($command['method']) ? $command['method'] : $ajax_settings['method']; - // The "method" is a jQuery DOM manipulation function. Emulate - // each one using PHP's DOMNode API. - switch ($method) { - case 'replaceWith': - $wrapperNode->parentNode->replaceChild($newNode, $wrapperNode); - break; - case 'append': - $wrapperNode->appendChild($newNode); - break; - case 'prepend': - // If no firstChild, insertBefore() falls back to - // appendChild(). - $wrapperNode->insertBefore($newNode, $wrapperNode->firstChild); - break; - case 'before': - $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode); - break; - case 'after': - // If no nextSibling, insertBefore() falls back to - // appendChild(). - $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode->nextSibling); - break; - case 'html': - foreach ($wrapperNode->childNodes as $childNode) { - $wrapperNode->removeChild($childNode); - } - $wrapperNode->appendChild($newNode); - break; - } - } - } - break; - - // @todo Add suitable implementations for these commands in order to - // have full test coverage of what ajax.js can do. - case 'remove': - break; - case 'changed': - break; - case 'css': - break; - case 'data': - break; - case 'restripe': - break; - } - } - $content = $dom->saveHTML(); - } - $this->drupalSetContent($content); - $this->drupalSetSettings($drupal_settings); - return $return; - } - - /** - * Runs cron in the Drupal installed by Simpletest. - */ - protected function cronRun() { - $this->drupalGet($GLOBALS['base_url'] . '/cron.php', array('external' => TRUE, 'query' => array('cron_key' => variable_get('cron_key', 'drupal')))); - } - - /** - * Check for meta refresh tag and if found call drupalGet() recursively. This - * function looks for the http-equiv attribute to be set to "Refresh" - * and is case-sensitive. - * - * @return - * Either the new page content or FALSE. - */ - protected function checkForMetaRefresh() { - if (strpos($this->drupalGetContent(), 'parse()) { - $refresh = $this->xpath('//meta[@http-equiv="Refresh"]'); - if (!empty($refresh)) { - // Parse the content attribute of the meta tag for the format: - // "[delay]: URL=[page_to_redirect_to]". - if (preg_match('/\d+;\s*URL=(?P.*)/i', $refresh[0]['content'], $match)) { - return $this->drupalGet($this->getAbsoluteUrl(decode_entities($match['url']))); - } - } - } - return FALSE; - } - - /** - * Retrieves only the headers for a Drupal path or an absolute path. - * - * @param $path - * Drupal path or URL to load into internal browser - * @param $options - * Options to be forwarded to url(). - * @param $headers - * An array containing additional HTTP request headers, each formatted as - * "name: value". - * @return - * The retrieved headers, also available as $this->drupalGetContent() - */ - protected function drupalHead($path, array $options = array(), array $headers = array()) { - $options['absolute'] = TRUE; - $out = $this->curlExec(array(CURLOPT_NOBODY => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_HTTPHEADER => $headers)); - $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up. - return $out; - } - - /** - * Handle form input related to drupalPost(). Ensure that the specified fields - * exist and attempt to create POST data in the correct manner for the particular - * field type. - * - * @param $post - * Reference to array of post values. - * @param $edit - * Reference to array of edit values to be checked against the form. - * @param $submit - * Form submit button value. - * @param $form - * Array of form elements. - * @return - * Submit value matches a valid submit input in the form. - */ - protected function handleForm(&$post, &$edit, &$upload, $submit, $form) { - // Retrieve the form elements. - $elements = $form->xpath('.//input[not(@disabled)]|.//textarea[not(@disabled)]|.//select[not(@disabled)]'); - $submit_matches = FALSE; - foreach ($elements as $element) { - // SimpleXML objects need string casting all the time. - $name = (string) $element['name']; - // This can either be the type of or the name of the tag itself - // for