Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • develop
  • issue-424
  • master
  • master-no-logins
  • svn-staging
  • svn-testing
  • svn-trunk
  • topics/add-contribution-info
  • 2010-11-11
  • 2010-12-15
  • 2011-01-11
  • 2011-01-18
  • 2011-01-26
  • 2011-02-10
  • 2011-02-23
  • 2011-03-09
  • 2011-03-15
  • 2011-03-30
  • 2011-04-05
  • 2011-05-03
  • 2011-05-12
  • 2011-06-16
  • 2011-06-21
  • 2011-06-23
  • 2011-06-29
  • 2011-06-30
  • 2011-07-11
  • 2011-07-18
  • 2011-07-20
  • 2011-07-21
  • 2011-07-28
  • 2011-08-03
  • 2011-08-05
  • 2011-08-15
  • 2011-08-17
  • 2011-08-29
  • 2011-08-30
  • 2011-09-19
  • 2011-10-03
  • 2011-10-06
  • 2011-10-27
  • 2011-11-01
  • 2011-11-08
  • 2011-11-08.2
  • 2011-11-14
  • 2011-11-17
  • 2011-12-05
  • 2011-12-16
  • 2012-01-12
  • 2012-01-13
  • 2012-02-07
  • 2012-03-01
  • 2012-04-02
  • 2012-04-03
  • 2012-04-18
  • 20120207
  • 7.1
  • 7.10
  • 7.11
  • 7.12
  • 7.13
  • 7.14
  • 7.15
  • 7.16
  • 7.16.1
  • 7.17
  • 7.18
  • 7.19
  • 7.2
  • 7.3
  • 7.3.1
  • 7.4
  • 7.5
  • 7.5.1
  • 7.6
  • 7.6.1
  • 7.7
  • 7.7.1
  • 7.7.2
  • 7.7.3
  • 7.8
  • 7.9
82 results

Target

Select target project
  • rklusman2/UNL-CMS
  • yzha1/UNL-CMS
  • pear/UNL-CMS
  • bbieber2/UNL-CMS
  • tsteiner2/UNL-CMS
  • erasmussen2/UNL-CMS
  • UNL-Information-Services/UNL-CMS
7 results
Select Git revision
  • develop
  • issue-424
  • issue-525
  • master
  • master-no-logins
  • svn-staging
  • svn-testing
  • svn-trunk
  • topics/add-contribution-info
  • 2010-11-11
  • 2010-12-15
  • 2011-01-11
  • 2011-01-18
  • 2011-01-26
  • 2011-02-10
  • 2011-02-23
  • 2011-03-09
  • 2011-03-15
  • 2011-03-30
  • 2011-04-05
  • 2011-05-03
  • 2011-05-12
  • 2011-06-16
  • 2011-06-21
  • 2011-06-23
  • 2011-06-29
  • 2011-06-30
  • 2011-07-11
  • 2011-07-18
  • 2011-07-20
  • 2011-07-21
  • 2011-07-28
  • 2011-08-03
  • 2011-08-05
  • 2011-08-15
  • 2011-08-17
  • 2011-08-29
  • 2011-08-30
  • 2011-09-19
  • 2011-10-03
  • 2011-10-06
  • 2011-10-27
  • 2011-11-01
  • 2011-11-08
  • 2011-11-08.2
  • 2011-11-14
  • 2011-11-17
  • 2011-12-05
  • 2011-12-16
  • 2012-01-12
  • 2012-01-13
  • 2012-02-07
  • 2012-03-01
  • 2012-04-02
  • 2012-04-03
  • 2012-04-18
  • 20120207
  • 7.1
  • 7.2
  • 7.3
  • 7.3.1
  • 7.4
  • 7.5
  • 7.5.1
  • 7.6
  • 7.6.1
66 results
Show changes
Showing
with 3903 additions and 1750 deletions
<?php <?php
// $Id: authorize.inc,v 1.11 2010/05/14 04:50:18 webchick Exp $
/** /**
* @file * @file
...@@ -7,13 +6,19 @@ ...@@ -7,13 +6,19 @@
*/ */
/** /**
* Build the form for choosing a FileTransfer type and supplying credentials. * Form constructor for the file transfer authorization form.
*
* Allows the user to choose a FileTransfer type and supply credentials.
*
* @see authorize_filetransfer_form_validate()
* @see authorize_filetransfer_form_submit()
* @ingroup forms
*/ */
function authorize_filetransfer_form($form_state) { function authorize_filetransfer_form($form, &$form_state) {
global $base_url, $is_https; global $base_url, $is_https;
$form = array(); $form = array();
// If possible, we want to post this form securely via https. // If possible, we want to post this form securely via HTTPS.
$form['#https'] = TRUE; $form['#https'] = TRUE;
// CSS we depend on lives in modules/system/maintenance.css, which is loaded // CSS we depend on lives in modules/system/maintenance.css, which is loaded
...@@ -21,15 +26,18 @@ function authorize_filetransfer_form($form_state) { ...@@ -21,15 +26,18 @@ function authorize_filetransfer_form($form_state) {
$form['#attached']['js'][] = $base_url . '/misc/authorize.js'; $form['#attached']['js'][] = $base_url . '/misc/authorize.js';
// Get all the available ways to transfer files. // Get all the available ways to transfer files.
if (empty($_SESSION['authorize_filetransfer_backends'])) { if (empty($_SESSION['authorize_filetransfer_info'])) {
drupal_set_message(t('Unable to continue, no available methods of file transfer'), 'error'); drupal_set_message(t('Unable to continue, no available methods of file transfer'), 'error');
return array(); return array();
} }
$available_backends = $_SESSION['authorize_filetransfer_backends']; $available_backends = $_SESSION['authorize_filetransfer_info'];
uasort($available_backends, 'drupal_sort_weight');
if (!$is_https) { if (!$is_https) {
drupal_set_message(t('WARNING: You are not using an encrypted connection, so your password will be sent in plain text. <a href="@https-link">Learn more</a>.', array('@https-link' => 'http://drupal.org/https-information')), 'error'); $form['information']['https_warning'] = array(
'#prefix' => '<div class="messages error">',
'#markup' => t('WARNING: You are not using an encrypted connection, so your password will be sent in plain text. <a href="@https-link">Learn more</a>.', array('@https-link' => 'http://drupal.org/https-information')),
'#suffix' => '</div>',
);
} }
// Decide on a default backend. // Decide on a default backend.
...@@ -78,17 +86,20 @@ function authorize_filetransfer_form($form_state) { ...@@ -78,17 +86,20 @@ function authorize_filetransfer_form($form_state) {
'#attributes' => array('style' => 'display:none'), '#attributes' => array('style' => 'display:none'),
); );
// Build a hidden fieldset for each one. // Build a container for each connection type.
foreach ($available_backends as $name => $backend) { foreach ($available_backends as $name => $backend) {
$form['connection_settings']['authorize_filetransfer_default']['#options'][$name] = $backend['title']; $form['connection_settings']['authorize_filetransfer_default']['#options'][$name] = $backend['title'];
$form['connection_settings'][$name] = array( $form['connection_settings'][$name] = array(
'#type' => 'fieldset', '#type' => 'container',
'#attributes' => array('class' => array("filetransfer-$name", 'filetransfer')), '#attributes' => array('class' => array("filetransfer-$name", 'filetransfer')),
'#title' => t('@backend connection settings', array('@backend' => $backend['title'])), );
// We can't use #prefix on the container itself since then the header won't
// be hidden and shown when the containers are being manipulated via JS.
$form['connection_settings'][$name]['header'] = array(
'#markup' => '<h4>' . t('@backend connection settings', array('@backend' => $backend['title'])) . '</h4>',
); );
$current_settings = variable_get('authorize_filetransfer_connection_settings_' . $name, array()); $form['connection_settings'][$name] += _authorize_filetransfer_connection_settings($name);
$form['connection_settings'][$name] += system_get_filetransfer_settings_form($name, $current_settings);
// Start non-JS code. // Start non-JS code.
if (isset($form_state['values']['connection_settings']['authorize_filetransfer_default']) && $form_state['values']['connection_settings']['authorize_filetransfer_default'] == $name) { if (isset($form_state['values']['connection_settings']['authorize_filetransfer_default']) && $form_state['values']['connection_settings']['authorize_filetransfer_default'] == $name) {
...@@ -113,7 +124,7 @@ function authorize_filetransfer_form($form_state) { ...@@ -113,7 +124,7 @@ function authorize_filetransfer_form($form_state) {
'#type' => 'submit', '#type' => 'submit',
'#value' => t('Change connection type'), '#value' => t('Change connection type'),
'#weight' => -5, '#weight' => -5,
'#attributes' => array('class' => 'filetransfer-change-connection-type'), '#attributes' => array('class' => array('filetransfer-change-connection-type')),
); );
} }
// End non-JS code. // End non-JS code.
...@@ -122,11 +133,76 @@ function authorize_filetransfer_form($form_state) { ...@@ -122,11 +133,76 @@ function authorize_filetransfer_form($form_state) {
} }
/** /**
* Validate callback for the filetransfer authorization form. * Generates the Form API array for a given connection backend's settings.
*
* @param $backend
* The name of the backend (e.g. 'ftp', 'ssh', etc).
*
* @return
* Form API array of connection settings for the given backend.
*
* @see hook_filetransfer_backends()
*/
function _authorize_filetransfer_connection_settings($backend) {
$defaults = variable_get('authorize_filetransfer_connection_settings_' . $backend, array());
$form = array();
// Create an instance of the file transfer class to get its settings form.
$filetransfer = authorize_get_filetransfer($backend);
if ($filetransfer) {
$form = $filetransfer->getSettingsForm();
}
// Fill in the defaults based on the saved settings, if any.
_authorize_filetransfer_connection_settings_set_defaults($form, NULL, $defaults);
return $form;
}
/**
* Sets the default settings on a file transfer connection form recursively.
*
* The default settings for the file transfer connection forms are saved in
* the database. The settings are stored as a nested array in the case of a
* settings form that has fieldsets or otherwise uses a nested structure.
* Therefore, to properly add defaults, we need to walk through all the
* children form elements and process those defaults recursively.
*
* @param $element
* Reference to the Form API form element we're operating on.
* @param $key
* The key for our current form element, if any.
* @param array $defaults
* The default settings for the file transfer backend we're operating on.
*/
function _authorize_filetransfer_connection_settings_set_defaults(&$element, $key, array $defaults) {
// If we're operating on a form element which isn't a fieldset, and we have
// a default setting saved, stash it in #default_value.
if (!empty($key) && isset($defaults[$key]) && isset($element['#type']) && $element['#type'] != 'fieldset') {
$element['#default_value'] = $defaults[$key];
}
// Now, we walk through all the child elements, and recursively invoke
// ourself on each one. Since the $defaults settings array can be nested
// (because of #tree, any values inside fieldsets will be nested), if
// there's a subarray of settings for the form key we're currently
// processing, pass in that subarray to the recursive call. Otherwise, just
// pass on the whole $defaults array.
foreach (element_children($element) as $child_key) {
_authorize_filetransfer_connection_settings_set_defaults($element[$child_key], $child_key, ((isset($defaults[$key]) && is_array($defaults[$key])) ? $defaults[$key] : $defaults));
}
}
/**
* Form validation handler for authorize_filetransfer_form().
* *
* @see authorize_filetransfer_form() * @see authorize_filetransfer_form()
* @see authorize_filetransfer_submit()
*/ */
function authorize_filetransfer_form_validate($form, &$form_state) { function authorize_filetransfer_form_validate($form, &$form_state) {
// Only validate the form if we have collected all of the user input and are
// ready to proceed with updating or installing.
if ($form_state['triggering_element']['#name'] != 'process_updates') {
return;
}
if (isset($form_state['values']['connection_settings'])) { if (isset($form_state['values']['connection_settings'])) {
$backend = $form_state['values']['connection_settings']['authorize_filetransfer_default']; $backend = $form_state['values']['connection_settings']['authorize_filetransfer_default'];
$filetransfer = authorize_get_filetransfer($backend, $form_state['values']['connection_settings'][$backend]); $filetransfer = authorize_get_filetransfer($backend, $form_state['values']['connection_settings'][$backend]);
...@@ -137,19 +213,25 @@ function authorize_filetransfer_form_validate($form, &$form_state) { ...@@ -137,19 +213,25 @@ function authorize_filetransfer_form_validate($form, &$form_state) {
$filetransfer->connect(); $filetransfer->connect();
} }
catch (Exception $e) { catch (Exception $e) {
form_set_error('connection_settings', $e->getMessage()); // The format of this error message is similar to that used on the
// database connection form in the installer.
form_set_error('connection_settings', t('Failed to connect to the server. The server reports the following message: !message For more help installing or updating code on your server, see the <a href="@handbook_url">handbook</a>.', array(
'!message' => '<p class="error">' . $e->getMessage() . '</p>',
'@handbook_url' => 'http://drupal.org/documentation/install/modules-themes',
)));
} }
} }
} }
/** /**
* Submit callback when a file transfer is being authorized. * Form submission handler for authorize_filetransfer_form().
* *
* @see authorize_filetransfer_form() * @see authorize_filetransfer_form()
* @see authorize_filetransfer_validate()
*/ */
function authorize_filetransfer_form_submit($form, &$form_state) { function authorize_filetransfer_form_submit($form, &$form_state) {
global $base_url; global $base_url;
switch ($form_state['clicked_button']['#name']) { switch ($form_state['triggering_element']['#name']) {
case 'process_updates': case 'process_updates':
// Save the connection settings to the DB. // Save the connection settings to the DB.
...@@ -205,7 +287,7 @@ function authorize_filetransfer_form_submit($form, &$form_state) { ...@@ -205,7 +287,7 @@ function authorize_filetransfer_form_submit($form, &$form_state) {
} }
/** /**
* Run the operation specified in $_SESSION['authorize_operation'] * Runs the operation specified in $_SESSION['authorize_operation'].
* *
* @param $filetransfer * @param $filetransfer
* The FileTransfer object to use for running the operation. * The FileTransfer object to use for running the operation.
...@@ -215,7 +297,7 @@ function authorize_run_operation($filetransfer) { ...@@ -215,7 +297,7 @@ function authorize_run_operation($filetransfer) {
unset($_SESSION['authorize_operation']); unset($_SESSION['authorize_operation']);
if (!empty($operation['page_title'])) { if (!empty($operation['page_title'])) {
drupal_set_title(check_plain($operation['page_title'])); drupal_set_title($operation['page_title']);
} }
require_once DRUPAL_ROOT . '/' . $operation['file']; require_once DRUPAL_ROOT . '/' . $operation['file'];
...@@ -223,21 +305,30 @@ function authorize_run_operation($filetransfer) { ...@@ -223,21 +305,30 @@ function authorize_run_operation($filetransfer) {
} }
/** /**
* Get a FileTransfer class for a specific transfer method and settings. * Gets a FileTransfer class for a specific transfer method and settings.
* *
* @param $backend * @param $backend
* The FileTransfer backend to get the class for. * The FileTransfer backend to get the class for.
* @param $settings * @param $settings
* Array of settings for the FileTransfer. * Array of settings for the FileTransfer.
*
* @return * @return
* An instantiated FileTransfer object for the requested method and settings, * An instantiated FileTransfer object for the requested method and settings,
* or FALSE if there was an error finding or instantiating it. * or FALSE if there was an error finding or instantiating it.
*/ */
function authorize_get_filetransfer($backend, $settings = array()) { function authorize_get_filetransfer($backend, $settings = array()) {
$filetransfer = FALSE; $filetransfer = FALSE;
if (!empty($_SESSION['authorize_filetransfer_backends'][$backend])) { if (!empty($_SESSION['authorize_filetransfer_info'][$backend])) {
$filetransfer = call_user_func_array(array($_SESSION['authorize_filetransfer_backends'][$backend]['class'], 'factory'), array(DRUPAL_ROOT, $settings)); $backend_info = $_SESSION['authorize_filetransfer_info'][$backend];
if (!empty($backend_info['file'])) {
$file = $backend_info['file path'] . '/' . $backend_info['file'];
require_once $file;
}
if (class_exists($backend_info['class'])) {
// PHP 5.2 doesn't support $class::factory() syntax, so we have to
// use call_user_func_array() until we can require PHP 5.3.
$filetransfer = call_user_func_array(array($backend_info['class'], 'factory'), array(DRUPAL_ROOT, $settings));
}
} }
return $filetransfer; return $filetransfer;
} }
<?php <?php
// $Id: batch.inc,v 1.53 2010/10/04 07:34:26 webchick Exp $
/** /**
* @file * @file
...@@ -22,6 +20,7 @@ ...@@ -22,6 +20,7 @@
* @param $id * @param $id
* The ID of the batch to load. When a progressive batch is being processed, * The ID of the batch to load. When a progressive batch is being processed,
* the relevant ID is found in $_REQUEST['id']. * the relevant ID is found in $_REQUEST['id'].
*
* @return * @return
* An array representing the batch, or FALSE if no batch was found. * An array representing the batch, or FALSE if no batch was found.
*/ */
...@@ -37,7 +36,7 @@ function batch_load($id) { ...@@ -37,7 +36,7 @@ function batch_load($id) {
} }
/** /**
* State-based dispatcher for the batch processing page. * Renders the batch processing page based on the current state of the batch.
* *
* @see _batch_shutdown() * @see _batch_shutdown()
*/ */
...@@ -95,7 +94,7 @@ function _batch_page() { ...@@ -95,7 +94,7 @@ function _batch_page() {
} }
/** /**
* Initialize the batch processing. * Initializes the batch processing.
* *
* JavaScript-enabled clients are identified by the 'has_js' cookie set in * JavaScript-enabled clients are identified by the 'has_js' cookie set in
* drupal.js. If no JavaScript-enabled page has been visited during the current * drupal.js. If no JavaScript-enabled page has been visited during the current
...@@ -111,7 +110,7 @@ function _batch_start() { ...@@ -111,7 +110,7 @@ function _batch_start() {
} }
/** /**
* Output a batch processing page with JavaScript support. * Outputs a batch processing page with JavaScript support.
* *
* This initializes the batch and error messages. Note that in JavaScript-based * This initializes the batch and error messages. Note that in JavaScript-based
* processing, the batch processing page is displayed only once and updated via * processing, the batch processing page is displayed only once and updated via
...@@ -139,14 +138,13 @@ function _batch_progress_page_js() { ...@@ -139,14 +138,13 @@ function _batch_progress_page_js() {
), ),
); );
drupal_add_js($js_setting, 'setting'); drupal_add_js($js_setting, 'setting');
drupal_add_js('misc/progress.js', array('cache' => FALSE)); drupal_add_library('system', 'drupal.batch');
drupal_add_js('misc/batch.js', array('cache' => FALSE));
return '<div id="progress"></div>'; return '<div id="progress"></div>';
} }
/** /**
* Do one execution pass in JavaScript-mode and return progress to the browser. * Does one execution pass with JavaScript and returns progress to the browser.
* *
* @see _batch_progress_page_js() * @see _batch_progress_page_js()
* @see _batch_process() * @see _batch_process()
...@@ -166,7 +164,7 @@ function _batch_do() { ...@@ -166,7 +164,7 @@ function _batch_do() {
} }
/** /**
* Output a batch processing page without JavaScript support. * Outputs a batch processing page without JavaScript support.
* *
* @see _batch_process() * @see _batch_process()
*/ */
...@@ -230,7 +228,7 @@ function _batch_progress_page_nojs() { ...@@ -230,7 +228,7 @@ function _batch_progress_page_nojs() {
} }
/** /**
* Process sets in a batch. * Processes sets in a batch.
* *
* If the batch was marked for progressive execution (default), this executes as * If the batch was marked for progressive execution (default), this executes as
* many operations in batch sets until an execution time of 1 second has been * many operations in batch sets until an execution time of 1 second has been
...@@ -341,6 +339,8 @@ function _batch_process() { ...@@ -341,6 +339,8 @@ function _batch_process() {
$progress_message = $old_set['progress_message']; $progress_message = $old_set['progress_message'];
} }
// Total progress is the number of operations that have fully run plus the
// completion level of the current operation.
$current = $total - $remaining + $finished; $current = $total - $remaining + $finished;
$percentage = _batch_api_percentage($total, $current); $percentage = _batch_api_percentage($total, $current);
$elapsed = isset($current_set['elapsed']) ? $current_set['elapsed'] : 0; $elapsed = isset($current_set['elapsed']) ? $current_set['elapsed'] : 0;
...@@ -370,17 +370,23 @@ function _batch_process() { ...@@ -370,17 +370,23 @@ function _batch_process() {
} }
/** /**
* Helper function for _batch_process(): returns the formatted percentage. * Formats the percent completion for a batch set.
* *
* @param $total * @param $total
* The total number of operations. * The total number of operations.
* @param $current * @param $current
* The number of the current operation. * The number of the current operation. This may be a floating point number
* rather than an integer in the case of a multi-step operation that is not
* yet complete; in that case, the fractional part of $current represents the
* fraction of the operation that has been completed.
*
* @return * @return
* The properly formatted percentage, as a string. We output percentages * The properly formatted percentage, as a string. We output percentages
* using the correct number of decimal places so that we never print "100%" * using the correct number of decimal places so that we never print "100%"
* until we are finished, but we also never print more decimal places than * until we are finished, but we also never print more decimal places than
* are meaningful. * are meaningful.
*
* @see _batch_process()
*/ */
function _batch_api_percentage($total, $current) { function _batch_api_percentage($total, $current) {
if (!$total || $total == $current) { if (!$total || $total == $current) {
...@@ -392,13 +398,22 @@ function _batch_api_percentage($total, $current) { ...@@ -392,13 +398,22 @@ function _batch_api_percentage($total, $current) {
// We add a new digit at 200, 2000, etc. (since, for example, 199/200 // We add a new digit at 200, 2000, etc. (since, for example, 199/200
// would round up to 100% if we didn't). // would round up to 100% if we didn't).
$decimal_places = max(0, floor(log10($total / 2.0)) - 1); $decimal_places = max(0, floor(log10($total / 2.0)) - 1);
do {
// Calculate the percentage to the specified number of decimal places.
$percentage = sprintf('%01.' . $decimal_places . 'f', round($current / $total * 100, $decimal_places)); $percentage = sprintf('%01.' . $decimal_places . 'f', round($current / $total * 100, $decimal_places));
// When $current is an integer, the above calculation will always be
// correct. However, if $current is a floating point number (in the case
// of a multi-step batch operation that is not yet complete), $percentage
// may be erroneously rounded up to 100%. To prevent that, we add one
// more decimal place and try again.
$decimal_places++;
} while ($percentage == '100');
} }
return $percentage; return $percentage;
} }
/** /**
* Return the batch set being currently processed. * Returns the batch set being currently processed.
*/ */
function &_batch_current_set() { function &_batch_current_set() {
$batch = &batch_get(); $batch = &batch_get();
...@@ -406,7 +421,7 @@ function &_batch_current_set() { ...@@ -406,7 +421,7 @@ function &_batch_current_set() {
} }
/** /**
* Retrieve the next set in a batch. * Retrieves the next set in a batch.
* *
* If there is a subsequent set in this batch, assign it as the new set to * If there is a subsequent set in this batch, assign it as the new set to
* process and execute its form submit handler (if defined), which may add * process and execute its form submit handler (if defined), which may add
...@@ -430,7 +445,7 @@ function _batch_next_set() { ...@@ -430,7 +445,7 @@ function _batch_next_set() {
} }
/** /**
* End the batch processing. * Ends the batch processing.
* *
* Call the 'finished' callback of each batch set to allow custom handling of * Call the 'finished' callback of each batch set to allow custom handling of
* the results and resolve page redirection. * the results and resolve page redirection.
...@@ -509,7 +524,10 @@ function _batch_finished() { ...@@ -509,7 +524,10 @@ function _batch_finished() {
} }
/** /**
* Shutdown function; store the current batch data for the next request. * Shutdown function: Stores the current batch data for the next request.
*
* @see _batch_page()
* @see drupal_register_shutdown_function()
*/ */
function _batch_shutdown() { function _batch_shutdown() {
if ($batch = batch_get()) { if ($batch = batch_get()) {
...@@ -519,4 +537,3 @@ function _batch_shutdown() { ...@@ -519,4 +537,3 @@ function _batch_shutdown() {
->execute(); ->execute();
} }
} }
<?php <?php
// $Id: batch.queue.inc,v 1.1 2010/01/08 06:36:34 webchick Exp $
/** /**
* @file * @file
* Queue handlers used by the Batch API. * Queue handlers used by the Batch API.
* *
* Those implementations: * These implementations:
* - ensure FIFO ordering, * - Ensure FIFO ordering.
* - let an item be repeatedly claimed until it is actually deleted (no notion * - Allow an item to be repeatedly claimed until it is actually deleted (no
* of lease time or 'expire' date), to allow multipass operations. * notion of lease time or 'expire' date), to allow multipass operations.
*/ */
/** /**
* Batch queue implementation. * Defines a batch queue.
* *
* Stale items from failed batches are cleaned from the {queue} table on cron * Stale items from failed batches are cleaned from the {queue} table on cron
* using the 'created' date. * using the 'created' date.
*/ */
class BatchQueue extends SystemQueue { class BatchQueue extends SystemQueue {
/**
* Overrides SystemQueue::claimItem().
*
* Unlike SystemQueue::claimItem(), this method provides a default lease
* time of 0 (no expiration) instead of 30. This allows the item to be
* claimed repeatedly until it is deleted.
*/
public function claimItem($lease_time = 0) { public function claimItem($lease_time = 0) {
$item = db_query('SELECT data, item_id FROM {queue} q WHERE name = :name ORDER BY item_id ASC', array(':name' => $this->name))->fetchObject(); $item = db_query_range('SELECT data, item_id FROM {queue} q WHERE name = :name ORDER BY item_id ASC', 0, 1, array(':name' => $this->name))->fetchObject();
if ($item) { if ($item) {
$item->data = unserialize($item->data); $item->data = unserialize($item->data);
return $item; return $item;
...@@ -30,9 +35,9 @@ class BatchQueue extends SystemQueue { ...@@ -30,9 +35,9 @@ class BatchQueue extends SystemQueue {
} }
/** /**
* Retrieve all remaining items in the queue. * Retrieves all remaining items in the queue.
* *
* This is specific to Batch API and is not part of the DrupalQueueInterface, * This is specific to Batch API and is not part of the DrupalQueueInterface.
*/ */
public function getAllItems() { public function getAllItems() {
$result = array(); $result = array();
...@@ -45,10 +50,17 @@ class BatchQueue extends SystemQueue { ...@@ -45,10 +50,17 @@ class BatchQueue extends SystemQueue {
} }
/** /**
* Batch queue implementation used for non-progressive batches. * Defines a batch queue for non-progressive batches.
*/ */
class BatchMemoryQueue extends MemoryQueue { class BatchMemoryQueue extends MemoryQueue {
/**
* Overrides MemoryQueue::claimItem().
*
* Unlike MemoryQueue::claimItem(), this method provides a default lease
* time of 0 (no expiration) instead of 30. This allows the item to be
* claimed repeatedly until it is deleted.
*/
public function claimItem($lease_time = 0) { public function claimItem($lease_time = 0) {
if (!empty($this->queue)) { if (!empty($this->queue)) {
reset($this->queue); reset($this->queue);
...@@ -58,9 +70,9 @@ class BatchMemoryQueue extends MemoryQueue { ...@@ -58,9 +70,9 @@ class BatchMemoryQueue extends MemoryQueue {
} }
/** /**
* Retrieve all remaining items in the queue. * Retrieves all remaining items in the queue.
* *
* This is specific to Batch API and is not part of the DrupalQueueInterface, * This is specific to Batch API and is not part of the DrupalQueueInterface.
*/ */
public function getAllItems() { public function getAllItems() {
$result = array(); $result = array();
......
<?php <?php
// $Id: bootstrap.inc,v 1.430 2010/10/23 05:30:57 webchick Exp $
/** /**
* @file * @file
...@@ -9,7 +8,7 @@ ...@@ -9,7 +8,7 @@
/** /**
* The current system version. * The current system version.
*/ */
define('VERSION', '7.0-beta2'); define('VERSION', '7.22');
/** /**
* Core API compatibility. * Core API compatibility.
...@@ -27,14 +26,19 @@ define('DRUPAL_MINIMUM_PHP', '5.2.4'); ...@@ -27,14 +26,19 @@ define('DRUPAL_MINIMUM_PHP', '5.2.4');
define('DRUPAL_MINIMUM_PHP_MEMORY_LIMIT', '32M'); define('DRUPAL_MINIMUM_PHP_MEMORY_LIMIT', '32M');
/** /**
* Minimum supported version of MySQL, if it is used. * Error reporting level: display no errors.
*/ */
define('DRUPAL_MINIMUM_MYSQL', '5.0.15'); define('ERROR_REPORTING_HIDE', 0);
/** /**
* Minimum supported version of PostgreSQL, if it is used. * Error reporting level: display errors and warnings.
*/ */
define('DRUPAL_MINIMUM_PGSQL', '8.3'); define('ERROR_REPORTING_DISPLAY_SOME', 1);
/**
* Error reporting level: display all messages.
*/
define('ERROR_REPORTING_DISPLAY_ALL', 2);
/** /**
* Indicates that the item should never be removed unless explicitly selected. * Indicates that the item should never be removed unless explicitly selected.
...@@ -49,93 +53,69 @@ define('CACHE_PERMANENT', 0); ...@@ -49,93 +53,69 @@ define('CACHE_PERMANENT', 0);
define('CACHE_TEMPORARY', -1); define('CACHE_TEMPORARY', -1);
/** /**
* Log message severity -- Emergency: system is unusable. * @defgroup logging_severity_levels Logging severity levels
* @{
* Logging severity levels as defined in RFC 3164.
* *
* The WATCHDOG_* constant definitions correspond to the logging severity levels * The WATCHDOG_* constant definitions correspond to the logging severity levels
* defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html * defined in RFC 3164, section 4.1.1. PHP supplies predefined LOG_* constants
* * for use in the syslog() function, but their values on Windows builds do not
* correspond to RFC 3164. The associated PHP bug report was closed with the
* comment, "And it's also not a bug, as Windows just have less log levels,"
* and "So the behavior you're seeing is perfectly normal."
*
* @see http://www.faqs.org/rfcs/rfc3164.html
* @see http://bugs.php.net/bug.php?id=18090
* @see http://php.net/manual/function.syslog.php
* @see http://php.net/manual/network.constants.php
* @see watchdog() * @see watchdog()
* @see watchdog_severity_levels() * @see watchdog_severity_levels()
*/ */
/**
* Log message severity -- Emergency: system is unusable.
*/
define('WATCHDOG_EMERGENCY', 0); define('WATCHDOG_EMERGENCY', 0);
/** /**
* Log message severity -- Alert: action must be taken immediately. * Log message severity -- Alert: action must be taken immediately.
*
* The WATCHDOG_* constant definitions correspond to the logging severity levels
* defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html
*
* @see watchdog()
* @see watchdog_severity_levels()
*/ */
define('WATCHDOG_ALERT', 1); define('WATCHDOG_ALERT', 1);
/** /**
* Log message severity -- Critical: critical conditions. * Log message severity -- Critical conditions.
*
* The WATCHDOG_* constant definitions correspond to the logging severity levels
* defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html
*
* @see watchdog()
* @see watchdog_severity_levels()
*/ */
define('WATCHDOG_CRITICAL', 2); define('WATCHDOG_CRITICAL', 2);
/** /**
* Log message severity -- Error: error conditions. * Log message severity -- Error conditions.
*
* The WATCHDOG_* constant definitions correspond to the logging severity levels
* defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html
*
* @see watchdog()
* @see watchdog_severity_levels()
*/ */
define('WATCHDOG_ERROR', 3); define('WATCHDOG_ERROR', 3);
/** /**
* Log message severity -- Warning: warning conditions. * Log message severity -- Warning conditions.
*
* The WATCHDOG_* constant definitions correspond to the logging severity levels
* defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html
*
* @see watchdog()
* @see watchdog_severity_levels()
*/ */
define('WATCHDOG_WARNING', 4); define('WATCHDOG_WARNING', 4);
/** /**
* Log message severity -- Notice: normal but significant condition. * Log message severity -- Normal but significant conditions.
*
* The WATCHDOG_* constant definitions correspond to the logging severity levels
* defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html
*
* @see watchdog()
* @see watchdog_severity_levels()
*/ */
define('WATCHDOG_NOTICE', 5); define('WATCHDOG_NOTICE', 5);
/** /**
* Log message severity -- Informational: informational messages. * Log message severity -- Informational messages.
*
* The WATCHDOG_* constant definitions correspond to the logging severity levels
* defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html
*
* @see watchdog()
* @see watchdog_severity_levels()
*/ */
define('WATCHDOG_INFO', 6); define('WATCHDOG_INFO', 6);
/** /**
* Log message severity -- Debug: debug-level messages. * Log message severity -- Debug-level messages.
*
* The WATCHDOG_* constant definitions correspond to the logging severity levels
* defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html
*
* @see watchdog()
* @see watchdog_severity_levels()
*/ */
define('WATCHDOG_DEBUG', 7); define('WATCHDOG_DEBUG', 7);
/**
* @} End of "defgroup logging_severity_levels".
*/
/** /**
* First bootstrap phase: initialize configuration. * First bootstrap phase: initialize configuration.
*/ */
...@@ -172,8 +152,7 @@ define('DRUPAL_BOOTSTRAP_PAGE_HEADER', 5); ...@@ -172,8 +152,7 @@ define('DRUPAL_BOOTSTRAP_PAGE_HEADER', 5);
define('DRUPAL_BOOTSTRAP_LANGUAGE', 6); define('DRUPAL_BOOTSTRAP_LANGUAGE', 6);
/** /**
* Final bootstrap phase: Drupal is fully loaded; validate and fix * Final bootstrap phase: Drupal is fully loaded; validate and fix input data.
* input data.
*/ */
define('DRUPAL_BOOTSTRAP_FULL', 7); define('DRUPAL_BOOTSTRAP_FULL', 7);
...@@ -188,8 +167,9 @@ define('DRUPAL_ANONYMOUS_RID', 1); ...@@ -188,8 +167,9 @@ define('DRUPAL_ANONYMOUS_RID', 1);
define('DRUPAL_AUTHENTICATED_RID', 2); define('DRUPAL_AUTHENTICATED_RID', 2);
/** /**
* The number of bytes in a kilobyte. For more information, visit * The number of bytes in a kilobyte.
* http://en.wikipedia.org/wiki/Kilobyte. *
* For more information, visit http://en.wikipedia.org/wiki/Kilobyte.
*/ */
define('DRUPAL_KILOBYTE', 1024); define('DRUPAL_KILOBYTE', 1024);
...@@ -226,9 +206,16 @@ define('LANGUAGE_LTR', 0); ...@@ -226,9 +206,16 @@ define('LANGUAGE_LTR', 0);
define('LANGUAGE_RTL', 1); define('LANGUAGE_RTL', 1);
/** /**
* For convenience, define a short form of the request time global. * Time of the current request in seconds elapsed since the Unix Epoch.
*
* This differs from $_SERVER['REQUEST_TIME'], which is stored as a float
* since PHP 5.4.0. Float timestamps confuse most PHP functions
* (including date_create()).
*
* @see http://php.net/manual/reserved.variables.server.php
* @see http://php.net/manual/function.time.php
*/ */
define('REQUEST_TIME', $_SERVER['REQUEST_TIME']); define('REQUEST_TIME', (int) $_SERVER['REQUEST_TIME']);
/** /**
* Flag for drupal_set_title(); text is not sanitized, so run check_plain(). * Flag for drupal_set_title(); text is not sanitized, so run check_plain().
...@@ -251,10 +238,219 @@ define('REGISTRY_RESET_LOOKUP_CACHE', 1); ...@@ -251,10 +238,219 @@ define('REGISTRY_RESET_LOOKUP_CACHE', 1);
define('REGISTRY_WRITE_LOOKUP_CACHE', 2); define('REGISTRY_WRITE_LOOKUP_CACHE', 2);
/** /**
* Start the timer with the specified name. If you start and stop the same * Regular expression to match PHP function names.
* timer multiple times, the measured intervals will be accumulated. *
* @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]*');
/**
* Provides a caching wrapper to be used in place of large array structures.
*
* This class should be extended by systems that need to cache large amounts
* of data and have it represented as an array to calling functions. These
* arrays can become very large, so ArrayAccess is used to allow different
* strategies to be used for caching internally (lazy loading, building caches
* over time etc.). This can dramatically reduce the amount of data that needs
* to be loaded from cache backends on each request, and memory usage from
* static caches of that same data.
*
* Note that array_* functions do not work with ArrayAccess. Systems using
* DrupalCacheArray should use this only internally. If providing API functions
* that return the full array, this can be cached separately or returned
* directly. However since DrupalCacheArray holds partial content by design, it
* should be a normal PHP array or otherwise contain the full structure.
*
* Note also that due to limitations in PHP prior to 5.3.4, it is impossible to
* write directly to the contents of nested arrays contained in this object.
* Only writes to the top-level array elements are possible. So if you
* previously had set $object['foo'] = array(1, 2, 'bar' => 'baz'), but later
* want to change the value of 'bar' from 'baz' to 'foobar', you cannot do so
* a targeted write like $object['foo']['bar'] = 'foobar'. Instead, you must
* overwrite the entire top-level 'foo' array with the entire set of new
* values: $object['foo'] = array(1, 2, 'bar' => 'foobar'). Due to this same
* limitation, attempts to create references to any contained data, nested or
* otherwise, will fail silently. So $var = &$object['foo'] will not throw an
* error, and $var will be populated with the contents of $object['foo'], but
* that data will be passed by value, not reference. For more information on
* the PHP limitation, see the note in the official PHP documentation at·
* http://php.net/manual/en/arrayaccess.offsetget.php on
* ArrayAccess::offsetGet().
*
* By default, the class accounts for caches where calling functions might
* request keys in the array that won't exist even after a cache rebuild. This
* prevents situations where a cache rebuild would be triggered over and over
* due to a 'missing' item. These cases are stored internally as a value of
* NULL. This means that the offsetGet() and offsetExists() methods
* must be overridden if caching an array where the top level values can
* legitimately be NULL, and where $object->offsetExists() needs to correctly
* return (equivalent to array_key_exists() vs. isset()). This should not
* be necessary in the majority of cases.
*
* Classes extending this class must override at least the
* resolveCacheMiss() method to have a working implementation.
* *
* @param name * offsetSet() is not overridden by this class by default. In practice this
* means that assigning an offset via arrayAccess will only apply while the
* object is in scope and will not be written back to the persistent cache.
* This follows a similar pattern to static vs. persistent caching in
* procedural code. Extending classes may wish to alter this behavior, for
* example by overriding offsetSet() and adding an automatic call to persist().
*
* @see SchemaCache
*/
abstract class DrupalCacheArray implements ArrayAccess {
/**
* A cid to pass to cache_set() and cache_get().
*/
protected $cid;
/**
* A bin to pass to cache_set() and cache_get().
*/
protected $bin;
/**
* An array of keys to add to the cache at the end of the request.
*/
protected $keysToPersist = array();
/**
* Storage for the data itself.
*/
protected $storage = array();
/**
* Constructs a DrupalCacheArray object.
*
* @param $cid
* The cid for the array being cached.
* @param $bin
* The bin to cache the array.
*/
public function __construct($cid, $bin) {
$this->cid = $cid;
$this->bin = $bin;
if ($cached = cache_get($this->cid, $this->bin)) {
$this->storage = $cached->data;
}
}
/**
* Implements ArrayAccess::offsetExists().
*/
public function offsetExists($offset) {
return $this->offsetGet($offset) !== NULL;
}
/**
* Implements ArrayAccess::offsetGet().
*/
public function offsetGet($offset) {
if (isset($this->storage[$offset]) || array_key_exists($offset, $this->storage)) {
return $this->storage[$offset];
}
else {
return $this->resolveCacheMiss($offset);
}
}
/**
* Implements ArrayAccess::offsetSet().
*/
public function offsetSet($offset, $value) {
$this->storage[$offset] = $value;
}
/**
* Implements ArrayAccess::offsetUnset().
*/
public function offsetUnset($offset) {
unset($this->storage[$offset]);
}
/**
* Flags an offset value to be written to the persistent cache.
*
* If a value is assigned to a cache object with offsetSet(), by default it
* will not be written to the persistent cache unless it is flagged with this
* method. This allows items to be cached for the duration of a request,
* without necessarily writing back to the persistent cache at the end.
*
* @param $offset
* The array offset that was request.
* @param $persist
* Optional boolean to specify whether the offset should be persisted or
* not, defaults to TRUE. When called with $persist = FALSE the offset will
* be unflagged so that it will not written at the end of the request.
*/
protected function persist($offset, $persist = TRUE) {
$this->keysToPersist[$offset] = $persist;
}
/**
* Resolves a cache miss.
*
* When an offset is not found in the object, this is treated as a cache
* miss. This method allows classes implementing the interface to look up
* the actual value and allow it to be cached.
*
* @param $offset
* The offset that was requested.
*
* @return
* The value of the offset, or NULL if no value was found.
*/
abstract protected function resolveCacheMiss($offset);
/**
* Writes a value to the persistent cache immediately.
*
* @param $data
* The data to write to the persistent cache.
* @param $lock
* Whether to acquire a lock before writing to cache.
*/
protected function set($data, $lock = TRUE) {
// Lock cache writes to help avoid stampedes.
// To implement locking for cache misses, override __construct().
$lock_name = $this->cid . ':' . $this->bin;
if (!$lock || lock_acquire($lock_name)) {
if ($cached = cache_get($this->cid, $this->bin)) {
$data = $cached->data + $data;
}
cache_set($this->cid, $data, $this->bin);
if ($lock) {
lock_release($lock_name);
}
}
}
/**
* Destructs the DrupalCacheArray object.
*/
public function __destruct() {
$data = array();
foreach ($this->keysToPersist as $offset => $persist) {
if ($persist) {
$data[$offset] = $this->storage[$offset];
}
}
if (!empty($data)) {
$this->set($data);
}
}
}
/**
* Starts the timer with the specified name.
*
* If you start and stop the same timer multiple times, the measured intervals
* will be accumulated.
*
* @param $name
* The name of the timer. * The name of the timer.
*/ */
function timer_start($name) { function timer_start($name) {
...@@ -265,10 +461,11 @@ function timer_start($name) { ...@@ -265,10 +461,11 @@ function timer_start($name) {
} }
/** /**
* Read the current timer value without stopping the timer. * Reads the current timer value without stopping the timer.
* *
* @param name * @param $name
* The name of the timer. * The name of the timer.
*
* @return * @return
* The current timer value in ms. * The current timer value in ms.
*/ */
...@@ -288,10 +485,11 @@ function timer_read($name) { ...@@ -288,10 +485,11 @@ function timer_read($name) {
} }
/** /**
* Stop the timer with the specified name. * Stops the timer with the specified name.
* *
* @param name * @param $name
* The name of the timer. * The name of the timer.
*
* @return * @return
* A timer array. The array contains the number of times the timer has been * 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). * started and stopped (count) and the accumulated timer value in ms (time).
...@@ -315,68 +513,26 @@ function timer_stop($name) { ...@@ -315,68 +513,26 @@ function timer_stop($name) {
} }
/** /**
* Find the appropriate configuration directory. * Returns 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 * Returns the configuration path based on the site's hostname, port, and
* 6. $confdir/www.drupal.org.mysite * pathname. Uses find_conf_path() to find the current configuration directory.
* 7. $confdir/drupal.org.mysite * See default.settings.php for examples on how the URL is converted to a
* 8. $confdir/org.mysite * directory.
* *
* 9. $confdir/8080.www.drupal.org * @param bool $require_settings
* 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 * Only configuration directories with an existing settings.php file
* will be recognized. Defaults to TRUE. During initial installation, * will be recognized. Defaults to TRUE. During initial installation,
* this is set to FALSE so that Drupal can detect a matching directory, * this is set to FALSE so that Drupal can detect a matching directory,
* then create a new settings.php file in it. * then create a new settings.php file in it.
* @param reset * @param bool $reset
* Force a full search for matching directories even if one had been * Force a full search for matching directories even if one had been
* found previously. * found previously. Defaults to FALSE.
*
* @return * @return
* The path of the matching directory. * The path of the matching directory.
*
* @see default.settings.php
*/ */
function conf_path($require_settings = TRUE, $reset = FALSE) { function conf_path($require_settings = TRUE, $reset = FALSE) {
$conf = &drupal_static(__FUNCTION__, ''); $conf = &drupal_static(__FUNCTION__, '');
...@@ -388,6 +544,9 @@ function conf_path($require_settings = TRUE, $reset = FALSE) { ...@@ -388,6 +544,9 @@ function conf_path($require_settings = TRUE, $reset = FALSE) {
$confdir = 'sites'; $confdir = 'sites';
$sites = array(); $sites = array();
// UNL Change
$default_domains = array();
// End UNL Change
if (file_exists(DRUPAL_ROOT . '/' . $confdir . '/sites.php')) { if (file_exists(DRUPAL_ROOT . '/' . $confdir . '/sites.php')) {
// This will overwrite $sites with the desired mappings. // This will overwrite $sites with the desired mappings.
include(DRUPAL_ROOT . '/' . $confdir . '/sites.php'); include(DRUPAL_ROOT . '/' . $confdir . '/sites.php');
...@@ -398,6 +557,21 @@ function conf_path($require_settings = TRUE, $reset = FALSE) { ...@@ -398,6 +557,21 @@ function conf_path($require_settings = TRUE, $reset = FALSE) {
for ($i = count($uri) - 1; $i > 0; $i--) { for ($i = count($uri) - 1; $i > 0; $i--) {
for ($j = count($server); $j > 0; $j--) { for ($j = count($server); $j > 0; $j--) {
$dir = implode('.', array_slice($server, -$j)) . implode('.', array_slice($uri, 0, $i)); $dir = implode('.', array_slice($server, -$j)) . implode('.', array_slice($uri, 0, $i));
// UNL Change
// Since we're truncating site_dir domains to just unl.edu, we need to skip any site_dir that
// Starts with "unl.edu" unless we're on the default site's domain (ie: unlcms.unl.edu)
if (substr($dir, 0, 7) == 'unl.edu' && count($default_domains) > 0) {
$is_primary_domain = FALSE;
foreach ($default_domains as $default_domain) {
if (substr($_SERVER['HTTP_HOST'], 0, strlen($default_domain)) == $default_domain) {
$is_primary_domain = TRUE;
}
}
if (!$is_primary_domain) {
continue;
}
}
// End UNL Change
if (isset($sites[$dir]) && file_exists(DRUPAL_ROOT . '/' . $confdir . '/' . $sites[$dir])) { if (isset($sites[$dir]) && file_exists(DRUPAL_ROOT . '/' . $confdir . '/' . $sites[$dir])) {
$dir = $sites[$dir]; $dir = $sites[$dir];
} }
...@@ -412,7 +586,7 @@ function conf_path($require_settings = TRUE, $reset = FALSE) { ...@@ -412,7 +586,7 @@ function conf_path($require_settings = TRUE, $reset = FALSE) {
} }
/** /**
* Set appropriate server variables needed for command line scripts to work. * Sets appropriate server variables needed for command line scripts to work.
* *
* This function can be called by command line scripts before bootstrapping * This function can be called by command line scripts before bootstrapping
* Drupal, to ensure that the page loads with the desired server parameters. * Drupal, to ensure that the page loads with the desired server parameters.
...@@ -446,21 +620,23 @@ function conf_path($require_settings = TRUE, $reset = FALSE) { ...@@ -446,21 +620,23 @@ function conf_path($require_settings = TRUE, $reset = FALSE) {
* @see ip_address() * @see ip_address()
*/ */
function drupal_override_server_variables($variables = array()) { function drupal_override_server_variables($variables = array()) {
// Set defaults based on the provided URL. // Allow the provided URL to override any existing values in $_SERVER.
if (isset($variables['url'])) { if (isset($variables['url'])) {
$url = parse_url($variables['url']); $url = parse_url($variables['url']);
unset($variables['url']); if (isset($url['host'])) {
$_SERVER['HTTP_HOST'] = $url['host'];
} }
else { if (isset($url['path'])) {
$url = array(); $_SERVER['SCRIPT_NAME'] = $url['path'];
} }
$url += array( unset($variables['url']);
'path' => '', }
'host' => 'localhost', // 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( $defaults = array(
'HTTP_HOST' => $url['host'], 'HTTP_HOST' => 'localhost',
'SCRIPT_NAME' => $url['path'], 'SCRIPT_NAME' => NULL,
'REMOTE_ADDR' => '127.0.0.1', 'REMOTE_ADDR' => '127.0.0.1',
'REQUEST_METHOD' => 'GET', 'REQUEST_METHOD' => 'GET',
'SERVER_NAME' => NULL, 'SERVER_NAME' => NULL,
...@@ -472,7 +648,7 @@ function drupal_override_server_variables($variables = array()) { ...@@ -472,7 +648,7 @@ function drupal_override_server_variables($variables = array()) {
} }
/** /**
* Initialize PHP environment. * Initializes the PHP environment.
*/ */
function drupal_environment_initialize() { function drupal_environment_initialize() {
if (!isset($_SERVER['HTTP_REFERER'])) { if (!isset($_SERVER['HTTP_REFERER'])) {
...@@ -513,8 +689,6 @@ function drupal_environment_initialize() { ...@@ -513,8 +689,6 @@ function drupal_environment_initialize() {
// sites/default/default.settings.php contains more runtime settings. // sites/default/default.settings.php contains more runtime settings.
// The .htaccess file contains settings that cannot be changed at runtime. // The .htaccess file contains settings that cannot be changed at runtime.
// Prevent PHP from generating HTML error messages.
ini_set('html_errors', 0);
// Don't escape quotes when reading files from the database, disk, etc. // Don't escape quotes when reading files from the database, disk, etc.
ini_set('magic_quotes_runtime', '0'); ini_set('magic_quotes_runtime', '0');
// Use session cookies, not transparent sessions that puts the session id in // Use session cookies, not transparent sessions that puts the session id in
...@@ -533,7 +707,7 @@ function drupal_environment_initialize() { ...@@ -533,7 +707,7 @@ function drupal_environment_initialize() {
} }
/** /**
* Validate that a hostname (for example $_SERVER['HTTP_HOST']) is safe. * Validates that a hostname (for example $_SERVER['HTTP_HOST']) is safe.
* *
* @return * @return
* TRUE if only containing valid characters, or FALSE otherwise. * TRUE if only containing valid characters, or FALSE otherwise.
...@@ -543,16 +717,21 @@ function drupal_valid_http_host($host) { ...@@ -543,16 +717,21 @@ function drupal_valid_http_host($host) {
} }
/** /**
* Loads the configuration and sets the base URL, cookie domain, and * Sets the base URL, cookie domain, and session name from configuration.
* session name correctly.
*/ */
function drupal_settings_initialize() { function drupal_settings_initialize() {
global $base_url, $base_path, $base_root; global $base_url, $base_path, $base_root;
// Export the following settings.php variables to the global namespace // Export these settings.php variables to the global namespace.
global $databases, $cookie_domain, $conf, $installed_profile, $update_free_access, $db_url, $db_prefix, $drupal_hash_salt, $is_https, $base_secure_url, $base_insecure_url; 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(); $conf = array();
// UNL change: include a "global" settings file that applies to all sites.
if (file_exists(DRUPAL_ROOT . '/sites/all/settings.php')) {
include_once DRUPAL_ROOT . '/sites/all/settings.php';
}
// End UNL change.
if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) { if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) {
include_once DRUPAL_ROOT . '/' . conf_path() . '/settings.php'; include_once DRUPAL_ROOT . '/' . conf_path() . '/settings.php';
} }
...@@ -561,7 +740,6 @@ function drupal_settings_initialize() { ...@@ -561,7 +740,6 @@ function drupal_settings_initialize() {
if (isset($base_url)) { if (isset($base_url)) {
// Parse fixed base URL from settings.php. // Parse fixed base URL from settings.php.
$parts = parse_url($base_url); $parts = parse_url($base_url);
$http_protocol = $parts['scheme'];
if (!isset($parts['path'])) { if (!isset($parts['path'])) {
$parts['path'] = ''; $parts['path'] = '';
} }
...@@ -570,7 +748,7 @@ function drupal_settings_initialize() { ...@@ -570,7 +748,7 @@ function drupal_settings_initialize() {
$base_root = substr($base_url, 0, strlen($base_url) - strlen($parts['path'])); $base_root = substr($base_url, 0, strlen($base_url) - strlen($parts['path']));
} }
else { else {
// Create base URL // Create base URL.
$http_protocol = $is_https ? 'https' : 'http'; $http_protocol = $is_https ? 'https' : 'http';
$base_root = $http_protocol . '://' . $_SERVER['HTTP_HOST']; $base_root = $http_protocol . '://' . $_SERVER['HTTP_HOST'];
...@@ -596,7 +774,7 @@ function drupal_settings_initialize() { ...@@ -596,7 +774,7 @@ function drupal_settings_initialize() {
} }
else { else {
// Otherwise use $base_url as session name, without the protocol // Otherwise use $base_url as session name, without the protocol
// to use the same session identifiers across http and https. // to use the same session identifiers across HTTP and HTTPS.
list( , $session_name) = explode('://', $base_url, 2); list( , $session_name) = explode('://', $base_url, 2);
// HTTP_HOST can be modified by a visitor, but we already sanitized it // HTTP_HOST can be modified by a visitor, but we already sanitized it
// in drupal_settings_initialize(). // in drupal_settings_initialize().
...@@ -630,9 +808,10 @@ function drupal_settings_initialize() { ...@@ -630,9 +808,10 @@ function drupal_settings_initialize() {
} }
/** /**
* Returns and optionally sets the filename for a system item (module, * Returns and optionally sets the filename for a system resource.
* theme, etc.). The filename, whether provided, cached, or retrieved *
* from the database, is only returned if the file exists. * 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 * 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 * and themes) to be located in different places depending on a site's
...@@ -655,13 +834,18 @@ function drupal_settings_initialize() { ...@@ -655,13 +834,18 @@ function drupal_settings_initialize() {
* than by consulting the database. * than by consulting the database.
* *
* @return * @return
* The filename of the requested item. * The filename of the requested item or NULL if the item is not found.
*/ */
function drupal_get_filename($type, $name, $filename = NULL) { function drupal_get_filename($type, $name, $filename = NULL) {
// The location of files will not change during the request, so do not use // The location of files will not change during the request, so do not use
// drupal_static(). // drupal_static().
static $files = array(); static $files = array(), $dirs = array();
// Profiles are a special case: they have a fixed location and naming.
if ($type == 'profile') {
$profile_filename = "profiles/$name/$name.profile";
$files[$type][$name] = file_exists($profile_filename) ? $profile_filename : FALSE;
}
if (!isset($files[$type])) { if (!isset($files[$type])) {
$files[$type] = array(); $files[$type] = array();
} }
...@@ -706,6 +890,8 @@ function drupal_get_filename($type, $name, $filename = NULL) { ...@@ -706,6 +890,8 @@ function drupal_get_filename($type, $name, $filename = NULL) {
$extension = $type; $extension = $type;
} }
if (!isset($dirs[$dir][$extension])) {
$dirs[$dir][$extension] = TRUE;
if (!function_exists('drupal_system_listing')) { if (!function_exists('drupal_system_listing')) {
require_once DRUPAL_ROOT . '/includes/common.inc'; require_once DRUPAL_ROOT . '/includes/common.inc';
} }
...@@ -713,12 +899,13 @@ function drupal_get_filename($type, $name, $filename = NULL) { ...@@ -713,12 +899,13 @@ function drupal_get_filename($type, $name, $filename = NULL) {
// extension, not just the file we are currently looking for. This // extension, not just the file we are currently looking for. This
// prevents unnecessary scans from being repeated when this function is // prevents unnecessary scans from being repeated when this function is
// called more than once in the same page request. // called more than once in the same page request.
$matches = drupal_system_listing("/\.$extension$/", $dir, 'name', 0); $matches = drupal_system_listing("/^" . DRUPAL_PHP_FUNCTION_PATTERN . "\.$extension$/", $dir, 'name', 0);
foreach ($matches as $matched_name => $file) { foreach ($matches as $matched_name => $file) {
$files[$type][$matched_name] = $file->uri; $files[$type][$matched_name] = $file->uri;
} }
} }
} }
}
if (isset($files[$type][$name])) { if (isset($files[$type][$name])) {
return $files[$type][$name]; return $files[$type][$name];
...@@ -726,11 +913,11 @@ function drupal_get_filename($type, $name, $filename = NULL) { ...@@ -726,11 +913,11 @@ function drupal_get_filename($type, $name, $filename = NULL) {
} }
/** /**
* Load the persistent variable table. * Loads the persistent variable table.
* *
* The variable table is composed of values that have been saved in the 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 * with variable_set() as well as those explicitly specified in the
* file. * configuration file.
*/ */
function variable_initialize($conf = array()) { function variable_initialize($conf = array()) {
// NOTE: caching the variables improves performance by 20% when serving // NOTE: caching the variables improves performance by 20% when serving
...@@ -775,7 +962,7 @@ function variable_initialize($conf = array()) { ...@@ -775,7 +962,7 @@ function variable_initialize($conf = array()) {
* The default value to use if this variable has never been set. * The default value to use if this variable has never been set.
* *
* @return * @return
* The value of the variable. * The value of the variable. Unserialization is taken care of as necessary.
* *
* @see variable_del() * @see variable_del()
* @see variable_set() * @see variable_set()
...@@ -837,7 +1024,7 @@ function variable_del($name) { ...@@ -837,7 +1024,7 @@ function variable_del($name) {
} }
/** /**
* Retrieve the current page from the cache. * Retrieves the current page from the cache.
* *
* Note: we do not serve cached pages to authenticated users, or to anonymous * Note: we do not serve cached pages to authenticated users, or to anonymous
* users when $_SESSION is non-empty. $_SESSION may contain status messages * users when $_SESSION is non-empty. $_SESSION may contain status messages
...@@ -869,7 +1056,7 @@ function drupal_page_get_cache($check_only = FALSE) { ...@@ -869,7 +1056,7 @@ function drupal_page_get_cache($check_only = FALSE) {
} }
/** /**
* Determine the cacheability of the current page. * Determines the cacheability of the current page.
* *
* @param $allow_caching * @param $allow_caching
* Set to FALSE if you want to prevent this page to get cached. * Set to FALSE if you want to prevent this page to get cached.
...@@ -888,7 +1075,7 @@ function drupal_page_is_cacheable($allow_caching = NULL) { ...@@ -888,7 +1075,7 @@ function drupal_page_is_cacheable($allow_caching = NULL) {
} }
/** /**
* Invoke a bootstrap hook in all bootstrap modules that implement it. * Invokes a bootstrap hook in all bootstrap modules that implement it.
* *
* @param $hook * @param $hook
* The name of the bootstrap hook to invoke. * The name of the bootstrap hook to invoke.
...@@ -897,7 +1084,12 @@ function drupal_page_is_cacheable($allow_caching = NULL) { ...@@ -897,7 +1084,12 @@ function drupal_page_is_cacheable($allow_caching = NULL) {
*/ */
function bootstrap_invoke_all($hook) { function bootstrap_invoke_all($hook) {
// Bootstrap modules should have been loaded when this function is called, so // Bootstrap modules should have been loaded when this function is called, so
// we don't need to tell module_list() to reset its bootstrap list. // 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) { foreach (module_list(FALSE, TRUE) as $module) {
drupal_load('module', $module); drupal_load('module', $module);
module_invoke($module, $hook); module_invoke($module, $hook);
...@@ -905,8 +1097,9 @@ function bootstrap_invoke_all($hook) { ...@@ -905,8 +1097,9 @@ function bootstrap_invoke_all($hook) {
} }
/** /**
* Includes a file with the provided type and name. This prevents * Includes a file with the provided type and name.
* including a theme, engine, module, etc., more than once. *
* This prevents including a theme, engine, module, etc., more than once.
* *
* @param $type * @param $type
* The type of item to load (i.e. theme, theme_engine, module). * The type of item to load (i.e. theme, theme_engine, module).
...@@ -938,7 +1131,7 @@ function drupal_load($type, $name) { ...@@ -938,7 +1131,7 @@ function drupal_load($type, $name) {
} }
/** /**
* Set an HTTP response header for the current page. * Sets an HTTP response header for the current page.
* *
* Note: When sending a Content-Type header, always include a 'charset' type, * 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). * too. This is necessary to avoid security bugs (e.g. UTF-7 XSS).
...@@ -974,11 +1167,12 @@ function drupal_add_http_header($name, $value, $append = FALSE) { ...@@ -974,11 +1167,12 @@ function drupal_add_http_header($name, $value, $append = FALSE) {
} }
/** /**
* Get the HTTP response headers for the current page. * Gets the HTTP response headers for the current page.
* *
* @param $name * @param $name
* An HTTP header name. If omitted, all headers are returned as name/value * 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. * pairs. If an array value is FALSE, the header has been unset.
*
* @return * @return
* A string containing the header value, or FALSE if the header has been set, * A string containing the header value, or FALSE if the header has been set,
* or NULL if the header has not been set. * or NULL if the header has not been set.
...@@ -995,6 +1189,8 @@ function drupal_get_http_header($name = NULL) { ...@@ -995,6 +1189,8 @@ function drupal_get_http_header($name = NULL) {
} }
/** /**
* Sets the preferred name for the HTTP header.
*
* Header names are case-insensitive, but for maximum compatibility they should * Header names are case-insensitive, but for maximum compatibility they should
* follow "common form" (see RFC 2617, section 4.2). * follow "common form" (see RFC 2617, section 4.2).
*/ */
...@@ -1008,14 +1204,16 @@ function _drupal_set_preferred_header_name($name = NULL) { ...@@ -1008,14 +1204,16 @@ function _drupal_set_preferred_header_name($name = NULL) {
} }
/** /**
* Send the HTTP response headers previously set using drupal_add_http_header(). * Sends the HTTP response headers that were previously set, adding defaults.
* Add default headers, unless they have been replaced or unset using
* drupal_add_http_header().
* *
* @param $default_headers * Headers are set in drupal_add_http_header(). Default headers are not set
* An array of headers as name/value pairs. * if they have been replaced or unset using drupal_add_http_header().
* @param $single *
* If TRUE and headers have already be sent, send only the specified header. * @param array $default_headers
* (optional) An array of headers as name/value pairs.
* @param bool $only_default
* (optional) If TRUE and headers have already been sent, send only the
* specified headers.
*/ */
function drupal_send_headers($default_headers = array(), $only_default = FALSE) { function drupal_send_headers($default_headers = array(), $only_default = FALSE) {
$headers_sent = &drupal_static(__FUNCTION__, FALSE); $headers_sent = &drupal_static(__FUNCTION__, FALSE);
...@@ -1038,14 +1236,14 @@ function drupal_send_headers($default_headers = array(), $only_default = FALSE) ...@@ -1038,14 +1236,14 @@ function drupal_send_headers($default_headers = array(), $only_default = FALSE)
header($_SERVER['SERVER_PROTOCOL'] . ' ' . $value); header($_SERVER['SERVER_PROTOCOL'] . ' ' . $value);
} }
// Skip headers that have been unset. // Skip headers that have been unset.
elseif ($value) { elseif ($value !== FALSE) {
header($header_names[$name_lower] . ': ' . $value); header($header_names[$name_lower] . ': ' . $value);
} }
} }
} }
/** /**
* Set HTTP headers in preparation for a page response. * Sets HTTP headers in preparation for a page response.
* *
* Authenticated users are always given a 'no-cache' header, and will fetch a * Authenticated users are always given a 'no-cache' header, and will fetch a
* fresh page on every request. This prevents authenticated users from seeing * fresh page on every request. This prevents authenticated users from seeing
...@@ -1088,7 +1286,7 @@ function drupal_page_header() { ...@@ -1088,7 +1286,7 @@ function drupal_page_header() {
} }
/** /**
* Set HTTP headers in preparation for a cached page response. * Sets HTTP headers in preparation for a cached page response.
* *
* The headers allow as much as possible in proxies and browsers without any * The headers allow as much as possible in proxies and browsers without any
* particular knowledge about the pages. Modules can override these headers * particular knowledge about the pages. Modules can override these headers
...@@ -1121,13 +1319,12 @@ function drupal_serve_page_from_cache(stdClass $cache) { ...@@ -1121,13 +1319,12 @@ function drupal_serve_page_from_cache(stdClass $cache) {
} }
} }
// If a cache is served from a HTTP proxy without hitting the web server, // If the client sent a session cookie, a cached copy will only be served
// the boot and exit hooks cannot be fired, so only allow caching in // to that one particular client due to Vary: Cookie. Thus, do not set
// proxies if boot hooks are disabled. If the client send a session cookie, // max-age > 0, allowing the page to be cached by external proxies, when a
// do not bother caching the page in a public proxy, because the cached copy // session cookie is present unless the Vary header has been replaced or
// will only be served to that particular user due to Vary: Cookie, unless // unset in hook_boot().
// the Vary header has been replaced or unset in hook_boot() (see below). $max_age = !isset($_COOKIE[session_name()]) || isset($hook_boot_headers['vary']) ? variable_get('page_cache_maximum_age', 0) : 0;
$max_age = !variable_get('page_cache_invoke_hooks', TRUE) && (!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; $default_headers['Cache-Control'] = 'public, max-age=' . $max_age;
// Entity tag should change if the output changes. // Entity tag should change if the output changes.
...@@ -1168,7 +1365,9 @@ function drupal_serve_page_from_cache(stdClass $cache) { ...@@ -1168,7 +1365,9 @@ function drupal_serve_page_from_cache(stdClass $cache) {
// revalidation. If a Vary header has been set in hook_boot(), it is assumed // revalidation. If a Vary header has been set in hook_boot(), it is assumed
// that the module knows how to cache the page. // that the module knows how to cache the page.
if (!isset($hook_boot_headers['vary']) && !variable_get('omit_vary_cookie')) { if (!isset($hook_boot_headers['vary']) && !variable_get('omit_vary_cookie')) {
header('Vary: Cookie'); // UNL Change!
drupal_add_http_header('Vary', 'Cookie', TRUE);
// End UNL Change!
} }
if ($page_compression) { if ($page_compression) {
...@@ -1192,7 +1391,7 @@ function drupal_serve_page_from_cache(stdClass $cache) { ...@@ -1192,7 +1391,7 @@ function drupal_serve_page_from_cache(stdClass $cache) {
} }
/** /**
* Define the critical hooks that force modules to always be loaded. * Defines the critical hooks that force modules to always be loaded.
*/ */
function bootstrap_hooks() { function bootstrap_hooks() {
return array('boot', 'exit', 'watchdog', 'language_init'); return array('boot', 'exit', 'watchdog', 'language_init');
...@@ -1220,185 +1419,60 @@ function drupal_unpack($obj, $field = 'data') { ...@@ -1220,185 +1419,60 @@ function drupal_unpack($obj, $field = 'data') {
/** /**
* Translates a string to the current language or to a given language. * Translates a string to the current language or to a given language.
* *
* All human-readable text that will be displayed on the site or sent to a user * The t() function serves two purposes. First, at run-time it translates
* should be passed through the t() function. This ensures that sites can be * user-visible text into the appropriate language. Second, various mechanisms
* fully translated into other languages. * 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.
* Here are some examples of translating static text using t(): * These strings are expected to be in English, so the first argument should
* always be in English. To enable a fully-translatable site, it is important
* that all human-readable text that will be displayed on the site or sent to
* a user is passed through the t() function, or a related function. See the
* @link http://drupal.org/node/322729 Localization API @endlink pages for
* more information, including recommendations on how to break up or not
* break up strings for translation.
*
* 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 * @code
* if (!$info || !$info['extension']) { * $text = t("@name's blog", array('@name' => format_username($account)));
* form_set_error('picture_upload', t('The uploaded file was not an image.'));
* }
*
* $form['submit'] = array(
* '#type' => 'submit',
* '#value' => t('Log in'),
* );
* @endcode * @endcode
* Basically, you can put variables like @name into your string, and t() will
* substitute their sanitized values at translation time. (See the
* Localization API pages referenced above and the documentation of
* format_string() for details about how to define variables in your string.)
* Translators can then rearrange the string as necessary for the language
* (e.g., in Spanish, it might be "blog de @name").
* *
* In addition to translating static text, t() can handle text that should not * During the Drupal installation phase, some resources used by t() wil not be
* be translated or that might change from time to time (such as link paths) * available to code that needs localization. See st() and get_t() for
* and dynamic text from variables, using special "placeholders". There are * alternatives.
* three styles of placeholders:
* - !variable: Indicates that the text should be inserted as-is. This is
* useful for inserting variables into things like e-mail. Example:
* @code
* $message[] = t("If you don't want to receive such e-mails, you can change your settings at !url.", array('!url' => url("user/$account->uid", array('absolute' => TRUE))));
* @endcode
* - @variable: Indicates that the text should be run through check_plain(), to
* escape HTML characters. Use this for any output that is displayed within a
* Drupal page. Example:
* @code
* drupal_set_title($title = t("@name's blog", array('@name' => format_username($account))), PASS_THROUGH);
* @endcode
* - %variable: Indicates that the string should be HTML-escaped and highlighted
* with theme_placeholder(), which shows up by default as <em>emphasized</em>.
* @code
* $message = t('%name-from sent %name-to an e-mail.', array('%name-from' => format_username($user), '%name-to' => format_username($account)));
* @endcode
*
* When using t(), try to put entire paragraphs in one t() call. This makes it
* easier for translators, as it provides context as to what each word refers
* to (and also allows translators to adjust word order, which may not be the
* same in all languages). HTML markup within translation strings is allowed,
* but should be avoided if possible. The exception is embedded links: link
* titles add context for translators and need to be translated, so they should
* be kept in the main string, while link URLs should be generated using
* placeholders.
* - Incorrect HTML in t():
* @code
* $output .= t('<p>Go to the @contact-page.</p>', array('@contact-page' => l(t('contact page'), 'contact')));
* @endcode
* - Correct HTML in t():
* @code
* $output .= '<p>' . t('Go to the <a href="@contact-page">contact page</a>.', array('@contact-page' => url('contact'))) . '</p>';
* @endcode
*
* Another thing that is helpful is to avoid escaping quotation marks wherever
* possible, because it can be confusing to translation teams.
* - Less desirable quotation mark escaping:
* @code
* $output .= t('Don\'t click me.');
* @endcode
* - Better way to use quotation marks:
* @code
* $output .= t("Don't click me.");
* @endcode
*
* It is important that all translation uses the t() mechanism, because in
* addition to actually translating the text at run-time, the t() function is
* also used by text-extraction routines to find text that needs to be
* translated, and build databases of text to be translated for translation
* teams. For that reason, you must put the actual string into the t() function,
* in most cases, and not a variable.
* - Incorrect use of a variable in t():
* @code
* $message = 'An error occurred.';
* drupal_set_message(t($message), 'error');
* $output .= t($message);
* @endcode
* - Correct translation of a variable with t():
* @code
* $message = t('An error occurred.');
* drupal_set_message($message, 'error');
* $output .= $message;
* @endcode
*
* The only case in which variables can be passed safely through t() is when
* code-based versions of the same strings will be passed through t() (or
* otherwise extracted) elsewhere.
*
* Also, you cannot use t() early in the bootstrap process, prior to the
* DRUPAL_BOOTSTRAP_LANGUAGE phase. The language variables will not be
* initialized yet, so the string will not be translated into the correct
* language. Examples of places where t() cannot be used include:
* - In a PHP define() statement.
* - In a hook_boot() implementation.
*
* In some cases, modules may include strings in code that can't use t()
* calls. For example, a module may use an external PHP application that
* produces strings that are loaded into variables in Drupal for output.
* In these cases, module authors may include a dummy file that passes the
* relevant strings through t(). This approach will allow the strings to be
* extracted.
*
* Sample external (non-Drupal) code:
* @code
* class Time {
* public $yesterday = 'Yesterday';
* public $today = 'Today';
* public $tomorrow = 'Tomorrow';
* }
* @endcode
*
* Sample dummy file:
* @code
* // Dummy function included in example.potx.inc.
* function example_potx() {
* $strings = array(
* t('Yesterday'),
* t('Today'),
* t('Tomorrow'),
* );
* // No return value needed, since this is a dummy function.
* }
* @endcode
*
* Having passed strings through t() in a dummy function, it is then
* possible to pass variables through t():
* @code
* $time = new Time();
* $output .= t($time->today);
* @endcode
*
* However tempting it is, custom data from user input or other non-code
* sources should not be passed through t(). Doing so leads to the following
* problems and errors:
* - The t() system doesn't support updates to existing strings. When user
* data is updated, the next time it's passed through t(), a new record is
* created instead of an update. The database bloats over time and any
* existing translations are orphaned with each update.
* - The t() system assumes any data it receives is in English. User data may
* be in another language, producing translation errors.
* - The "Built-in interface" text group in the locale system is used to
* produce translations for storage in .po files. When non-code strings are
* passed through t(), they are added to this text group, which is rendered
* inaccurate since it is a mix of actual interface strings and various user
* input strings of uncertain origin.
* Instead, translation of these data can be done through the locale system,
* either directly through hook_local() or through helper functions provided by
* contributed modules.
*
* Incorrect:
* @code
* $item = item_load();
* $output .= check_plain(t($item['title']));
* @endcode
*
* During installation, st() is used in place of t(). Code that may be called
* during installation or during normal operation should use the get_t()
* helper function.
* *
* @param $string * @param $string
* A string containing the English string to translate. * A string containing the English string to translate.
* @param $args * @param $args
* An associative array of replacements to make after translation. Incidences * An associative array of replacements to make after translation. Based
* 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.
* on the first character of the key, the value is escaped and/or themed: * See format_string() for details.
* - !variable: inserted as is
* - @variable: escape plain text to HTML (using check_plain())
* - %variable: escape text and theme as a placeholder for user-submitted
* content (using check_plain() + theme_placeholder())
* @param $options * @param $options
* An associative array of additional options, with the following keys: * An associative array of additional options, with the following elements:
* - 'langcode' (defaults to the current language) The language code to * - 'langcode' (defaults to the current language): The language code to
* translate to a language other than what is used to display the page. * translate to a language other than what is used to display the page.
* - 'context' (defaults to the empty context) The context the source string * - 'context' (defaults to the empty context): The context the source string
* belongs to. * belongs to.
* *
* @return * @return
* The translated string. * The translated string.
* *
* @see st()
* @see get_t()
* @see format_string()
* @ingroup sanitization * @ingroup sanitization
*/ */
function t($string, array $args = array(), array $options = array()) { function t($string, array $args = array(), array $options = array()) {
...@@ -1425,13 +1499,51 @@ function t($string, array $args = array(), array $options = array()) { ...@@ -1425,13 +1499,51 @@ function t($string, array $args = array(), array $options = array()) {
$string = $custom_strings[$options['langcode']][$options['context']][$string]; $string = $custom_strings[$options['langcode']][$options['context']][$string];
} }
// Translate with locale module if enabled. // Translate with locale module if enabled.
elseif (function_exists('locale') && $options['langcode'] != 'en') { elseif ($options['langcode'] != 'en' && function_exists('locale')) {
$string = locale($string, $options['context'], $options['langcode']); $string = locale($string, $options['context'], $options['langcode']);
} }
if (empty($args)) { if (empty($args)) {
return $string; return $string;
} }
else { else {
return format_string($string, $args);
}
}
/**
* Formats a string for HTML display by replacing variable placeholders.
*
* This function replaces variable placeholders in a string with the requested
* values and escapes the values so they can be safely displayed as HTML. It
* should be used on any unknown text that is intended to be printed to an HTML
* page (especially text that may have come from untrusted users, since in that
* case it prevents cross-site scripting and other security problems).
*
* In most cases, you should use t() rather than calling this function
* directly, since it will translate the text (on non-English-only sites) in
* addition to formatting it.
*
* @param $string
* A string containing placeholders.
* @param $args
* An associative array of replacements to make. Occurrences in $string of
* any key in $args are replaced with the corresponding value, after optional
* sanitization and formatting. The type of sanitization and formatting
* depends on the first character of the key:
* - @variable: Escaped to HTML using check_plain(). Use this as the default
* choice for anything displayed on a page on the site.
* - %variable: Escaped to HTML and formatted using drupal_placeholder(),
* which makes it display as <em>emphasized</em> text.
* - !variable: Inserted as is, with no sanitization or formatting. Only use
* this for text that has already been prepared for HTML display (for
* example, user-supplied text that has already been run through
* check_plain() previously, or is expected to contain some limited HTML
* tags and has already been run through filter_xss() previously).
*
* @see t()
* @ingroup sanitization
*/
function format_string($string, array $args = array()) {
// Transform arguments before inserting them. // Transform arguments before inserting them.
foreach ($args as $key => $value) { foreach ($args as $key => $value) {
switch ($key[0]) { switch ($key[0]) {
...@@ -1452,10 +1564,9 @@ function t($string, array $args = array(), array $options = array()) { ...@@ -1452,10 +1564,9 @@ function t($string, array $args = array(), array $options = array()) {
} }
return strtr($string, $args); return strtr($string, $args);
} }
}
/** /**
* Encode special characters in a plain-text string for display as HTML. * Encodes special characters in a plain-text string for display as HTML.
* *
* Also validates strings as UTF-8 to prevent cross site scripting attacks on * Also validates strings as UTF-8 to prevent cross site scripting attacks on
* Internet Explorer 6. * Internet Explorer 6.
...@@ -1494,6 +1605,7 @@ function check_plain($text) { ...@@ -1494,6 +1605,7 @@ function check_plain($text) {
* *
* @param $text * @param $text
* The text to check. * The text to check.
*
* @return * @return
* TRUE if the text is valid UTF-8, FALSE if not. * TRUE if the text is valid UTF-8, FALSE if not.
*/ */
...@@ -1508,11 +1620,12 @@ function drupal_validate_utf8($text) { ...@@ -1508,11 +1620,12 @@ function drupal_validate_utf8($text) {
} }
/** /**
* Since $_SERVER['REQUEST_URI'] is only available on Apache, we * Returns the equivalent of Apache's $_SERVER['REQUEST_URI'] variable.
* generate an equivalent using other environment variables. *
* Because $_SERVER['REQUEST_URI'] is only available on Apache, we generate an
* equivalent using other environment variables.
*/ */
function request_uri() { function request_uri() {
if (isset($_SERVER['REQUEST_URI'])) { if (isset($_SERVER['REQUEST_URI'])) {
$uri = $_SERVER['REQUEST_URI']; $uri = $_SERVER['REQUEST_URI'];
} }
...@@ -1534,7 +1647,7 @@ function request_uri() { ...@@ -1534,7 +1647,7 @@ function request_uri() {
} }
/** /**
* Log an exception. * Logs an exception.
* *
* This is a wrapper function for watchdog() which automatically decodes an * This is a wrapper function for watchdog() which automatically decodes an
* exception. * exception.
...@@ -1545,7 +1658,7 @@ function request_uri() { ...@@ -1545,7 +1658,7 @@ function request_uri() {
* The exception that is going to be logged. * The exception that is going to be logged.
* @param $message * @param $message
* The message to store in the log. If empty, a text that contains all useful * The message to store in the log. If empty, a text that contains all useful
* information about the passed in exception is used. * information about the passed-in exception is used.
* @param $variables * @param $variables
* Array of variables to replace in the message on display. Defaults to the * Array of variables to replace in the message on display. Defaults to the
* return value of drupal_decode_exception(). * return value of drupal_decode_exception().
...@@ -1561,7 +1674,8 @@ function watchdog_exception($type, Exception $exception, $message = NULL, $varia ...@@ -1561,7 +1674,8 @@ function watchdog_exception($type, Exception $exception, $message = NULL, $varia
// Use a default value if $message is not set. // Use a default value if $message is not set.
if (empty($message)) { if (empty($message)) {
$message = '%type: %message in %function (line %line of %file).'; // 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. // $variables must be an array so that we can add the exception information.
if (!is_array($variables)) { if (!is_array($variables)) {
...@@ -1574,7 +1688,7 @@ function watchdog_exception($type, Exception $exception, $message = NULL, $varia ...@@ -1574,7 +1688,7 @@ function watchdog_exception($type, Exception $exception, $message = NULL, $varia
} }
/** /**
* Log a system message. * Logs a system message.
* *
* @param $type * @param $type
* The category to which this message belongs. Can be any string, but the * The category to which this message belongs. Can be any string, but the
...@@ -1590,8 +1704,16 @@ function watchdog_exception($type, Exception $exception, $message = NULL, $varia ...@@ -1590,8 +1704,16 @@ function watchdog_exception($type, Exception $exception, $message = NULL, $varia
* NULL if message is already translated or not possible to * NULL if message is already translated or not possible to
* translate. * translate.
* @param $severity * @param $severity
* The severity of the message, as per RFC 3164. Possible values are * The severity of the message; one of the following values as defined in
* WATCHDOG_ERROR, WATCHDOG_WARNING, etc. * @link http://www.faqs.org/rfcs/rfc3164.html RFC 3164: @endlink
* - WATCHDOG_EMERGENCY: Emergency, system is unusable.
* - WATCHDOG_ALERT: Alert, action must be taken immediately.
* - WATCHDOG_CRITICAL: Critical conditions.
* - WATCHDOG_ERROR: Error conditions.
* - WATCHDOG_WARNING: Warning conditions.
* - WATCHDOG_NOTICE: (default) Normal but significant conditions.
* - WATCHDOG_INFO: Informational messages.
* - WATCHDOG_DEBUG: Debug-level messages.
* @param $link * @param $link
* A link to associate with the message. * A link to associate with the message.
* *
...@@ -1608,6 +1730,9 @@ function watchdog($type, $message, $variables = array(), $severity = WATCHDOG_NO ...@@ -1608,6 +1730,9 @@ function watchdog($type, $message, $variables = array(), $severity = WATCHDOG_NO
if (!$in_error_state && function_exists('module_implements')) { if (!$in_error_state && function_exists('module_implements')) {
$in_error_state = TRUE; $in_error_state = TRUE;
// The user object may not exist in all conditions, so 0 is substituted if needed.
$user_uid = isset($user->uid) ? $user->uid : 0;
// Prepare the fields to be logged // Prepare the fields to be logged
$log_entry = array( $log_entry = array(
'type' => $type, 'type' => $type,
...@@ -1616,10 +1741,12 @@ function watchdog($type, $message, $variables = array(), $severity = WATCHDOG_NO ...@@ -1616,10 +1741,12 @@ function watchdog($type, $message, $variables = array(), $severity = WATCHDOG_NO
'severity' => $severity, 'severity' => $severity,
'link' => $link, 'link' => $link,
'user' => $user, 'user' => $user,
'uid' => $user_uid,
'request_uri' => $base_root . request_uri(), 'request_uri' => $base_root . request_uri(),
'referer' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '', 'referer' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '',
'ip' => ip_address(), 'ip' => ip_address(),
'timestamp' => REQUEST_TIME, // Request time isn't accurate for long processes, use time() instead.
'timestamp' => time(),
); );
// Call the logging hooks to log/process the message // Call the logging hooks to log/process the message
...@@ -1634,22 +1761,37 @@ function watchdog($type, $message, $variables = array(), $severity = WATCHDOG_NO ...@@ -1634,22 +1761,37 @@ function watchdog($type, $message, $variables = array(), $severity = WATCHDOG_NO
} }
/** /**
* Set a message which reflects the status of the performed operation. * Sets a message to display to the user.
* *
* If the function is called with no arguments, this function returns all set * Messages are stored in a session variable and displayed in page.tpl.php via
* messages without clearing them. * the $messages theme variable.
* *
* @param $message * Example usage:
* The message should begin with a capital letter and always ends with a * @code
* period '.'. * drupal_set_message(t('An error occurred and processing did not complete.'), 'error');
* @param $type * @endcode
* The type of the message. One of the following values are possible: *
* @param string $message
* (optional) The translated message to be displayed to the user. For
* consistency with other messages, it should begin with a capital letter and
* end with a period.
* @param string $type
* (optional) The message's type. Defaults to 'status'. These values are
* supported:
* - 'status' * - 'status'
* - 'warning' * - 'warning'
* - 'error' * - 'error'
* @param $repeat * @param bool $repeat
* If this is FALSE and the message is already set, then the message won't * (optional) If this is FALSE and the message is already set, then the
* be repeated. * message won't be repeated. Defaults to TRUE.
*
* @return array|null
* A multidimensional array with keys corresponding to the set message types.
* The indexed array values of each contain the set messages for that type.
* Or, if there are no messages set, the function returns NULL.
*
* @see drupal_get_messages()
* @see theme_status_messages()
*/ */
function drupal_set_message($message = NULL, $type = 'status', $repeat = TRUE) { function drupal_set_message($message = NULL, $type = 'status', $repeat = TRUE) {
if ($message) { if ($message) {
...@@ -1670,17 +1812,29 @@ function drupal_set_message($message = NULL, $type = 'status', $repeat = TRUE) { ...@@ -1670,17 +1812,29 @@ function drupal_set_message($message = NULL, $type = 'status', $repeat = TRUE) {
} }
/** /**
* Return all messages that have been set. * Returns all messages that have been set with drupal_set_message().
* *
* @param $type * @param string $type
* (optional) Only return messages of this type. * (optional) Limit the messages returned by type. Defaults to NULL, meaning
* @param $clear_queue * all types. These values are supported:
* (optional) Set to FALSE if you do not want to clear the messages queue * - NULL
* @return * - 'status'
* An associative array, the key is the message type, the value an array * - 'warning'
* of messages. If the $type parameter is passed, you get only that type, * - 'error'
* or an empty array if there are no such messages. If $type is not passed, * @param bool $clear_queue
* all message types are returned, or an empty array if none exist. * (optional) If this is TRUE, the queue will be cleared of messages of the
* type specified in the $type parameter. Otherwise the queue will be left
* intact. Defaults to TRUE.
*
* @return array
* A multidimensional array with keys corresponding to the set message types.
* The indexed array values of each contain the set messages for that type.
* The messages returned are limited to the type specified in the $type
* parameter. If there are no messages of the specified type, an empty array
* is returned.
*
* @see drupal_set_message()
* @see theme_status_messages()
*/ */
function drupal_get_messages($type = NULL, $clear_queue = TRUE) { function drupal_get_messages($type = NULL, $clear_queue = TRUE) {
if ($messages = drupal_set_message()) { if ($messages = drupal_set_message()) {
...@@ -1703,7 +1857,9 @@ function drupal_get_messages($type = NULL, $clear_queue = TRUE) { ...@@ -1703,7 +1857,9 @@ function drupal_get_messages($type = NULL, $clear_queue = TRUE) {
} }
/** /**
* Get the title of the current page, for display on the page and in the title bar. * Gets the title of the current page.
*
* The title is displayed on the page and in the title bar.
* *
* @return * @return
* The current page's title. * The current page's title.
...@@ -1720,7 +1876,9 @@ function drupal_get_title() { ...@@ -1720,7 +1876,9 @@ function drupal_get_title() {
} }
/** /**
* Set the title of the current page, for display on the page and in the title bar. * Sets the title of the current page.
*
* The title is displayed on the page and in the title bar.
* *
* @param $title * @param $title
* Optional string value to assign to the page title; or if set to NULL * Optional string value to assign to the page title; or if set to NULL
...@@ -1745,7 +1903,7 @@ function drupal_set_title($title = NULL, $output = CHECK_PLAIN) { ...@@ -1745,7 +1903,7 @@ function drupal_set_title($title = NULL, $output = CHECK_PLAIN) {
} }
/** /**
* Check to see if an IP address has been blocked. * Checks to see if an IP address has been blocked.
* *
* Blocked IP addresses are stored in the database by default. However for * Blocked IP addresses are stored in the database by default. However for
* performance reasons we allow an override in settings.php. This allows us * performance reasons we allow an override in settings.php. This allows us
...@@ -1754,6 +1912,7 @@ function drupal_set_title($title = NULL, $output = CHECK_PLAIN) { ...@@ -1754,6 +1912,7 @@ function drupal_set_title($title = NULL, $output = CHECK_PLAIN) {
* *
* @param $ip * @param $ip
* IP address to check. * IP address to check.
*
* @return bool * @return bool
* TRUE if access is denied, FALSE if access is allowed. * TRUE if access is denied, FALSE if access is allowed.
*/ */
...@@ -1779,7 +1938,7 @@ function drupal_is_denied($ip) { ...@@ -1779,7 +1938,7 @@ function drupal_is_denied($ip) {
} }
/** /**
* Handle denied users. * Handles denied users.
* *
* @param $ip * @param $ip
* IP address to check. Prints a message and exits if access is denied. * IP address to check. Prints a message and exits if access is denied.
...@@ -1798,14 +1957,15 @@ function drupal_block_denied($ip) { ...@@ -1798,14 +1957,15 @@ function drupal_block_denied($ip) {
* *
* This function is better than simply calling mt_rand() or any other built-in * 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 * 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. * bytes normally from mt_rand()) and uses the best available pseudo-random
* source.
* *
* @param $count * @param $count
* The number of characters (bytes) to return in the string. * The number of characters (bytes) to return in the string.
*/ */
function drupal_random_bytes($count) { function drupal_random_bytes($count) {
// $random_state does not use drupal_static as it stores random bytes. // $random_state does not use drupal_static as it stores random bytes.
static $random_state, $bytes; static $random_state, $bytes, $php_compatible;
// Initialize on the first call. The contents of $_SERVER includes a mix of // 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. // user-specific and system information that varies a little with each page.
if (!isset($random_state)) { if (!isset($random_state)) {
...@@ -1817,6 +1977,11 @@ function drupal_random_bytes($count) { ...@@ -1817,6 +1977,11 @@ function drupal_random_bytes($count) {
$bytes = ''; $bytes = '';
} }
if (strlen($bytes) < $count) { if (strlen($bytes) < $count) {
// PHP versions prior 5.3.4 experienced openssl_random_pseudo_bytes()
// locking on Windows and rendered it unusable.
if (!isset($php_compatible)) {
$php_compatible = version_compare(PHP_VERSION, '5.3.4', '>=');
}
// /dev/urandom is available on many *nix systems and is considered the // /dev/urandom is available on many *nix systems and is considered the
// best commonly available pseudo-random source. // best commonly available pseudo-random source.
if ($fh = @fopen('/dev/urandom', 'rb')) { if ($fh = @fopen('/dev/urandom', 'rb')) {
...@@ -1826,6 +1991,11 @@ function drupal_random_bytes($count) { ...@@ -1826,6 +1991,11 @@ function drupal_random_bytes($count) {
$bytes .= fread($fh, max(4096, $count)); $bytes .= fread($fh, max(4096, $count));
fclose($fh); fclose($fh);
} }
// openssl_random_pseudo_bytes() will find entropy in a system-dependent
// way.
elseif ($php_compatible && function_exists('openssl_random_pseudo_bytes')) {
$bytes .= openssl_random_pseudo_bytes($count - strlen($bytes));
}
// If /dev/urandom is not available or returns no bytes, this loop will // If /dev/urandom is not available or returns no bytes, this loop will
// generate a good set of pseudo-random bytes on any system. // generate a good set of pseudo-random bytes on any system.
// Note that it may be important that our $random_state is passed // Note that it may be important that our $random_state is passed
...@@ -1845,7 +2015,7 @@ function drupal_random_bytes($count) { ...@@ -1845,7 +2015,7 @@ function drupal_random_bytes($count) {
} }
/** /**
* Calculate a base-64 encoded, URL-safe sha-256 hmac. * Calculates a base-64 encoded, URL-safe sha-256 hmac.
* *
* @param $data * @param $data
* String to be validated with the hmac. * String to be validated with the hmac.
...@@ -1863,7 +2033,7 @@ function drupal_hmac_base64($data, $key) { ...@@ -1863,7 +2033,7 @@ function drupal_hmac_base64($data, $key) {
} }
/** /**
* Calculate a base-64 encoded, URL-safe sha-256 hash. * Calculates a base-64 encoded, URL-safe sha-256 hash.
* *
* @param $data * @param $data
* String to be hashed. * String to be hashed.
...@@ -1878,6 +2048,80 @@ function drupal_hash_base64($data) { ...@@ -1878,6 +2048,80 @@ function drupal_hash_base64($data) {
return strtr($hash, array('+' => '-', '/' => '_', '=' => '')); return strtr($hash, array('+' => '-', '/' => '_', '=' => ''));
} }
/**
* Merges multiple arrays, recursively, and returns the merged array.
*
* This function is similar to PHP's array_merge_recursive() function, but it
* handles non-array values differently. When merging values that are not both
* arrays, the latter value replaces the former rather than merging with it.
*
* Example:
* @code
* $link_options_1 = array('fragment' => 'x', 'attributes' => array('title' => t('X'), 'class' => array('a', 'b')));
* $link_options_2 = array('fragment' => 'y', 'attributes' => array('title' => t('Y'), 'class' => array('c', 'd')));
*
* // This results in array('fragment' => array('x', 'y'), 'attributes' => array('title' => array(t('X'), t('Y')), 'class' => array('a', 'b', 'c', 'd'))).
* $incorrect = array_merge_recursive($link_options_1, $link_options_2);
*
* // This results in array('fragment' => 'y', 'attributes' => array('title' => t('Y'), 'class' => array('a', 'b', 'c', 'd'))).
* $correct = drupal_array_merge_deep($link_options_1, $link_options_2);
* @endcode
*
* @param ...
* Arrays to merge.
*
* @return
* The merged array.
*
* @see drupal_array_merge_deep_array()
*/
function drupal_array_merge_deep() {
$args = func_get_args();
return drupal_array_merge_deep_array($args);
}
/**
* Merges multiple arrays, recursively, and returns the merged array.
*
* This function is equivalent to drupal_array_merge_deep(), except the
* input arrays are passed as a single array parameter rather than a variable
* parameter list.
*
* The following are equivalent:
* - drupal_array_merge_deep($a, $b);
* - drupal_array_merge_deep_array(array($a, $b));
*
* The following are also equivalent:
* - call_user_func_array('drupal_array_merge_deep', $arrays_to_merge);
* - drupal_array_merge_deep_array($arrays_to_merge);
*
* @see drupal_array_merge_deep()
*/
function drupal_array_merge_deep_array($arrays) {
$result = array();
foreach ($arrays as $array) {
foreach ($array as $key => $value) {
// Renumber integer keys as array_merge_recursive() does. Note that PHP
// automatically converts array keys that are integer strings (e.g., '1')
// to integers.
if (is_integer($key)) {
$result[] = $value;
}
// Recurse when both values are arrays.
elseif (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
$result[$key] = drupal_array_merge_deep_array(array($result[$key], $value));
}
// Otherwise, use the latter value, overriding any previous value.
else {
$result[$key] = $value;
}
}
}
return $result;
}
/** /**
* Generates a default anonymous $user object. * Generates a default anonymous $user object.
* *
...@@ -1894,20 +2138,34 @@ function drupal_anonymous_user() { ...@@ -1894,20 +2138,34 @@ function drupal_anonymous_user() {
} }
/** /**
* A string describing a phase of Drupal to load. Each phase adds to the * Ensures Drupal is bootstrapped to the specified phase.
* 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 * In order to bootstrap Drupal from another PHP script, you can use this code:
* Drupal database from a script without loading anything else, you can * @code
* include bootstrap.inc, and call drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE). * define('DRUPAL_ROOT', '/path/to/drupal');
* require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
* drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
* @endcode
* *
* @param $phase * @param $phase
* A constant. Allowed values are the DRUPAL_BOOTSTRAP_* constants. * A constant telling which phase to bootstrap to. When you bootstrap to a
* particular phase, all earlier phases are run automatically. Possible
* values:
* - DRUPAL_BOOTSTRAP_CONFIGURATION: Initializes configuration.
* - DRUPAL_BOOTSTRAP_PAGE_CACHE: Tries to serve a cached page.
* - DRUPAL_BOOTSTRAP_DATABASE: Initializes the database layer.
* - DRUPAL_BOOTSTRAP_VARIABLES: Initializes the variable system.
* - DRUPAL_BOOTSTRAP_SESSION: Initializes session handling.
* - DRUPAL_BOOTSTRAP_PAGE_HEADER: Sets up the page header.
* - DRUPAL_BOOTSTRAP_LANGUAGE: Finds out the language of the page.
* - DRUPAL_BOOTSTRAP_FULL: Fully loads Drupal. Validates and fixes input
* data.
* @param $new_phase * @param $new_phase
* A boolean, set to FALSE if calling drupal_bootstrap from inside a * A boolean, set to FALSE if calling drupal_bootstrap from inside a
* function called from drupal_bootstrap (recursion). * function called from drupal_bootstrap (recursion).
*
* @return * @return
* The most recently completed phase. * The most recently completed phase.
*
*/ */
function drupal_bootstrap($phase = NULL, $new_phase = TRUE) { function drupal_bootstrap($phase = NULL, $new_phase = TRUE) {
// Not drupal_static(), because does not depend on any run-time information. // Not drupal_static(), because does not depend on any run-time information.
...@@ -1986,7 +2244,7 @@ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) { ...@@ -1986,7 +2244,7 @@ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) {
} }
/** /**
* Return the time zone of the current user. * Returns the time zone of the current user.
*/ */
function drupal_get_user_timezone() { function drupal_get_user_timezone() {
global $user; global $user;
...@@ -2001,7 +2259,7 @@ function drupal_get_user_timezone() { ...@@ -2001,7 +2259,7 @@ function drupal_get_user_timezone() {
} }
/** /**
* Custom PHP error handler. * Provides custom PHP error handling.
* *
* @param $error_level * @param $error_level
* The level of the error raised. * The level of the error raised.
...@@ -2012,7 +2270,8 @@ function drupal_get_user_timezone() { ...@@ -2012,7 +2270,8 @@ function drupal_get_user_timezone() {
* @param $line * @param $line
* The line number the error was raised at. * The line number the error was raised at.
* @param $context * @param $context
* An array that points to the active symbol table at the point the error occurred. * 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) { function _drupal_error_handler($error_level, $message, $filename, $line, $context) {
require_once DRUPAL_ROOT . '/includes/errors.inc'; require_once DRUPAL_ROOT . '/includes/errors.inc';
...@@ -2020,7 +2279,7 @@ function _drupal_error_handler($error_level, $message, $filename, $line, $contex ...@@ -2020,7 +2279,7 @@ function _drupal_error_handler($error_level, $message, $filename, $line, $contex
} }
/** /**
* Custom PHP exception handler. * Provides custom PHP exception handling.
* *
* Uncaught exceptions are those not enclosed in a try/catch block. They are * 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 * always fatal: the execution of the script will stop as soon as the exception
...@@ -2048,7 +2307,7 @@ function _drupal_exception_handler($exception) { ...@@ -2048,7 +2307,7 @@ function _drupal_exception_handler($exception) {
} }
/** /**
* Bootstrap configuration: Setup script environment and load settings.php. * Sets up the script environment and loads settings.php.
*/ */
function _drupal_bootstrap_configuration() { function _drupal_bootstrap_configuration() {
// Set the Drupal custom error handler. // Set the Drupal custom error handler.
...@@ -2063,7 +2322,7 @@ function _drupal_bootstrap_configuration() { ...@@ -2063,7 +2322,7 @@ function _drupal_bootstrap_configuration() {
} }
/** /**
* Bootstrap page cache: Try to serve a page from cache. * Attempts to serve a page from the cache.
*/ */
function _drupal_bootstrap_page_cache() { function _drupal_bootstrap_page_cache() {
global $user; global $user;
...@@ -2119,7 +2378,7 @@ function _drupal_bootstrap_page_cache() { ...@@ -2119,7 +2378,7 @@ function _drupal_bootstrap_page_cache() {
} }
/** /**
* Bootstrap database: Initialize database system and register autoload functions. * Initializes the database system and registers autoload functions.
*/ */
function _drupal_bootstrap_database() { function _drupal_bootstrap_database() {
// Redirect the user to the installation script if Drupal has not been // Redirect the user to the installation script if Drupal has not been
...@@ -2133,15 +2392,7 @@ function _drupal_bootstrap_database() { ...@@ -2133,15 +2392,7 @@ function _drupal_bootstrap_database() {
// The user agent header is used to pass a database prefix in the request when // 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 // running tests. However, for security reasons, it is imperative that we
// validate we ourselves made the request. // validate we ourselves made the request.
if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^(simpletest\d+);/", $_SERVER['HTTP_USER_AGENT'], $matches)) { if ($test_prefix = drupal_valid_test_ua()) {
if (!drupal_valid_test_ua($_SERVER['HTTP_USER_AGENT'])) {
header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
exit;
}
// The first part of the user agent is the prefix itself.
$test_prefix = $matches[1];
// Set the test run id for use in other parts of Drupal. // Set the test run id for use in other parts of Drupal.
$test_info = &$GLOBALS['drupal_test_info']; $test_info = &$GLOBALS['drupal_test_info'];
$test_info['test_run_id'] = $test_prefix; $test_info['test_run_id'] = $test_prefix;
...@@ -2174,13 +2425,12 @@ function _drupal_bootstrap_database() { ...@@ -2174,13 +2425,12 @@ function _drupal_bootstrap_database() {
// The database autoload routine comes first so that we can load the database // The database autoload routine comes first so that we can load the database
// system without hitting the database. That is especially important during // system without hitting the database. That is especially important during
// the install or upgrade process. // the install or upgrade process.
spl_autoload_register('db_autoload');
spl_autoload_register('drupal_autoload_class'); spl_autoload_register('drupal_autoload_class');
spl_autoload_register('drupal_autoload_interface'); spl_autoload_register('drupal_autoload_interface');
} }
/** /**
* Bootstrap variables: Load system variables and all enabled bootstrap modules. * Loads system variables and all enabled bootstrap modules.
*/ */
function _drupal_bootstrap_variables() { function _drupal_bootstrap_variables() {
global $conf; global $conf;
...@@ -2197,7 +2447,7 @@ function _drupal_bootstrap_variables() { ...@@ -2197,7 +2447,7 @@ function _drupal_bootstrap_variables() {
} }
/** /**
* Bootstrap page header: Invoke hook_boot(), initialize locking system, and send default HTTP headers. * Invokes hook_boot(), initializes locking system, and sends HTTP headers.
*/ */
function _drupal_bootstrap_page_header() { function _drupal_bootstrap_page_header() {
bootstrap_invoke_all('boot'); bootstrap_invoke_all('boot');
...@@ -2220,26 +2470,44 @@ function drupal_get_bootstrap_phase() { ...@@ -2220,26 +2470,44 @@ function drupal_get_bootstrap_phase() {
} }
/** /**
* Validate the HMAC and timestamp of a user agent header from simpletest. * Returns the test prefix if this is an internal request from SimpleTest.
*
* @return
* Either the simpletest prefix (the string "simpletest" followed by any
* number of digits) or FALSE if the user agent does not contain a valid
* HMAC and timestamp.
*/ */
function drupal_valid_test_ua($user_agent) { function drupal_valid_test_ua() {
global $drupal_hash_salt; global $drupal_hash_salt;
// No reason to reset this.
static $test_prefix;
list($prefix, $time, $salt, $hmac) = explode(';', $user_agent); 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; $check_string = $prefix . ';' . $time . ';' . $salt;
// We use the salt from settings.php to make the HMAC key, since // 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 database is not yet initialized and we can't access any Drupal variables.
// The file properties add more entropy not easily accessible to others. // The file properties add more entropy not easily accessible to others.
$filepath = DRUPAL_ROOT . '/includes/bootstrap.inc'; $key = $drupal_hash_salt . filectime(__FILE__) . fileinode(__FILE__);
$key = $drupal_hash_salt . filectime($filepath) . fileinode($filepath);
$time_diff = REQUEST_TIME - $time; $time_diff = REQUEST_TIME - $time;
// Since we are making a local request a 5 second time window is allowed, // Since we are making a local request a 5 second time window is allowed,
// and the HMAC must match. // and the HMAC must match.
return ($time_diff >= 0) && ($time_diff <= 5) && ($hmac == drupal_hmac_base64($check_string, $key)); if ($time_diff >= 0 && $time_diff <= 5 && $hmac == drupal_hmac_base64($check_string, $key)) {
$test_prefix = $prefix;
return $test_prefix;
}
}
$test_prefix = FALSE;
return $test_prefix;
} }
/** /**
* Generate a user agent string with a HMAC and timestamp for simpletest. * Generates a user agent string with a HMAC and timestamp for simpletest.
*/ */
function drupal_generate_test_ua($prefix) { function drupal_generate_test_ua($prefix) {
global $drupal_hash_salt; global $drupal_hash_salt;
...@@ -2249,8 +2517,7 @@ function drupal_generate_test_ua($prefix) { ...@@ -2249,8 +2517,7 @@ function drupal_generate_test_ua($prefix) {
// We use the salt from settings.php to make the HMAC key, since // 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 database is not yet initialized and we can't access any Drupal variables.
// The file properties add more entropy not easily accessible to others. // The file properties add more entropy not easily accessible to others.
$filepath = DRUPAL_ROOT . '/includes/bootstrap.inc'; $key = $drupal_hash_salt . filectime(__FILE__) . fileinode(__FILE__);
$key = $drupal_hash_salt . filectime($filepath) . fileinode($filepath);
} }
// Generate a moderately secure HMAC based on the database credentials. // Generate a moderately secure HMAC based on the database credentials.
$salt = uniqid('', TRUE); $salt = uniqid('', TRUE);
...@@ -2272,15 +2539,65 @@ function drupal_maintenance_theme() { ...@@ -2272,15 +2539,65 @@ function drupal_maintenance_theme() {
} }
/** /**
* Return TRUE if a Drupal installation is currently being attempted. * Returns a simple 404 Not Found page.
*
* If fast 404 pages are enabled, and this is a matching page then print a
* simple 404 page and exit.
*
* This function is called from drupal_deliver_html_page() at the time when a
* a normal 404 page is generated, but it can also optionally be called directly
* from settings.php to prevent a Drupal bootstrap on these pages. See
* documentation in settings.php for the benefits and drawbacks of using this.
*
* Paths to dynamically-generated content, such as image styles, should also be
* accounted for in this function.
*/
function drupal_fast_404() {
$exclude_paths = variable_get('404_fast_paths_exclude', FALSE);
if ($exclude_paths && !preg_match($exclude_paths, $_GET['q'])) {
$fast_paths = variable_get('404_fast_paths', FALSE);
if ($fast_paths && preg_match($fast_paths, $_GET['q'])) {
drupal_add_http_header('Status', '404 Not Found');
$fast_404_html = variable_get('404_fast_html', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL "@path" was not found on this server.</p></body></html>');
// Replace @path in the variable with the page path.
print strtr($fast_404_html, array('@path' => check_plain(request_uri())));
exit;
}
}
}
/**
* Returns TRUE if a Drupal installation is currently being attempted.
*/ */
function drupal_installation_attempted() { function drupal_installation_attempted() {
return defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'install'; return defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'install';
} }
/** /**
* Return the name of the localization function. Use in code that needs to * Returns the name of the proper localization function.
* run both during installation and normal operation. *
* 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 usage:
* @code
* $t = get_t();
* $translated = $t('translate this');
* @endcode
*
* Use t() if your code will never run during the Drupal installation phase.
* Use st() if your code will only run during installation and never any other
* time. Use get_t() if your code could run in either circumstance.
*
* @see t()
* @see st()
* @ingroup sanitization
*/ */
function get_t() { function get_t() {
static $t; static $t;
...@@ -2293,7 +2610,7 @@ function get_t() { ...@@ -2293,7 +2610,7 @@ function get_t() {
} }
/** /**
* Initialize all the defined language types. * Initializes all the defined language types.
*/ */
function drupal_language_initialize() { function drupal_language_initialize() {
$types = language_types(); $types = language_types();
...@@ -2318,7 +2635,7 @@ function drupal_language_initialize() { ...@@ -2318,7 +2635,7 @@ function drupal_language_initialize() {
} }
/** /**
* The built-in language types. * Returns a list of the built-in language types.
* *
* @return * @return
* An array of key-values pairs where the key is the language type and the * An array of key-values pairs where the key is the language type and the
...@@ -2333,23 +2650,42 @@ function drupal_language_types() { ...@@ -2333,23 +2650,42 @@ function drupal_language_types() {
} }
/** /**
* Return true if there is more than one language enabled. * Returns TRUE if there is more than one language enabled.
*
* @return
* TRUE if more than one language is enabled.
*/ */
function drupal_multilingual() { 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 variable_get('language_count', 1) > 1;
} }
/** /**
* Return an array of the available language types. * Returns an array of the available language types.
*
* @return
* An array of all language types where the keys of each are the language type
* name and its value is its configurability (TRUE/FALSE).
*/ */
function language_types() { function language_types() {
return array_keys(variable_get('language_types', drupal_language_types())); return array_keys(variable_get('language_types', drupal_language_types()));
} }
/** /**
* Get a list of languages set up indexed by the specified key * Returns a list of installed languages, indexed by the specified key.
* *
* @param $field The field to index the list with. * @param $field
* (optional) The field to index the list with.
*
* @return
* An associative array, keyed on the values of $field.
* - If $field is 'weight' or 'enabled', the array is nested, with the outer
* array's values each being associative arrays with language codes as
* keys and language objects as values.
* - For all other values of $field, the array is only one level deep, and
* the array's values are language objects.
*/ */
function language_list($field = 'language') { function language_list($field = 'language') {
$languages = &drupal_static(__FUNCTION__); $languages = &drupal_static(__FUNCTION__);
...@@ -2388,7 +2724,7 @@ function language_list($field = 'language') { ...@@ -2388,7 +2724,7 @@ function language_list($field = 'language') {
} }
/** /**
* Default language used on the site * Returns the default language used on the site
* *
* @param $property * @param $property
* Optional property of the language object to return * Optional property of the language object to return
...@@ -2407,6 +2743,8 @@ function language_default($property = NULL) { ...@@ -2407,6 +2743,8 @@ function language_default($property = NULL) {
* base_path() returns "/drupalfolder/". * base_path() returns "/drupalfolder/".
* - http://example.com/path/alias (which is a path alias for node/306) returns * - http://example.com/path/alias (which is a path alias for node/306) returns
* "path/alias" as opposed to the internal path. * "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 * @return
* The requested Drupal URL path. * The requested Drupal URL path.
...@@ -2420,7 +2758,7 @@ function request_path() { ...@@ -2420,7 +2758,7 @@ function request_path() {
return $path; return $path;
} }
if (isset($_GET['q'])) { if (isset($_GET['q']) && is_string($_GET['q'])) {
// This is a request with a ?q=foo/bar query string. $_GET['q'] is // This is a request with a ?q=foo/bar query string. $_GET['q'] is
// overwritten in drupal_path_initialize(), but request_path() is called // overwritten in drupal_path_initialize(), but request_path() is called
// very early in the bootstrap process, so the original value is saved in // very early in the bootstrap process, so the original value is saved in
...@@ -2428,11 +2766,19 @@ function request_path() { ...@@ -2428,11 +2766,19 @@ function request_path() {
$path = $_GET['q']; $path = $_GET['q'];
} }
elseif (isset($_SERVER['REQUEST_URI'])) { elseif (isset($_SERVER['REQUEST_URI'])) {
// This is a request using a clean URL. Extract the path from 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'], '?'); $request_path = strtok($_SERVER['REQUEST_URI'], '?');
$base_path_len = strlen(rtrim(dirname($_SERVER['SCRIPT_NAME']), '\/')); $base_path_len = strlen(rtrim(dirname($_SERVER['SCRIPT_NAME']), '\/'));
// Unescape and strip $base_path prefix, leaving q without a leading slash. // Unescape and strip $base_path prefix, leaving q without a leading slash.
$path = substr(urldecode($request_path), $base_path_len + 1); $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 { else {
// This is the front page. // This is the front page.
...@@ -2448,16 +2794,16 @@ function request_path() { ...@@ -2448,16 +2794,16 @@ function request_path() {
} }
/** /**
* Return a component of the current Drupal path. * Returns a component of the current Drupal path.
* *
* When viewing a page at the path "admin/structure/types", for example, arg(0) * 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". * 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. * Avoid use of this function where possible, as resulting code is hard to
* In menu callback functions, attempt to use named arguments. See the explanation * read. In menu callback functions, attempt to use named arguments. See the
* in menu.inc for how to construct callbacks that take arguments. When attempting * explanation in menu.inc for how to construct callbacks that take arguments.
* to use this function to load an element from the current path, e.g. loading the * When attempting to use this function to load an element from the current
* node on a node page, please use menu_get_object() instead. * path, e.g. loading the node on a node page, use menu_get_object() instead.
* *
* @param $index * @param $index
* The index of the component, where each component is separated by a '/' * The index of the component, where each component is separated by a '/'
...@@ -2467,7 +2813,8 @@ function request_path() { ...@@ -2467,7 +2813,8 @@ function request_path() {
* *
* @return * @return
* The component specified by $index, or NULL if the specified component was * The component specified by $index, or NULL if the specified component was
* not found. * not found. If called without arguments, it returns an array containing all
* the components of the current path.
*/ */
function arg($index = NULL, $path = NULL) { function arg($index = NULL, $path = NULL) {
// Even though $arguments doesn't need to be resettable for any functional // Even though $arguments doesn't need to be resettable for any functional
...@@ -2496,6 +2843,8 @@ function arg($index = NULL, $path = NULL) { ...@@ -2496,6 +2843,8 @@ function arg($index = NULL, $path = NULL) {
} }
/** /**
* Returns the IP address of the client machine.
*
* If Drupal is behind a reverse proxy, we use the X-Forwarded-For header * 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 * 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 * the proxy server, and not the client's. The actual header name can be
...@@ -2540,12 +2889,12 @@ function ip_address() { ...@@ -2540,12 +2889,12 @@ function ip_address() {
} }
/** /**
* @ingroup schemaapi * @addtogroup schemaapi
* @{ * @{
*/ */
/** /**
* Get the schema definition of a table, or the whole database schema. * Gets the schema definition of a table, or the whole database schema.
* *
* The returned schema will include any modifications made by any * The returned schema will include any modifications made by any
* module that implements hook_schema_alter(). * module that implements hook_schema_alter().
...@@ -2556,6 +2905,61 @@ function ip_address() { ...@@ -2556,6 +2905,61 @@ function ip_address() {
* If true, the schema will be rebuilt instead of retrieved from the cache. * If true, the schema will be rebuilt instead of retrieved from the cache.
*/ */
function drupal_get_schema($table = NULL, $rebuild = FALSE) { function drupal_get_schema($table = NULL, $rebuild = FALSE) {
static $schema;
if ($rebuild || !isset($table)) {
$schema = drupal_get_complete_schema($rebuild);
}
elseif (!isset($schema)) {
$schema = new SchemaCache();
}
if (!isset($table)) {
return $schema;
}
if (isset($schema[$table])) {
return $schema[$table];
}
else {
return FALSE;
}
}
/**
* Extends DrupalCacheArray to allow for dynamic building of the schema cache.
*/
class SchemaCache extends DrupalCacheArray {
/**
* Constructs a SchemaCache object.
*/
public function __construct() {
// Cache by request method.
parent::__construct('schema:runtime:' . ($_SERVER['REQUEST_METHOD'] == 'GET'), 'cache');
}
/**
* Overrides DrupalCacheArray::resolveCacheMiss().
*/
protected function resolveCacheMiss($offset) {
$complete_schema = drupal_get_complete_schema();
$value = isset($complete_schema[$offset]) ? $complete_schema[$offset] : NULL;
$this->storage[$offset] = $value;
$this->persist($offset);
return $value;
}
}
/**
* Gets the whole database schema.
*
* The returned schema will include any modifications made by any
* module that implements hook_schema_alter().
*
* @param $rebuild
* If true, the schema will be rebuilt instead of retrieved from the cache.
*/
function drupal_get_complete_schema($rebuild = FALSE) {
static $schema = array(); static $schema = array();
if (empty($schema) || $rebuild) { if (empty($schema) || $rebuild) {
...@@ -2570,15 +2974,11 @@ function drupal_get_schema($table = NULL, $rebuild = FALSE) { ...@@ -2570,15 +2974,11 @@ function drupal_get_schema($table = NULL, $rebuild = FALSE) {
// On some databases this function may be called before bootstrap has // 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. // been completed, so we force the functions we need to load just in case.
if (function_exists('module_load_all_includes')) { if (function_exists('module_load_all_includes')) {
// There is currently a bug in module_list() where it caches what it // This function can be called very early in the bootstrap process, so
// was last called with, which is not always what you want. // we force the module_list() cache to be refreshed to ensure that it
// module_load_all_includes() calls module_list(), but if this function // contains the complete list of modules before we go on to call
// is called very early in the bootstrap process then it will be // module_load_all_includes().
// uninitialized and therefore return no modules. Instead, we have to module_list(TRUE);
// "prime" module_list() here to to values we want, specifically
// "yes rebuild the list and don't limit to bootstrap".
// @todo Remove this call after http://drupal.org/node/222109 is fixed.
module_list(TRUE, FALSE);
module_load_all_includes('install'); module_load_all_includes('install');
} }
...@@ -2601,38 +3001,34 @@ function drupal_get_schema($table = NULL, $rebuild = FALSE) { ...@@ -2601,38 +3001,34 @@ function drupal_get_schema($table = NULL, $rebuild = FALSE) {
if (!empty($schema) && (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL)) { if (!empty($schema) && (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL)) {
cache_set('schema', $schema); cache_set('schema', $schema);
} }
if ($rebuild) {
cache_clear_all('schema:', 'cache', TRUE);
}
} }
} }
if (!isset($table)) {
return $schema; return $schema;
} }
elseif (isset($schema[$table])) {
return $schema[$table];
}
else {
return FALSE;
}
}
/** /**
* @} End of "ingroup schemaapi". * @} End of "addtogroup schemaapi".
*/ */
/** /**
* @ingroup registry * @addtogroup registry
* @{ * @{
*/ */
/** /**
* Confirm that an interface is available. * Confirms that an interface is available.
* *
* This function is rarely called directly. Instead, it is registered as an * This function is rarely called directly. Instead, it is registered as an
* spl_autoload() handler, and PHP calls it for us when necessary. * spl_autoload() handler, and PHP calls it for us when necessary.
* *
* @param $interface * @param $interface
* The name of the interface to check or load. * The name of the interface to check or load.
*
* @return * @return
* TRUE if the interface is currently available, FALSE otherwise. * TRUE if the interface is currently available, FALSE otherwise.
*/ */
...@@ -2641,13 +3037,14 @@ function drupal_autoload_interface($interface) { ...@@ -2641,13 +3037,14 @@ function drupal_autoload_interface($interface) {
} }
/** /**
* Confirm that a class is available. * Confirms that a class is available.
* *
* This function is rarely called directly. Instead, it is registered as an * This function is rarely called directly. Instead, it is registered as an
* spl_autoload() handler, and PHP calls it for us when necessary. * spl_autoload() handler, and PHP calls it for us when necessary.
* *
* @param $class * @param $class
* The name of the class to check or load. * The name of the class to check or load.
*
* @return * @return
* TRUE if the class is currently available, FALSE otherwise. * TRUE if the class is currently available, FALSE otherwise.
*/ */
...@@ -2656,7 +3053,7 @@ function drupal_autoload_class($class) { ...@@ -2656,7 +3053,7 @@ function drupal_autoload_class($class) {
} }
/** /**
* Helper to check for a resource in the registry. * Checks for a resource in the registry.
* *
* @param $type * @param $type
* The type of resource we are looking up, or one of the constants * The type of resource we are looking up, or one of the constants
...@@ -2665,6 +3062,7 @@ function drupal_autoload_class($class) { ...@@ -2665,6 +3062,7 @@ function drupal_autoload_class($class) {
* @param $name * @param $name
* The name of the resource, or NULL if either of the REGISTRY_* constants * The name of the resource, or NULL if either of the REGISTRY_* constants
* is passed in. * is passed in.
*
* @return * @return
* TRUE if the resource was found, FALSE if not. * TRUE if the resource was found, FALSE if not.
* NULL if either of the REGISTRY_* constants is passed in as $type. * NULL if either of the REGISTRY_* constants is passed in as $type.
...@@ -2736,7 +3134,7 @@ function _registry_check_code($type, $name = NULL) { ...@@ -2736,7 +3134,7 @@ function _registry_check_code($type, $name = NULL) {
} }
/** /**
* Rescan all enabled modules and rebuild the registry. * Rescans all enabled modules and rebuilds the registry.
* *
* Rescans all code in modules or includes directories, storing the location of * Rescans all code in modules or includes directories, storing the location of
* each interface or class in the database. * each interface or class in the database.
...@@ -2747,25 +3145,44 @@ function registry_rebuild() { ...@@ -2747,25 +3145,44 @@ function registry_rebuild() {
} }
/** /**
* Update the registry based on the latest files listed in the database. * Updates the registry based on the latest files listed in the database.
* *
* This function should be used when system_rebuild_module_data() does not need * 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 * to be called, because it is already known that the list of files in the
* {system} table matches those in the file system. * {system} table matches those in the file system.
* *
* @return
* TRUE if the registry was rebuilt, FALSE if another thread was rebuilding
* in parallel and the current thread just waited for completion.
*
* @see registry_rebuild() * @see registry_rebuild()
*/ */
function registry_update() { function registry_update() {
// install_system_module() calls module_enable() which calls into this
// function during initial system installation, so the lock system is neither
// loaded nor does its storage exist yet.
$in_installer = drupal_installation_attempted();
if (!$in_installer && !lock_acquire(__FUNCTION__)) {
// Another request got the lock, wait for it to finish.
lock_wait(__FUNCTION__);
return FALSE;
}
require_once DRUPAL_ROOT . '/includes/registry.inc'; require_once DRUPAL_ROOT . '/includes/registry.inc';
_registry_update(); _registry_update();
if (!$in_installer) {
lock_release(__FUNCTION__);
}
return TRUE;
} }
/** /**
* @} End of "ingroup registry". * @} End of "addtogroup registry".
*/ */
/** /**
* Central static variable storage. * Provides central static variable storage.
* *
* All functions requiring a static variable to persist or cache data within * 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 * a single page request are encouraged to use this function unless it is
...@@ -2916,7 +3333,7 @@ function &drupal_static($name, $default_value = NULL, $reset = FALSE) { ...@@ -2916,7 +3333,7 @@ function &drupal_static($name, $default_value = NULL, $reset = FALSE) {
} }
/** /**
* Reset one or all centrally stored static variable(s). * Resets one or all centrally stored static variable(s).
* *
* @param $name * @param $name
* Name of the static variable to reset. Omit to reset all variables. * Name of the static variable to reset. Omit to reset all variables.
...@@ -2926,7 +3343,7 @@ function drupal_static_reset($name = NULL) { ...@@ -2926,7 +3343,7 @@ function drupal_static_reset($name = NULL) {
} }
/** /**
* Detect whether the current script is running in a command-line environment. * Detects whether the current script is running in a command-line environment.
*/ */
function drupal_is_cli() { function drupal_is_cli() {
return (!isset($_SERVER['SERVER_SOFTWARE']) && (php_sapi_name() == 'cli' || (is_numeric($_SERVER['argc']) && $_SERVER['argc'] > 0))); return (!isset($_SERVER['SERVER_SOFTWARE']) && (php_sapi_name() == 'cli' || (is_numeric($_SERVER['argc']) && $_SERVER['argc'] > 0)));
...@@ -2934,7 +3351,8 @@ function drupal_is_cli() { ...@@ -2934,7 +3351,8 @@ function drupal_is_cli() {
/** /**
* Formats text for emphasized display in a placeholder inside a sentence. * Formats text for emphasized display in a placeholder inside a sentence.
* Used automatically by t(). *
* Used automatically by format_string().
* *
* @param $text * @param $text
* The text to format (plain-text). * The text to format (plain-text).
...@@ -2947,7 +3365,7 @@ function drupal_placeholder($text) { ...@@ -2947,7 +3365,7 @@ function drupal_placeholder($text) {
} }
/** /**
* Register a function for execution on shutdown. * Registers a function for execution on shutdown.
* *
* Wrapper for register_shutdown_function() that catches thrown exceptions to * Wrapper for register_shutdown_function() that catches thrown exceptions to
* avoid "Exception thrown without a stack frame in Unknown". * avoid "Exception thrown without a stack frame in Unknown".
...@@ -2976,20 +3394,23 @@ function &drupal_register_shutdown_function($callback = NULL) { ...@@ -2976,20 +3394,23 @@ function &drupal_register_shutdown_function($callback = NULL) {
$args = func_get_args(); $args = func_get_args();
array_shift($args); array_shift($args);
// Save callback and arguments // Save callback and arguments
$callbacks[] = array('callback' => $callback, 'arguments' => $args, 'cwd' => getcwd()); $callbacks[] = array('callback' => $callback, 'arguments' => $args);
} }
return $callbacks; return $callbacks;
} }
/** /**
* Internal function used to execute registered shutdown functions. * Executes registered shutdown functions.
*/ */
function _drupal_shutdown_function() { function _drupal_shutdown_function() {
$callbacks = &drupal_register_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 { try {
while (list($key, $callback) = each($callbacks)) { while (list($key, $callback) = each($callbacks)) {
chdir($callback['cwd']);
call_user_func_array($callback['callback'], $callback['arguments']); call_user_func_array($callback['callback'], $callback['arguments']);
} }
} }
......
<?php <?php
// $Id: cache-install.inc,v 1.9 2010/05/18 18:26:30 dries Exp $
/** /**
* @file * @file
...@@ -7,7 +6,7 @@ ...@@ -7,7 +6,7 @@
*/ */
/** /**
* A stub cache implementation to be used during the installation process. * Defines a stub cache implementation to be used during installation.
* *
* The stub implementation is needed when database access is not yet available. * The stub implementation is needed when database access is not yet available.
* Because Drupal's caching system never requires that cached data be present, * Because Drupal's caching system never requires that cached data be present,
...@@ -16,22 +15,35 @@ ...@@ -16,22 +15,35 @@
* normal operations would have a negative impact on performance. * normal operations would have a negative impact on performance.
*/ */
class DrupalFakeCache extends DrupalDatabaseCache implements DrupalCacheInterface { class DrupalFakeCache extends DrupalDatabaseCache implements DrupalCacheInterface {
/**
* Overrides DrupalDatabaseCache::get().
*/
function get($cid) { function get($cid) {
return FALSE; return FALSE;
} }
/**
* Overrides DrupalDatabaseCache::getMultiple().
*/
function getMultiple(&$cids) { function getMultiple(&$cids) {
return array(); return array();
} }
/**
* Overrides DrupalDatabaseCache::set().
*/
function set($cid, $data, $expire = CACHE_PERMANENT) { function set($cid, $data, $expire = CACHE_PERMANENT) {
} }
/**
* Overrides DrupalDatabaseCache::clear().
*/
function clear($cid = NULL, $wildcard = FALSE) { function clear($cid = NULL, $wildcard = FALSE) {
// If there is a database cache, attempt to clear it whenever possible. The // If there is a database cache, attempt to clear it whenever possible. The
// reason for doing this is that the database cache can accumulate data // reason for doing this is that the database cache can accumulate data
// during installation due to any full bootstraps that may occur at the // during installation due to any full bootstraps that may occur at the
// same time (for example, AJAX requests triggered by the installer). If we // same time (for example, Ajax requests triggered by the installer). If we
// didn't try to clear it whenever this function is called, the data in the // didn't try to clear it whenever this function is called, the data in the
// cache would become stale; for example, the installer sometimes calls // cache would become stale; for example, the installer sometimes calls
// variable_set(), which updates the {variable} table and then clears the // variable_set(), which updates the {variable} table and then clears the
...@@ -53,6 +65,9 @@ class DrupalFakeCache extends DrupalDatabaseCache implements DrupalCacheInterfac ...@@ -53,6 +65,9 @@ class DrupalFakeCache extends DrupalDatabaseCache implements DrupalCacheInterfac
} }
} }
/**
* Overrides DrupalDatabaseCache::isEmpty().
*/
function isEmpty() { function isEmpty() {
return TRUE; return TRUE;
} }
......
<?php <?php
// $Id: cache.inc,v 1.49 2010/10/07 17:44:53 dries Exp $
/** /**
* Get the cache object for a cache bin. * @file
* Functions and interfaces for cache handling.
*/
/**
* Gets the cache object for a cache bin.
* *
* By default, this returns an instance of the DrupalDatabaseCache class. * By default, this returns an instance of the DrupalDatabaseCache class.
* Classes implementing DrupalCacheInterface can register themselves both as a * Classes implementing DrupalCacheInterface can register themselves both as a
* default implementation and for specific bins. * default implementation and for specific bins.
* *
* @see DrupalCacheInterface
*
* @param $bin * @param $bin
* The cache bin for which the cache object should be returned. * The cache bin for which the cache object should be returned.
* @return DrupalCacheInterface * @return DrupalCacheInterface
* The cache object associated with the specified bin. * The cache object associated with the specified bin.
*
* @see DrupalCacheInterface
*/ */
function _cache_get_object($bin) { function _cache_get_object($bin) {
// We do not use drupal_static() here because we do not want to change the // We do not use drupal_static() here because we do not want to change the
...@@ -30,7 +34,7 @@ function _cache_get_object($bin) { ...@@ -30,7 +34,7 @@ function _cache_get_object($bin) {
} }
/** /**
* Return data from the persistent cache * Returns data from the persistent cache.
* *
* Data may be stored as either plain text or as serialized data. cache_get * Data may be stored as either plain text or as serialized data. cache_get
* will automatically return unserialized objects and arrays. * will automatically return unserialized objects and arrays.
...@@ -45,19 +49,22 @@ function _cache_get_object($bin) { ...@@ -45,19 +49,22 @@ function _cache_get_object($bin) {
* *
* @return * @return
* The cache or FALSE on failure. * The cache or FALSE on failure.
*
* @see cache_set()
*/ */
function cache_get($cid, $bin = 'cache') { function cache_get($cid, $bin = 'cache') {
return _cache_get_object($bin)->get($cid); return _cache_get_object($bin)->get($cid);
} }
/** /**
* Return data from the persistent cache when given an array of cache IDs. * Returns data from the persistent cache when given an array of cache IDs.
* *
* @param $cids * @param $cids
* An array of cache IDs for the data to retrieve. This is passed by * An array of cache IDs for the data to retrieve. This is passed by
* reference, and will have the IDs successfully returned from cache removed. * reference, and will have the IDs successfully returned from cache removed.
* @param $bin * @param $bin
* The cache bin where the data is stored. * The cache bin where the data is stored.
*
* @return * @return
* An array of the items successfully returned from cache indexed by cid. * An array of the items successfully returned from cache indexed by cid.
*/ */
...@@ -66,50 +73,22 @@ function cache_get_multiple(array &$cids, $bin = 'cache') { ...@@ -66,50 +73,22 @@ function cache_get_multiple(array &$cids, $bin = 'cache') {
} }
/** /**
* Store data in the persistent cache. * Stores data in the persistent cache.
* *
* The persistent cache is split up into several cache bins. In the default * The persistent cache is split up into several cache bins. In the default
* cache implementation, each cache bin corresponds to a database table by the * cache implementation, each cache bin corresponds to a database table by the
* same name. Other implementations might want to store several bins in data * same name. Other implementations might want to store several bins in data
* structures that get flushed together. While it is not a problem for most * structures that get flushed together. While it is not a problem for most
* cache bins if the entries in them are flushed before their expire time, some * cache bins if the entries in them are flushed before their expire time, some
* might break functionality or are extremely expensive to recalculate. These * might break functionality or are extremely expensive to recalculate. The
* will be marked with a (*). The other bins expired automatically by core. * other bins are expired automatically by core. Contributed modules can add
* Contributed modules can add additional bins and get them expired * additional bins and get them expired automatically by implementing
* automatically by implementing hook_flush_caches(). * hook_flush_caches().
*
* - cache: Generic cache storage bin (used for variables, theme registry,
* locale date, list of simpletest tests etc).
*
* - cache_block: Stores the content of various blocks.
*
* - cache field: Stores the field data belonging to a given object.
*
* - cache_filter: Stores filtered pieces of content.
*
* - cache_form(*): Stores multistep forms. Flushing this bin means that some
* forms displayed to users lose their state and the data already submitted
* to them.
*
* - cache_menu: Stores the structure of visible navigation menus per page.
*
* - cache_page: Stores generated pages for anonymous users. It is flushed
* very often, whenever a page changes, at least for every ode and comment
* submission. This is the only bin affected by the page cache setting on
* the administrator panel.
*
* - cache path: Stores the system paths that have an alias.
*
* - cache update(*): Stores available releases. The update server (for
* example, drupal.org) needs to produce the relevant XML for every project
* installed on the current site. As this is different for (almost) every
* site, it's very expensive to recalculate for the update server.
* *
* The reasons for having several bins are as follows: * The reasons for having several bins are as follows:
* * - Smaller bins mean smaller database tables and allow for faster selects and
* - smaller bins mean smaller database tables and allow for faster selects and * inserts.
* inserts * - We try to put fast changing cache items and rather static ones into
* - we try to put fast changing cache items and rather static ones into
* different bins. The effect is that only the fast changing bins will need a * different bins. The effect is that only the fast changing bins will need a
* lot of writes to disk. The more static bins will also be better cacheable * lot of writes to disk. The more static bins will also be better cacheable
* with MySQL's query cache. * with MySQL's query cache.
...@@ -118,13 +97,27 @@ function cache_get_multiple(array &$cids, $bin = 'cache') { ...@@ -118,13 +97,27 @@ function cache_get_multiple(array &$cids, $bin = 'cache') {
* The cache ID of the data to store. * The cache ID of the data to store.
* @param $data * @param $data
* The data to store in the cache. Complex data types will be automatically * The data to store in the cache. Complex data types will be automatically
* serialized before insertion. * serialized before insertion. Strings will be stored as plain text and are
* Strings will be stored as plain text and not serialized. * not serialized.
* @param $bin * @param $bin
* The cache bin to store the data in. Valid core values are 'cache_block', * The cache bin to store the data in. Valid core values are:
* 'cache_bootstrap', 'cache_field', 'cache_filter', 'cache_form', * - cache: (default) Generic cache storage bin (used for theme registry,
* 'cache_menu', 'cache_page', 'cache_update' or 'cache' for the default * locale date, list of simpletest tests, etc.).
* cache. * - cache_block: Stores the content of various blocks.
* - cache_bootstrap: Stores the class registry, the system list of modules,
* the list of which modules implement which hooks, and the Drupal variable
* list.
* - cache_field: Stores the field data belonging to a given object.
* - cache_filter: Stores filtered pieces of content.
* - cache_form: Stores multistep forms. Flushing this bin means that some
* forms displayed to users lose their state and the data already submitted
* to them. This bin should not be flushed before its expired time.
* - cache_menu: Stores the structure of visible navigation menus per page.
* - cache_page: Stores generated pages for anonymous users. It is flushed
* very often, whenever a page changes, at least for every node and comment
* submission. This is the only bin affected by the page cache setting on
* the administrator panel.
* - cache_path: Stores the system paths that have an alias.
* @param $expire * @param $expire
* One of the following values: * One of the following values:
* - CACHE_PERMANENT: Indicates that the item should never be removed unless * - CACHE_PERMANENT: Indicates that the item should never be removed unless
...@@ -133,13 +126,16 @@ function cache_get_multiple(array &$cids, $bin = 'cache') { ...@@ -133,13 +126,16 @@ function cache_get_multiple(array &$cids, $bin = 'cache') {
* general cache wipe. * general cache wipe.
* - A Unix timestamp: Indicates that the item should be kept at least until * - A Unix timestamp: Indicates that the item should be kept at least until
* the given time, after which it behaves like CACHE_TEMPORARY. * the given time, after which it behaves like CACHE_TEMPORARY.
*
* @see _update_cache_set()
* @see cache_get()
*/ */
function cache_set($cid, $data, $bin = 'cache', $expire = CACHE_PERMANENT) { function cache_set($cid, $data, $bin = 'cache', $expire = CACHE_PERMANENT) {
return _cache_get_object($bin)->set($cid, $data, $expire); return _cache_get_object($bin)->set($cid, $data, $expire);
} }
/** /**
* Expire data from the cache. * Expires data from the cache.
* *
* If called without arguments, expirable entries will be cleared from the * If called without arguments, expirable entries will be cleared from the
* cache_page and cache_block bins. * cache_page and cache_block bins.
...@@ -147,15 +143,12 @@ function cache_set($cid, $data, $bin = 'cache', $expire = CACHE_PERMANENT) { ...@@ -147,15 +143,12 @@ function cache_set($cid, $data, $bin = 'cache', $expire = CACHE_PERMANENT) {
* @param $cid * @param $cid
* If set, the cache ID to delete. Otherwise, all cache entries that can * If set, the cache ID to delete. Otherwise, all cache entries that can
* expire are deleted. * expire are deleted.
*
* @param $bin * @param $bin
* If set, the bin $bin to delete from. Mandatory * If set, the cache bin to delete from. Mandatory argument if $cid is set.
* argument if $cid is set.
*
* @param $wildcard * @param $wildcard
* If $wildcard is TRUE, cache IDs starting with $cid are deleted in * If TRUE, cache IDs starting with $cid are deleted in addition to the
* addition to the exact cache ID specified by $cid. If $wildcard is * exact cache ID specified by $cid. If $wildcard is TRUE and $cid is '*',
* TRUE and $cid is '*' then the entire bin $bin is emptied. * the entire cache bin is emptied.
*/ */
function cache_clear_all($cid = NULL, $bin = NULL, $wildcard = FALSE) { function cache_clear_all($cid = NULL, $bin = NULL, $wildcard = FALSE) {
if (!isset($cid) && !isset($bin)) { if (!isset($cid) && !isset($bin)) {
...@@ -171,13 +164,14 @@ function cache_clear_all($cid = NULL, $bin = NULL, $wildcard = FALSE) { ...@@ -171,13 +164,14 @@ function cache_clear_all($cid = NULL, $bin = NULL, $wildcard = FALSE) {
} }
/** /**
* Check if a cache bin is empty. * Checks if a cache bin is empty.
* *
* A cache bin is considered empty if it does not contain any valid data for any * A cache bin is considered empty if it does not contain any valid data for any
* cache ID. * cache ID.
* *
* @param $bin * @param $bin
* The cache bin to check. * The cache bin to check.
*
* @return * @return
* TRUE if the cache bin specified is empty. * TRUE if the cache bin specified is empty.
*/ */
...@@ -186,7 +180,7 @@ function cache_is_empty($bin) { ...@@ -186,7 +180,7 @@ function cache_is_empty($bin) {
} }
/** /**
* Interface for cache implementations. * Defines an interface for cache implementations.
* *
* All cache implementations have to implement this interface. * All cache implementations have to implement this interface.
* DrupalDatabaseCache provides the default implementation, which can be * DrupalDatabaseCache provides the default implementation, which can be
...@@ -198,7 +192,7 @@ function cache_is_empty($bin) { ...@@ -198,7 +192,7 @@ function cache_is_empty($bin) {
* DrupalCacheInterface was called MyCustomCache, the following line would make * DrupalCacheInterface was called MyCustomCache, the following line would make
* Drupal use it for the 'cache_page' bin: * Drupal use it for the 'cache_page' bin:
* @code * @code
* variable_set('cache_page', 'MyCustomCache'); * variable_set('cache_class_cache_page', 'MyCustomCache');
* @endcode * @endcode
* *
* Additionally, you can register your cache implementation to be used by * Additionally, you can register your cache implementation to be used by
...@@ -208,12 +202,23 @@ function cache_is_empty($bin) { ...@@ -208,12 +202,23 @@ function cache_is_empty($bin) {
* variable_set('cache_default_class', 'MyCustomCache'); * variable_set('cache_default_class', 'MyCustomCache');
* @endcode * @endcode
* *
* To implement a completely custom cache bin, use the same variable format:
* @code
* variable_set('cache_class_custom_bin', 'MyCustomCache');
* @endcode
* To access your custom cache bin, specify the name of the bin when storing
* or retrieving cached data:
* @code
* cache_set($cid, $data, 'custom_bin', $expire);
* cache_get($cid, 'custom_bin');
* @endcode
*
* @see _cache_get_object() * @see _cache_get_object()
* @see DrupalDatabaseCache * @see DrupalDatabaseCache
*/ */
interface DrupalCacheInterface { interface DrupalCacheInterface {
/** /**
* Constructor. * Constructs a new cache interface.
* *
* @param $bin * @param $bin
* The cache bin for which the object is created. * The cache bin for which the object is created.
...@@ -221,31 +226,34 @@ interface DrupalCacheInterface { ...@@ -221,31 +226,34 @@ interface DrupalCacheInterface {
function __construct($bin); function __construct($bin);
/** /**
* Return data from the persistent cache. Data may be stored as either plain * Returns data from the persistent cache.
* text or as serialized data. cache_get will automatically return *
* unserialized objects and arrays. * Data may be stored as either plain text or as serialized data. cache_get()
* will automatically return unserialized objects and arrays.
* *
* @param $cid * @param $cid
* The cache ID of the data to retrieve. * The cache ID of the data to retrieve.
*
* @return * @return
* The cache or FALSE on failure. * The cache or FALSE on failure.
*/ */
function get($cid); function get($cid);
/** /**
* Return data from the persistent cache when given an array of cache IDs. * Returns data from the persistent cache when given an array of cache IDs.
* *
* @param $cids * @param $cids
* An array of cache IDs for the data to retrieve. This is passed by * An array of cache IDs for the data to retrieve. This is passed by
* reference, and will have the IDs successfully returned from cache * reference, and will have the IDs successfully returned from cache
* removed. * removed.
*
* @return * @return
* An array of the items successfully returned from cache indexed by cid. * An array of the items successfully returned from cache indexed by cid.
*/ */
function getMultiple(&$cids); function getMultiple(&$cids);
/** /**
* Store data in the persistent cache. * Stores data in the persistent cache.
* *
* @param $cid * @param $cid
* The cache ID of the data to store. * The cache ID of the data to store.
...@@ -266,8 +274,10 @@ interface DrupalCacheInterface { ...@@ -266,8 +274,10 @@ interface DrupalCacheInterface {
/** /**
* Expire data from the cache. If called without arguments, expirable * Expires data from the cache.
* entries will be cleared from the cache_page and cache_block bins. *
* If called without arguments, expirable entries will be cleared from the
* cache_page and cache_block bins.
* *
* @param $cid * @param $cid
* If set, the cache ID to delete. Otherwise, all cache entries that can * If set, the cache ID to delete. Otherwise, all cache entries that can
...@@ -280,7 +290,7 @@ interface DrupalCacheInterface { ...@@ -280,7 +290,7 @@ interface DrupalCacheInterface {
function clear($cid = NULL, $wildcard = FALSE); function clear($cid = NULL, $wildcard = FALSE);
/** /**
* Check if a cache bin is empty. * Checks if a cache bin is empty.
* *
* A cache bin is considered empty if it does not contain any valid data for * A cache bin is considered empty if it does not contain any valid data for
* any cache ID. * any cache ID.
...@@ -292,7 +302,7 @@ interface DrupalCacheInterface { ...@@ -292,7 +302,7 @@ interface DrupalCacheInterface {
} }
/** /**
* Default cache implementation. * Defines a default cache implementation.
* *
* This is Drupal's default cache implementation. It uses the database to store * This is Drupal's default cache implementation. It uses the database to store
* cached data. Each cache bin corresponds to a database table by the same name. * cached data. Each cache bin corresponds to a database table by the same name.
...@@ -300,32 +310,38 @@ interface DrupalCacheInterface { ...@@ -300,32 +310,38 @@ interface DrupalCacheInterface {
class DrupalDatabaseCache implements DrupalCacheInterface { class DrupalDatabaseCache implements DrupalCacheInterface {
protected $bin; protected $bin;
/**
* Constructs a new DrupalDatabaseCache object.
*/
function __construct($bin) { function __construct($bin) {
$this->bin = $bin; $this->bin = $bin;
} }
/**
* Implements DrupalCacheInterface::get().
*/
function get($cid) { function get($cid) {
try { $cids = array($cid);
// Garbage collection necessary when enforcing a minimum cache lifetime. $cache = $this->getMultiple($cids);
$this->garbageCollection($this->bin); return reset($cache);
$cache = db_query("SELECT data, created, expire, serialized FROM {" . $this->bin . "} WHERE cid = :cid", array(':cid' => $cid))->fetchObject();
return $this->prepareItem($cache);
}
catch (Exception $e) {
// If the database is never going to be available, cache requests should
// return FALSE in order to allow exception handling to occur.
return FALSE;
}
} }
/**
* Implements DrupalCacheInterface::getMultiple().
*/
function getMultiple(&$cids) { function getMultiple(&$cids) {
try { try {
// Garbage collection necessary when enforcing a minimum cache lifetime. // Garbage collection necessary when enforcing a minimum cache lifetime.
$this->garbageCollection($this->bin); $this->garbageCollection($this->bin);
$query = db_select($this->bin);
$query->fields($this->bin, array('cid', 'data', 'created', 'expire', 'serialized')); // When serving cached pages, the overhead of using db_select() was found
$query->condition($this->bin . '.cid', $cids, 'IN'); // to add around 30% overhead to the request. Since $this->bin is a
$result = $query->execute(); // variable, this means the call to db_query() here uses a concatenated
// string. This is highly discouraged under any other circumstances, and
// is used here only due to the performance overhead we would incur
// otherwise. When serving an uncached page, the overhead of using
// db_select() is a much smaller proportion of the request.
$result = db_query('SELECT cid, data, created, expire, serialized FROM {' . db_escape_table($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids));
$cache = array(); $cache = array();
foreach ($result as $item) { foreach ($result as $item) {
$item = $this->prepareItem($item); $item = $this->prepareItem($item);
...@@ -350,11 +366,31 @@ class DrupalDatabaseCache implements DrupalCacheInterface { ...@@ -350,11 +366,31 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
* The bin being requested. * The bin being requested.
*/ */
protected function garbageCollection() { protected function garbageCollection() {
global $user; $cache_lifetime = variable_get('cache_lifetime', 0);
// Garbage collection necessary when enforcing a minimum cache lifetime. // Clean-up the per-user cache expiration session data, so that the session
// handler can properly clean-up the session data for anonymous users.
if (isset($_SESSION['cache_expiration'])) {
$expire = REQUEST_TIME - $cache_lifetime;
foreach ($_SESSION['cache_expiration'] as $bin => $timestamp) {
if ($timestamp < $expire) {
unset($_SESSION['cache_expiration'][$bin]);
}
}
if (!$_SESSION['cache_expiration']) {
unset($_SESSION['cache_expiration']);
}
}
// Garbage collection of temporary items is only necessary when enforcing
// a minimum cache lifetime.
if (!$cache_lifetime) {
return;
}
// When cache lifetime is in force, avoid running garbage collection too
// often since this will remove temporary cache items indiscriminately.
$cache_flush = variable_get('cache_flush_' . $this->bin, 0); $cache_flush = variable_get('cache_flush_' . $this->bin, 0);
if ($cache_flush && ($cache_flush + variable_get('cache_lifetime', 0) <= REQUEST_TIME)) { if ($cache_flush && ($cache_flush + $cache_lifetime <= REQUEST_TIME)) {
// Reset the variable immediately to prevent a meltdown in heavy load situations. // Reset the variable immediately to prevent a meltdown in heavy load situations.
variable_set('cache_flush_' . $this->bin, 0); variable_set('cache_flush_' . $this->bin, 0);
// Time to flush old cache data // Time to flush old cache data
...@@ -366,13 +402,14 @@ class DrupalDatabaseCache implements DrupalCacheInterface { ...@@ -366,13 +402,14 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
} }
/** /**
* Prepare a cached item. * Prepares a cached item.
* *
* Checks that items are either permanent or did not expire, and unserializes * Checks that items are either permanent or did not expire, and unserializes
* data as appropriate. * data as appropriate.
* *
* @param $cache * @param $cache
* An item loaded from cache_get() or cache_get_multiple(). * An item loaded from cache_get() or cache_get_multiple().
*
* @return * @return
* The item with data unserialized as appropriate or FALSE if there is no * The item with data unserialized as appropriate or FALSE if there is no
* valid item to load. * valid item to load.
...@@ -383,17 +420,16 @@ class DrupalDatabaseCache implements DrupalCacheInterface { ...@@ -383,17 +420,16 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
if (!isset($cache->data)) { if (!isset($cache->data)) {
return FALSE; return FALSE;
} }
// If enforcing a minimum cache lifetime, validate that the data is // If the cached data is temporary and subject to a per-user minimum
// currently valid for this user before we return it by making sure the cache // lifetime, compare the cache entry timestamp with the user session
// entry was created before the timestamp in the current session's cache // cache_expiration timestamp. If the cache entry is too old, ignore it.
// timer. The cache variable is loaded into the $user object by _drupal_session_read() if ($cache->expire != CACHE_PERMANENT && variable_get('cache_lifetime', 0) && isset($_SESSION['cache_expiration'][$this->bin]) && $_SESSION['cache_expiration'][$this->bin] > $cache->created) {
// in session.inc. If the data is permanent or we're not enforcing a minimum // Ignore cache data that is too old and thus not valid for this user.
// cache lifetime always return the cached data.
if ($cache->expire != CACHE_PERMANENT && variable_get('cache_lifetime', 0) && $user->cache > $cache->created) {
// This cache data is too old and thus not valid for us, ignore it.
return FALSE; return FALSE;
} }
// If the data is permanent or not subject to a minimum cache lifetime,
// unserialize and return the cached data.
if ($cache->serialized) { if ($cache->serialized) {
$cache->data = unserialize($cache->data); $cache->data = unserialize($cache->data);
} }
...@@ -401,6 +437,9 @@ class DrupalDatabaseCache implements DrupalCacheInterface { ...@@ -401,6 +437,9 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
return $cache; return $cache;
} }
/**
* Implements DrupalCacheInterface::set().
*/
function set($cid, $data, $expire = CACHE_PERMANENT) { function set($cid, $data, $expire = CACHE_PERMANENT) {
$fields = array( $fields = array(
'serialized' => 0, 'serialized' => 0,
...@@ -427,16 +466,18 @@ class DrupalDatabaseCache implements DrupalCacheInterface { ...@@ -427,16 +466,18 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
} }
} }
/**
* Implements DrupalCacheInterface::clear().
*/
function clear($cid = NULL, $wildcard = FALSE) { function clear($cid = NULL, $wildcard = FALSE) {
global $user; global $user;
if (empty($cid)) { if (empty($cid)) {
if (variable_get('cache_lifetime', 0)) { if (variable_get('cache_lifetime', 0)) {
// We store the time in the current user's $user->cache variable which // We store the time in the current user's session. We then simulate
// will be saved into the sessions bin by _drupal_session_write(). We then // that the cache was flushed for this user by not returning cached
// simulate that the cache was flushed for this user by not returning // data that was cached before the timestamp.
// cached data that was cached before the timestamp. $_SESSION['cache_expiration'][$this->bin] = REQUEST_TIME;
$user->cache = REQUEST_TIME;
$cache_flush = variable_get('cache_flush_' . $this->bin, 0); $cache_flush = variable_get('cache_flush_' . $this->bin, 0);
if ($cache_flush == 0) { if ($cache_flush == 0) {
...@@ -489,6 +530,9 @@ class DrupalDatabaseCache implements DrupalCacheInterface { ...@@ -489,6 +530,9 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
} }
} }
/**
* Implements DrupalCacheInterface::isEmpty().
*/
function isEmpty() { function isEmpty() {
$this->garbageCollection(); $this->garbageCollection();
$query = db_select($this->bin); $query = db_select($this->bin);
......
<?php <?php
// $Id: common.inc,v 1.1244 2010/10/21 19:31:39 dries Exp $
/** /**
* @file * @file
...@@ -37,7 +36,7 @@ ...@@ -37,7 +36,7 @@
* $my_substring = drupal_substr($original_string, 0, 5); * $my_substring = drupal_substr($original_string, 0, 5);
* @endcode * @endcode
* *
* @} End of "defgroup php_wrappers". * @}
*/ */
/** /**
...@@ -71,8 +70,7 @@ define('CSS_DEFAULT', 0); ...@@ -71,8 +70,7 @@ define('CSS_DEFAULT', 0);
define('CSS_THEME', 100); define('CSS_THEME', 100);
/** /**
* The default group for JavaScript libraries, settings or jQuery plugins added * The default group for JavaScript and jQuery libraries added to the page.
* to the page.
*/ */
define('JS_LIBRARY', -100); define('JS_LIBRARY', -100);
...@@ -87,20 +85,27 @@ define('JS_DEFAULT', 0); ...@@ -87,20 +85,27 @@ define('JS_DEFAULT', 0);
define('JS_THEME', 100); define('JS_THEME', 100);
/** /**
* Error code indicating that the request made by drupal_http_request() exceeded * Error code indicating that the request exceeded the specified timeout.
* the specified timeout. *
* @see drupal_http_request()
*/ */
define('HTTP_REQUEST_TIMEOUT', 1); define('HTTP_REQUEST_TIMEOUT', -1);
/** /**
* Constants defining cache granularity for blocks and renderable arrays. * @defgroup block_caching Block Caching
* * @{
* Modules specify the caching patterns for their blocks using binary * Constants that define each block's caching state.
* combinations of these constants in their hook_block_info(): *
* $block[delta]['cache'] = DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE; * Modules specify how their blocks can be cached in their hook_block_info()
* DRUPAL_CACHE_PER_ROLE is used as a default when no caching pattern is * implementations. Caching can be turned off (DRUPAL_NO_CACHE), managed by the
* specified. Use DRUPAL_CACHE_CUSTOM to disable standard block cache and * module declaring the block (DRUPAL_CACHE_CUSTOM), or managed by the core
* implement * Block module. If the Block module is managing the cache, you can specify that
* the block is the same for every page and user (DRUPAL_CACHE_GLOBAL), or that
* it can change depending on the page (DRUPAL_CACHE_PER_PAGE) or by user
* (DRUPAL_CACHE_PER_ROLE or DRUPAL_CACHE_PER_USER). Page and user settings can
* be combined with a bitwise-binary or operator; for example,
* DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE means that the block can change
* depending on the user role or page it is on.
* *
* The block cache is cleared in cache_clear_all(), and uses the same clearing * The block cache is cleared in cache_clear_all(), and uses the same clearing
* policy than page cache (node, comment, user, taxonomy added or updated...). * policy than page cache (node, comment, user, taxonomy added or updated...).
...@@ -111,31 +116,35 @@ define('HTTP_REQUEST_TIMEOUT', 1); ...@@ -111,31 +116,35 @@ define('HTTP_REQUEST_TIMEOUT', 1);
*/ */
/** /**
* The block should not get cached. This setting should be used: * The block should not get cached.
* - for simple blocks (notably those that do not perform any db query), *
* where querying the db cache would be more expensive than directly generating * This setting should be used:
* the content. * - For simple blocks (notably those that do not perform any db query), where
* - for blocks that change too frequently. * querying the db cache would be more expensive than directly generating the
* content.
* - For blocks that change too frequently.
*/ */
define('DRUPAL_NO_CACHE', -1); define('DRUPAL_NO_CACHE', -1);
/** /**
* The block is handling its own caching in its hook_block_view(). From the * The block is handling its own caching in its hook_block_view().
* perspective of the block cache system, this is equivalent to DRUPAL_NO_CACHE. *
* Useful when time based expiration is needed or a site uses a node access * This setting is useful when time based expiration is needed or a site uses a
* which invalidates standard block cache. * node access which invalidates standard block cache.
*/ */
define('DRUPAL_CACHE_CUSTOM', -2); define('DRUPAL_CACHE_CUSTOM', -2);
/** /**
* The block or element can change depending on the roles the user viewing the * The block or element can change depending on the user's roles.
* page belongs to. This is the default setting for blocks, used when the block *
* does not specify anything. * This is the default setting for blocks, used when the block does not specify
* anything.
*/ */
define('DRUPAL_CACHE_PER_ROLE', 0x0001); define('DRUPAL_CACHE_PER_ROLE', 0x0001);
/** /**
* The block or element can change depending on the user viewing the page. * The block or element can change depending on the user.
*
* This setting can be resource-consuming for sites with large number of users, * This setting can be resource-consuming for sites with large number of users,
* and thus should only be used when DRUPAL_CACHE_PER_ROLE is not sufficient. * and thus should only be used when DRUPAL_CACHE_PER_ROLE is not sufficient.
*/ */
...@@ -147,12 +156,16 @@ define('DRUPAL_CACHE_PER_USER', 0x0002); ...@@ -147,12 +156,16 @@ define('DRUPAL_CACHE_PER_USER', 0x0002);
define('DRUPAL_CACHE_PER_PAGE', 0x0004); define('DRUPAL_CACHE_PER_PAGE', 0x0004);
/** /**
* The block or element is the same for every user on every page where it is visible. * The block or element is the same for every user and page that it is visible.
*/ */
define('DRUPAL_CACHE_GLOBAL', 0x0008); define('DRUPAL_CACHE_GLOBAL', 0x0008);
/** /**
* Add content to a specified region. * @} End of "defgroup block_caching".
*/
/**
* Adds content to a specified region.
* *
* @param $region * @param $region
* Page region the content is added to. * Page region the content is added to.
...@@ -169,7 +182,7 @@ function drupal_add_region_content($region = NULL, $data = NULL) { ...@@ -169,7 +182,7 @@ function drupal_add_region_content($region = NULL, $data = NULL) {
} }
/** /**
* Get assigned content for a given region. * Gets assigned content for a given region.
* *
* @param $region * @param $region
* A specified region to fetch content for. If NULL, all regions will be * A specified region to fetch content for. If NULL, all regions will be
...@@ -195,16 +208,16 @@ function drupal_get_region_content($region = NULL, $delimiter = ' ') { ...@@ -195,16 +208,16 @@ function drupal_get_region_content($region = NULL, $delimiter = ' ') {
} }
/** /**
* Get the name of the currently active install profile. * Gets the name of the currently active installation profile.
* *
* When this function is called during Drupal's initial installation process, * When this function is called during Drupal's initial installation process,
* the name of the profile that's about to be installed is stored in the global * the name of the profile that's about to be installed is stored in the global
* installation state. At all other times, the standard Drupal systems variable * installation state. At all other times, the standard Drupal systems variable
* table contains the name of the current profile, and we can call variable_get() * table contains the name of the current profile, and we can call
* to determine what one is active. * variable_get() to determine what one is active.
* *
* @return $profile * @return $profile
* The name of the install profile. * The name of the installation profile.
*/ */
function drupal_get_profile() { function drupal_get_profile() {
global $install_state; global $install_state;
...@@ -221,7 +234,7 @@ function drupal_get_profile() { ...@@ -221,7 +234,7 @@ function drupal_get_profile() {
/** /**
* Set the breadcrumb trail for the current page. * Sets the breadcrumb trail for the current page.
* *
* @param $breadcrumb * @param $breadcrumb
* Array of links, starting with "home" and proceeding up to but not including * Array of links, starting with "home" and proceeding up to but not including
...@@ -237,7 +250,7 @@ function drupal_set_breadcrumb($breadcrumb = NULL) { ...@@ -237,7 +250,7 @@ function drupal_set_breadcrumb($breadcrumb = NULL) {
} }
/** /**
* Get the breadcrumb trail for the current page. * Gets the breadcrumb trail for the current page.
*/ */
function drupal_get_breadcrumb() { function drupal_get_breadcrumb() {
$breadcrumb = drupal_set_breadcrumb(); $breadcrumb = drupal_set_breadcrumb();
...@@ -266,9 +279,9 @@ function drupal_get_rdf_namespaces() { ...@@ -266,9 +279,9 @@ function drupal_get_rdf_namespaces() {
} }
/** /**
* Add output to the head tag of the HTML page. * Adds output to the HEAD tag of the HTML page.
* *
* This function can be called as long the headers aren't sent. Pass no * This function can be called as long as the headers aren't sent. Pass no
* arguments (or NULL for both) to retrieve the currently stored elements. * arguments (or NULL for both) to retrieve the currently stored elements.
* *
* @param $data * @param $data
...@@ -334,7 +347,7 @@ function _drupal_default_html_head() { ...@@ -334,7 +347,7 @@ function _drupal_default_html_head() {
} }
/** /**
* Retrieve output to be displayed in the HEAD tag of the HTML page. * Retrieves output to be displayed in the HEAD tag of the HTML page.
*/ */
function drupal_get_html_head() { function drupal_get_html_head() {
$elements = drupal_add_html_head(); $elements = drupal_add_html_head();
...@@ -343,12 +356,12 @@ function drupal_get_html_head() { ...@@ -343,12 +356,12 @@ function drupal_get_html_head() {
} }
/** /**
* Add a feed URL for the current page. * Adds a feed URL for the current page.
* *
* This function can be called as long the HTML header hasn't been sent. * This function can be called as long the HTML header hasn't been sent.
* *
* @param $url * @param $url
* A url for the feed. * An internal system path or a fully qualified external URL of the feed.
* @param $title * @param $title
* The title of the feed. * The title of the feed.
*/ */
...@@ -358,16 +371,20 @@ function drupal_add_feed($url = NULL, $title = '') { ...@@ -358,16 +371,20 @@ function drupal_add_feed($url = NULL, $title = '') {
if (isset($url)) { if (isset($url)) {
$stored_feed_links[$url] = theme('feed_icon', array('url' => $url, 'title' => $title)); $stored_feed_links[$url] = theme('feed_icon', array('url' => $url, 'title' => $title));
drupal_add_html_head_link(array('rel' => 'alternate', drupal_add_html_head_link(array(
'rel' => 'alternate',
'type' => 'application/rss+xml', 'type' => 'application/rss+xml',
'title' => $title, 'title' => $title,
'href' => $url)); // Force the URL to be absolute, for consistency with other <link> tags
// output by Drupal.
'href' => url($url, array('absolute' => TRUE)),
));
} }
return $stored_feed_links; return $stored_feed_links;
} }
/** /**
* Get the feed URLs for the current page. * Gets the feed URLs for the current page.
* *
* @param $delimiter * @param $delimiter
* A delimiter to split feeds by. * A delimiter to split feeds by.
...@@ -384,7 +401,7 @@ function drupal_get_feeds($delimiter = "\n") { ...@@ -384,7 +401,7 @@ function drupal_get_feeds($delimiter = "\n") {
*/ */
/** /**
* Process a URL query parameter array to remove unwanted elements. * Processes a URL query parameter array to remove unwanted elements.
* *
* @param $query * @param $query
* (optional) An array to be processed. Defaults to $_GET. * (optional) An array to be processed. Defaults to $_GET.
...@@ -429,13 +446,13 @@ function drupal_get_query_parameters(array $query = NULL, array $exclude = array ...@@ -429,13 +446,13 @@ function drupal_get_query_parameters(array $query = NULL, array $exclude = array
} }
/** /**
* Split an URL-encoded query string into an array. * Splits a URL-encoded query string into an array.
* *
* @param $query * @param $query
* The query string to split. * The query string to split.
* *
* @return * @return
* An array of url decoded couples $param_name => $value. * An array of URL decoded couples $param_name => $value.
*/ */
function drupal_get_query_array($query) { function drupal_get_query_array($query) {
$result = array(); $result = array();
...@@ -449,7 +466,7 @@ function drupal_get_query_array($query) { ...@@ -449,7 +466,7 @@ function drupal_get_query_array($query) {
} }
/** /**
* Parse an array into a valid, rawurlencoded query string. * Parses an array into a valid, rawurlencoded query string.
* *
* This differs from http_build_query() as we need to rawurlencode() (instead of * This differs from http_build_query() as we need to rawurlencode() (instead of
* urlencode()) all query parameters. * urlencode()) all query parameters.
...@@ -490,13 +507,19 @@ function drupal_http_build_query(array $query, $parent = '') { ...@@ -490,13 +507,19 @@ function drupal_http_build_query(array $query, $parent = '') {
} }
/** /**
* Prepare a 'destination' URL query parameter for use in combination with drupal_goto(). * Prepares a 'destination' URL query parameter for use with drupal_goto().
* *
* Used to direct the user back to the referring page after completing a form. * 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 * By default the current URL is returned. If a destination exists in the
* previous request, that destination is returned. As such, a destination can * previous request, that destination is returned. As such, a destination can
* persist across multiple pages. * persist across multiple pages.
* *
* @return
* An associative array containing the key:
* - destination: The path provided via the destination query string or, if
* not available, the current path.
*
* @see current_path()
* @see drupal_goto() * @see drupal_goto()
*/ */
function drupal_get_destination() { function drupal_get_destination() {
...@@ -521,11 +544,11 @@ function drupal_get_destination() { ...@@ -521,11 +544,11 @@ function drupal_get_destination() {
} }
/** /**
* Wrapper around parse_url() to parse a system URL string into an associative array, suitable for url(). * Parses 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 * 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 * system, such as via url(). It should not be used for URLs that come from
* sources, or URLs that link to external resources. * external sources, or URLs that link to external resources.
* *
* The returned array contains a 'path' that may be passed separately to url(). * The returned array contains a 'path' that may be passed separately to url().
* For example: * For example:
...@@ -618,7 +641,7 @@ function drupal_encode_path($path) { ...@@ -618,7 +641,7 @@ function drupal_encode_path($path) {
} }
/** /**
* Send the user to a different Drupal page. * Sends the user to a different Drupal page.
* *
* This issues an on-site HTTP redirect. The function makes sure the redirected * This issues an on-site HTTP redirect. The function makes sure the redirected
* URL is formatted correctly. * URL is formatted correctly.
...@@ -639,20 +662,23 @@ function drupal_encode_path($path) { ...@@ -639,20 +662,23 @@ function drupal_encode_path($path) {
* callback. * callback.
* *
* @param $path * @param $path
* A Drupal path or a full URL. * (optional) A Drupal path or a full URL, which will be passed to url() to
* compute the redirect for the URL.
* @param $options * @param $options
* An associative array of additional URL options to pass to url(). * (optional) An associative array of additional URL options to pass to url().
* @param $http_response_code * @param $http_response_code
* Valid values for an actual "goto" as per RFC 2616 section 10.3 are: * (optional) The HTTP status code to use for the redirection, defaults to
* - 301 Moved Permanently (the recommended value for most redirects) * 302. The valid values for 3xx redirection status codes are defined in
* - 302 Found (default in Drupal and PHP, sometimes used for spamming search * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3 RFC 2616 @endlink
* engines) * and the
* - 303 See Other * @link http://tools.ietf.org/html/draft-reschke-http-status-308-07 draft for the new HTTP status codes: @endlink
* - 304 Not Modified * - 301: Moved Permanently (the recommended value for most redirects).
* - 305 Use Proxy * - 302: Found (default in Drupal and PHP, sometimes used for spamming search
* - 307 Temporary Redirect (alternative to "503 Site Down for Maintenance") * engines).
* Note: Other values are defined by RFC 2616, but are rarely used and poorly * - 303: See Other.
* supported. * - 304: Not Modified.
* - 305: Use Proxy.
* - 307: Temporary Redirect.
* *
* @see drupal_get_destination() * @see drupal_get_destination()
* @see url() * @see url()
...@@ -683,7 +709,7 @@ function drupal_goto($path = '', array $options = array(), $http_response_code = ...@@ -683,7 +709,7 @@ function drupal_goto($path = '', array $options = array(), $http_response_code =
} }
/** /**
* Deliver a "site is under maintenance" message to the browser. * Delivers a "site is under maintenance" message to the browser.
* *
* Page callback functions wanting to report a "site offline" message should * Page callback functions wanting to report a "site offline" message should
* return MENU_SITE_OFFLINE instead of calling drupal_site_offline(). However, * return MENU_SITE_OFFLINE instead of calling drupal_site_offline(). However,
...@@ -695,7 +721,7 @@ function drupal_site_offline() { ...@@ -695,7 +721,7 @@ function drupal_site_offline() {
} }
/** /**
* Deliver a "page not found" error to the browser. * Delivers a "page not found" error to the browser.
* *
* Page callback functions wanting to report a "page not found" message should * Page callback functions wanting to report a "page not found" message should
* return MENU_NOT_FOUND instead of calling drupal_not_found(). However, * return MENU_NOT_FOUND instead of calling drupal_not_found(). However,
...@@ -707,67 +733,65 @@ function drupal_not_found() { ...@@ -707,67 +733,65 @@ function drupal_not_found() {
} }
/** /**
* Deliver a "access denied" error to the browser. * Delivers an "access denied" error to the browser.
* *
* Page callback functions wanting to report an "access denied" message should * Page callback functions wanting to report an "access denied" message should
* return MENU_ACCESS_DENIED instead of calling drupal_access_denied(). However, * return MENU_ACCESS_DENIED instead of calling drupal_access_denied(). However,
* functions that are invoked in contexts where that return value might not * functions that are invoked in contexts where that return value might not
* bubble up to menu_execute_active_handler() should call drupal_access_denied(). * bubble up to menu_execute_active_handler() should call
* drupal_access_denied().
*/ */
function drupal_access_denied() { function drupal_access_denied() {
drupal_deliver_page(MENU_ACCESS_DENIED); drupal_deliver_page(MENU_ACCESS_DENIED);
} }
/** /**
* Perform an HTTP request. * Performs an HTTP request.
* *
* This is a flexible and powerful HTTP client implementation. Correctly * This is a flexible and powerful HTTP client implementation. Correctly
* handles GET, POST, PUT or any other HTTP requests. Handles redirects. * handles GET, POST, PUT or any other HTTP requests. Handles redirects.
* *
* @param $url * @param $url
* A string containing a fully qualified URI. * A string containing a fully qualified URI.
* @param $options * @param array $options
* (optional) An array which can have one or more of following keys: * (optional) An array that can have one or more of the following elements:
* - headers * - headers: An array containing request headers to send as name/value pairs.
* An array containing request headers to send as name/value pairs. * - method: A string containing the request method. Defaults to 'GET'.
* - method * - data: A string containing the request body, formatted as
* A string containing the request method. Defaults to 'GET'. * 'param=value&param=value&...'. Defaults to NULL.
* - data * - max_redirects: An integer representing how many times a redirect
* A string containing the request body. Defaults to NULL. * may be followed. Defaults to 3.
* - max_redirects * - timeout: A float representing the maximum number of seconds the function
* An integer representing how many times a redirect may be followed. * call may take. The default is 30 seconds. If a timeout occurs, the error
* 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. * code is set to the HTTP_REQUEST_TIMEOUT constant.
* - context * - context: A context resource created with stream_context_create().
* A context resource created with stream_context_create(). *
* @return * @return object
* An object which can have one or more of the following parameters: * An object that can have one or more of the following components:
* - request * - request: A string containing the request body that was sent.
* A string containing the request body that was sent. * - code: An integer containing the response status code, or the error code
* - code * if an error occurred.
* An integer containing the response status code, or the error code if * - protocol: The response protocol (e.g. HTTP/1.1 or HTTP/1.0).
* an error occurred. * - status_message: The status message from the response, if a response was
* - protocol * received.
* The response protocol (e.g. HTTP/1.1 or HTTP/1.0). * - redirect_code: If redirected, an integer containing the initial response
* - status_message * status code.
* The status message from the response, if a response was received. * - redirect_url: If redirected, a string containing the URL of the redirect
* - redirect_code * target.
* If redirected, an integer containing the initial response status code. * - error: If an error occurred, the error message. Otherwise not set.
* - redirect_url * - headers: An array containing the response headers as name/value pairs.
* If redirected, a string containing the redirection location. * HTTP header names are case-insensitive (RFC 2616, section 4.2), so for
* - error * easy access the array keys are returned in lower case.
* If an error occurred, the error message. Otherwise not set. * - data: A string containing the response body that was received.
* - 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()) { function drupal_http_request($url, array $options = array()) {
// Allow an alternate HTTP client library to replace Drupal's default
// implementation.
$override_function = variable_get('drupal_http_request_function', FALSE);
if (!empty($override_function) && function_exists($override_function)) {
return $override_function($url, $options);
}
$result = new stdClass(); $result = new stdClass();
// Parse the URL and make sure we can handle the schema. // Parse the URL and make sure we can handle the schema.
...@@ -796,10 +820,51 @@ function drupal_http_request($url, array $options = array()) { ...@@ -796,10 +820,51 @@ function drupal_http_request($url, array $options = array()) {
'timeout' => 30.0, 'timeout' => 30.0,
'context' => NULL, 'context' => NULL,
); );
// Merge the default headers.
$options['headers'] += array(
'User-Agent' => 'Drupal (+http://drupal.org/)',
);
// stream_socket_client() requires timeout to be a float. // stream_socket_client() requires timeout to be a float.
$options['timeout'] = (float) $options['timeout']; $options['timeout'] = (float) $options['timeout'];
// Use a proxy if one is defined and the host is not on the excluded list.
$proxy_server = variable_get('proxy_server', '');
if ($proxy_server && _drupal_http_use_proxy($uri['host'])) {
// Set the scheme so we open a socket to the proxy server.
$uri['scheme'] = 'proxy';
// Set the path to be the full URL.
$uri['path'] = $url;
// Since the URL is passed as the path, we won't use the parsed query.
unset($uri['query']);
// Add in username and password to Proxy-Authorization header if needed.
if ($proxy_username = variable_get('proxy_username', '')) {
$proxy_password = variable_get('proxy_password', '');
$options['headers']['Proxy-Authorization'] = 'Basic ' . base64_encode($proxy_username . (!empty($proxy_password) ? ":" . $proxy_password : ''));
}
// Some proxies reject requests with any User-Agent headers, while others
// require a specific one.
$proxy_user_agent = variable_get('proxy_user_agent', '');
// The default value matches neither condition.
if ($proxy_user_agent === NULL) {
unset($options['headers']['User-Agent']);
}
elseif ($proxy_user_agent) {
$options['headers']['User-Agent'] = $proxy_user_agent;
}
}
switch ($uri['scheme']) { switch ($uri['scheme']) {
case 'proxy':
// Make the socket connection to a proxy server.
$socket = 'tcp://' . $proxy_server . ':' . variable_get('proxy_port', 8080);
// The Host header still needs to match the real request.
$options['headers']['Host'] = $uri['host'];
$options['headers']['Host'] .= isset($uri['port']) && $uri['port'] != 80 ? ':' . $uri['port'] : '';
break;
case 'http': case 'http':
case 'feed': case 'feed':
$port = isset($uri['port']) ? $uri['port'] : 80; $port = isset($uri['port']) ? $uri['port'] : 80;
...@@ -809,12 +874,14 @@ function drupal_http_request($url, array $options = array()) { ...@@ -809,12 +874,14 @@ function drupal_http_request($url, array $options = array()) {
// checking the host that do not take into account the port number. // checking the host that do not take into account the port number.
$options['headers']['Host'] = $uri['host'] . ($port != 80 ? ':' . $port : ''); $options['headers']['Host'] = $uri['host'] . ($port != 80 ? ':' . $port : '');
break; break;
case 'https': case 'https':
// Note: Only works when PHP is compiled with OpenSSL support. // Note: Only works when PHP is compiled with OpenSSL support.
$port = isset($uri['port']) ? $uri['port'] : 443; $port = isset($uri['port']) ? $uri['port'] : 443;
$socket = 'ssl://' . $uri['host'] . ':' . $port; $socket = 'ssl://' . $uri['host'] . ':' . $port;
$options['headers']['Host'] = $uri['host'] . ($port != 443 ? ':' . $port : ''); $options['headers']['Host'] = $uri['host'] . ($port != 443 ? ':' . $port : '');
break; break;
default: default:
$result->error = 'invalid schema ' . $uri['scheme']; $result->error = 'invalid schema ' . $uri['scheme'];
$result->code = -1003; $result->code = -1003;
...@@ -839,7 +906,7 @@ function drupal_http_request($url, array $options = array()) { ...@@ -839,7 +906,7 @@ function drupal_http_request($url, array $options = array()) {
// Mark that this request failed. This will trigger a check of the web // 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 // server's ability to make outgoing HTTP requests the next time that
// requirements checking is performed. // requirements checking is performed.
// See system_requirements() // See system_requirements().
variable_set('drupal_http_request_fails', TRUE); variable_set('drupal_http_request_fails', TRUE);
return $result; return $result;
...@@ -851,11 +918,6 @@ function drupal_http_request($url, array $options = array()) { ...@@ -851,11 +918,6 @@ function drupal_http_request($url, array $options = array()) {
$path .= '?' . $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 // 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 // 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 // at least HEAD/GET requests, and Squid always requires Content-Length in
...@@ -867,7 +929,7 @@ function drupal_http_request($url, array $options = array()) { ...@@ -867,7 +929,7 @@ function drupal_http_request($url, array $options = array()) {
// If the server URL has a user then attempt to use basic authentication. // If the server URL has a user then attempt to use basic authentication.
if (isset($uri['user'])) { if (isset($uri['user'])) {
$options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (!empty($uri['pass']) ? ":" . $uri['pass'] : '')); $options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (isset($uri['pass']) ? ':' . $uri['pass'] : ''));
} }
// If the database prefix is being used by SimpleTest to run the tests in a copied // If the database prefix is being used by SimpleTest to run the tests in a copied
...@@ -922,7 +984,9 @@ function drupal_http_request($url, array $options = array()) { ...@@ -922,7 +984,9 @@ function drupal_http_request($url, array $options = array()) {
return $result; return $result;
} }
// Parse response headers from the response body. // Parse response headers from the response body.
list($response, $result->data) = explode("\r\n\r\n", $response, 2); // 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); $response = preg_split("/\r\n|\n|\r/", $response);
// Parse the response status line. // Parse the response status line.
...@@ -1014,7 +1078,9 @@ function drupal_http_request($url, array $options = array()) { ...@@ -1014,7 +1078,9 @@ function drupal_http_request($url, array $options = array()) {
$result = drupal_http_request($location, $options); $result = drupal_http_request($location, $options);
$result->redirect_code = $code; $result->redirect_code = $code;
} }
if (!isset($result->redirect_url)) {
$result->redirect_url = $location; $result->redirect_url = $location;
}
break; break;
default: default:
$result->error = $status_message; $result->error = $status_message;
...@@ -1022,10 +1088,30 @@ function drupal_http_request($url, array $options = array()) { ...@@ -1022,10 +1088,30 @@ function drupal_http_request($url, array $options = array()) {
return $result; return $result;
} }
/**
* Helper function for determining hosts excluded from needing a proxy.
*
* @return
* TRUE if a proxy should be used for this host.
*/
function _drupal_http_use_proxy($host) {
$proxy_exceptions = variable_get('proxy_exceptions', array('localhost', '127.0.0.1'));
return !in_array(strtolower($host), $proxy_exceptions, TRUE);
}
/** /**
* @} End of "HTTP handling". * @} End of "HTTP handling".
*/ */
/**
* Strips slashes from a string or array of strings.
*
* Callback for array_walk() within fix_gpx_magic().
*
* @param $item
* An individual string or array of strings from superglobals.
*/
function _fix_gpc_magic(&$item) { function _fix_gpc_magic(&$item) {
if (is_array($item)) { if (is_array($item)) {
array_walk($item, '_fix_gpc_magic'); array_walk($item, '_fix_gpc_magic');
...@@ -1036,11 +1122,19 @@ function _fix_gpc_magic(&$item) { ...@@ -1036,11 +1122,19 @@ function _fix_gpc_magic(&$item) {
} }
/** /**
* Helper function to strip slashes from $_FILES skipping over the tmp_name keys * Strips slashes from $_FILES items.
* since PHP generates single backslashes for file paths on Windows systems. *
* Callback for array_walk() within fix_gpc_magic().
* *
* tmp_name does not have backslashes added see * The tmp_name key is skipped keys since PHP generates single backslashes for
* http://php.net/manual/en/features.file-upload.php#42280 * file paths on Windows systems.
*
* @param $item
* An item from $_FILES.
* @param $key
* The key for the item within $_FILES.
*
* @see http://php.net/manual/en/features.file-upload.php#42280
*/ */
function _fix_gpc_magic_files(&$item, $key) { function _fix_gpc_magic_files(&$item, $key) {
if ($key != 'tmp_name') { if ($key != 'tmp_name') {
...@@ -1054,18 +1148,21 @@ function _fix_gpc_magic_files(&$item, $key) { ...@@ -1054,18 +1148,21 @@ function _fix_gpc_magic_files(&$item, $key) {
} }
/** /**
* Fix double-escaping problems caused by "magic quotes" in some PHP installations. * Fixes double-escaping caused by "magic quotes" in some PHP installations.
*
* @see _fix_gpc_magic()
* @see _fix_gpc_magic_files()
*/ */
function fix_gpc_magic() { function fix_gpc_magic() {
$fixed = &drupal_static(__FUNCTION__, FALSE); static $fixed = FALSE;
if (!$fixed && ini_get('magic_quotes_gpc')) { if (!$fixed && ini_get('magic_quotes_gpc')) {
array_walk($_GET, '_fix_gpc_magic'); array_walk($_GET, '_fix_gpc_magic');
array_walk($_POST, '_fix_gpc_magic'); array_walk($_POST, '_fix_gpc_magic');
array_walk($_COOKIE, '_fix_gpc_magic'); array_walk($_COOKIE, '_fix_gpc_magic');
array_walk($_REQUEST, '_fix_gpc_magic'); array_walk($_REQUEST, '_fix_gpc_magic');
array_walk($_FILES, '_fix_gpc_magic_files'); array_walk($_FILES, '_fix_gpc_magic_files');
$fixed = TRUE;
} }
$fixed = TRUE;
} }
/** /**
...@@ -1075,12 +1172,14 @@ function fix_gpc_magic() { ...@@ -1075,12 +1172,14 @@ function fix_gpc_magic() {
*/ */
/** /**
* Verify the syntax of the given e-mail address. * Verifies the syntax of the given e-mail address.
* *
* Empty e-mail addresses are allowed. See RFC 2822 for details. * This uses the
* @link http://php.net/manual/filter.filters.validate.php PHP e-mail validation filter. @endlink
* *
* @param $mail * @param $mail
* A string containing an e-mail address. * A string containing an e-mail address.
*
* @return * @return
* TRUE if the address is in a valid format. * TRUE if the address is in a valid format.
*/ */
...@@ -1089,7 +1188,7 @@ function valid_email_address($mail) { ...@@ -1089,7 +1188,7 @@ function valid_email_address($mail) {
} }
/** /**
* Verify the syntax of the given URL. * Verifies the syntax of the given URL.
* *
* This function should only be used on actual URLs. It should not be used for * This function should only be used on actual URLs. It should not be used for
* Drupal menu paths, which can contain arbitrary characters. * Drupal menu paths, which can contain arbitrary characters.
...@@ -1098,6 +1197,7 @@ function valid_email_address($mail) { ...@@ -1098,6 +1197,7 @@ function valid_email_address($mail) {
* The URL to verify. * The URL to verify.
* @param $absolute * @param $absolute
* Whether the URL is absolute (beginning with a scheme such as "http:"). * Whether the URL is absolute (beginning with a scheme such as "http:").
*
* @return * @return
* TRUE if the URL is in a valid format. * TRUE if the URL is in a valid format.
*/ */
...@@ -1130,7 +1230,7 @@ function valid_url($url, $absolute = FALSE) { ...@@ -1130,7 +1230,7 @@ function valid_url($url, $absolute = FALSE) {
*/ */
/** /**
* Register an event for the current visitor to the flood control mechanism. * Registers an event for the current visitor to the flood control mechanism.
* *
* @param $name * @param $name
* The name of an event. * The name of an event.
...@@ -1157,7 +1257,7 @@ function flood_register_event($name, $window = 3600, $identifier = NULL) { ...@@ -1157,7 +1257,7 @@ function flood_register_event($name, $window = 3600, $identifier = NULL) {
} }
/** /**
* Make the flood control mechanism forget about an event for the current visitor. * Makes the flood control mechanism forget an event for the current visitor.
* *
* @param $name * @param $name
* The name of an event. * The name of an event.
...@@ -1175,7 +1275,7 @@ function flood_clear_event($name, $identifier = NULL) { ...@@ -1175,7 +1275,7 @@ function flood_clear_event($name, $identifier = NULL) {
} }
/** /**
* Checks whether user is allowed to proceed with the specified event. * Checks whether a user is allowed to proceed with the specified event.
* *
* Events can have thresholds saying that each user can only do that event * 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 * a certain number of times in a time window. This function verifies that the
...@@ -1269,7 +1369,7 @@ function drupal_strip_dangerous_protocols($uri) { ...@@ -1269,7 +1369,7 @@ function drupal_strip_dangerous_protocols($uri) {
} }
/** /**
* Strips dangerous protocols (e.g. 'javascript:') from a URI and encodes it for output to an HTML attribute value. * Strips dangerous protocols from a URI and encodes it for output to HTML.
* *
* @param $uri * @param $uri
* A plain-text URI that might contain dangerous protocols. * A plain-text URI that might contain dangerous protocols.
...@@ -1289,7 +1389,7 @@ function check_url($uri) { ...@@ -1289,7 +1389,7 @@ function check_url($uri) {
} }
/** /**
* Very permissive XSS/HTML filter for admin-only use. * Applies a very permissive XSS/HTML filter for admin-only use.
* *
* Use only for fields where it is impractical to use the * Use only for fields where it is impractical to use the
* whole filter system, but where some (mainly inline) mark-up * whole filter system, but where some (mainly inline) mark-up
...@@ -1299,11 +1399,11 @@ function check_url($uri) { ...@@ -1299,11 +1399,11 @@ function check_url($uri) {
* for scripts and styles. * for scripts and styles.
*/ */
function filter_xss_admin($string) { 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')); return filter_xss($string, array('a', 'abbr', 'acronym', 'address', 'article', 'aside', 'b', 'bdi', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'command', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'figcaption', 'figure', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'mark', 'menu', 'meter', 'nav', 'ol', 'output', 'p', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'small', 'span', 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time', 'tr', 'tt', 'u', 'ul', 'var', 'wbr'));
} }
/** /**
* Filters an HTML string to prevent cross-site-scripting (XSS) vulnerabilities. * Filters HTML to prevent cross-site-scripting (XSS) vulnerabilities.
* *
* Based on kses by Ulf Harnhammar, see http://sourceforge.net/projects/kses. * 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. * For examples of various XSS attacks, see: http://ha.ckers.org/xss.html.
...@@ -1334,21 +1434,21 @@ function filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', ...@@ -1334,21 +1434,21 @@ function filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite',
if (!drupal_validate_utf8($string)) { if (!drupal_validate_utf8($string)) {
return ''; return '';
} }
// Store the text format // Store the text format.
_filter_xss_split($allowed_tags, TRUE); _filter_xss_split($allowed_tags, TRUE);
// Remove NULL characters (ignored by some browsers) // Remove NULL characters (ignored by some browsers).
$string = str_replace(chr(0), '', $string); $string = str_replace(chr(0), '', $string);
// Remove Netscape 4 JS entities // Remove Netscape 4 JS entities.
$string = preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string); $string = preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string);
// Defuse all HTML entities // Defuse all HTML entities.
$string = str_replace('&', '&amp;', $string); $string = str_replace('&', '&amp;', $string);
// Change back only well-formed entities in our whitelist // Change back only well-formed entities in our whitelist:
// Decimal numeric entities // Decimal numeric entities.
$string = preg_replace('/&amp;#([0-9]+;)/', '&#\1', $string); $string = preg_replace('/&amp;#([0-9]+;)/', '&#\1', $string);
// Hexadecimal numeric entities // Hexadecimal numeric entities.
$string = preg_replace('/&amp;#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/', '&#x\1', $string); $string = preg_replace('/&amp;#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/', '&#x\1', $string);
// Named entities // Named entities.
$string = preg_replace('/&amp;([A-Za-z][A-Za-z0-9]*;)/', '&\1', $string); $string = preg_replace('/&amp;([A-Za-z][A-Za-z0-9]*;)/', '&\1', $string);
return preg_replace_callback('% return preg_replace_callback('%
...@@ -1372,6 +1472,7 @@ function filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', ...@@ -1372,6 +1472,7 @@ function filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite',
* If $store is FALSE then the array has one element, the HTML tag to process. * If $store is FALSE then the array has one element, the HTML tag to process.
* @param $store * @param $store
* Whether to store $m. * Whether to store $m.
*
* @return * @return
* If the element isn't allowed, an empty string. Otherwise, the cleaned up * If the element isn't allowed, an empty string. Otherwise, the cleaned up
* version of the HTML element. * version of the HTML element.
...@@ -1387,16 +1488,16 @@ function _filter_xss_split($m, $store = FALSE) { ...@@ -1387,16 +1488,16 @@ function _filter_xss_split($m, $store = FALSE) {
$string = $m[1]; $string = $m[1];
if (substr($string, 0, 1) != '<') { if (substr($string, 0, 1) != '<') {
// We matched a lone ">" character // We matched a lone ">" character.
return '&gt;'; return '&gt;';
} }
elseif (strlen($string) == 1) { elseif (strlen($string) == 1) {
// We matched a lone "<" character // We matched a lone "<" character.
return '&lt;'; return '&lt;';
} }
if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9]+)([^>]*)>?|(<!--.*?-->)$%', $string, $matches)) { if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9]+)([^>]*)>?|(<!--.*?-->)$%', $string, $matches)) {
// Seriously malformed // Seriously malformed.
return ''; return '';
} }
...@@ -1410,7 +1511,7 @@ function _filter_xss_split($m, $store = FALSE) { ...@@ -1410,7 +1511,7 @@ function _filter_xss_split($m, $store = FALSE) {
} }
if (!isset($allowed_html[strtolower($elem)])) { if (!isset($allowed_html[strtolower($elem)])) {
// Disallowed HTML element // Disallowed HTML element.
return ''; return '';
} }
...@@ -1426,7 +1527,7 @@ function _filter_xss_split($m, $store = FALSE) { ...@@ -1426,7 +1527,7 @@ function _filter_xss_split($m, $store = FALSE) {
$attrlist = preg_replace('%(\s?)/\s*$%', '\1', $attrlist, -1, $count); $attrlist = preg_replace('%(\s?)/\s*$%', '\1', $attrlist, -1, $count);
$xhtml_slash = $count ? ' /' : ''; $xhtml_slash = $count ? ' /' : '';
// Clean up attributes // Clean up attributes.
$attr2 = implode(' ', _filter_xss_attributes($attrlist)); $attr2 = implode(' ', _filter_xss_attributes($attrlist));
$attr2 = preg_replace('/[<>]/', '', $attr2); $attr2 = preg_replace('/[<>]/', '', $attr2);
$attr2 = strlen($attr2) ? ' ' . $attr2 : ''; $attr2 = strlen($attr2) ? ' ' . $attr2 : '';
...@@ -1451,7 +1552,7 @@ function _filter_xss_attributes($attr) { ...@@ -1451,7 +1552,7 @@ function _filter_xss_attributes($attr) {
switch ($mode) { switch ($mode) {
case 0: case 0:
// Attribute name, href for instance // Attribute name, href for instance.
if (preg_match('/^([-a-zA-Z]+)/', $attr, $match)) { if (preg_match('/^([-a-zA-Z]+)/', $attr, $match)) {
$attrname = strtolower($match[1]); $attrname = strtolower($match[1]);
$skip = ($attrname == 'style' || substr($attrname, 0, 2) == 'on'); $skip = ($attrname == 'style' || substr($attrname, 0, 2) == 'on');
...@@ -1461,7 +1562,7 @@ function _filter_xss_attributes($attr) { ...@@ -1461,7 +1562,7 @@ function _filter_xss_attributes($attr) {
break; break;
case 1: case 1:
// Equals sign or valueless ("selected") // Equals sign or valueless ("selected").
if (preg_match('/^\s*=\s*/', $attr)) { if (preg_match('/^\s*=\s*/', $attr)) {
$working = 1; $mode = 2; $working = 1; $mode = 2;
$attr = preg_replace('/^\s*=\s*/', '', $attr); $attr = preg_replace('/^\s*=\s*/', '', $attr);
...@@ -1478,7 +1579,7 @@ function _filter_xss_attributes($attr) { ...@@ -1478,7 +1579,7 @@ function _filter_xss_attributes($attr) {
break; break;
case 2: case 2:
// Attribute value, a URL after href= for instance // Attribute value, a URL after href= for instance.
if (preg_match('/^"([^"]*)"(\s+|$)/', $attr, $match)) { if (preg_match('/^"([^"]*)"(\s+|$)/', $attr, $match)) {
$thisval = filter_xss_bad_protocol($match[1]); $thisval = filter_xss_bad_protocol($match[1]);
...@@ -1515,7 +1616,7 @@ function _filter_xss_attributes($attr) { ...@@ -1515,7 +1616,7 @@ function _filter_xss_attributes($attr) {
} }
if ($working == 0) { if ($working == 0) {
// not well formed, remove and try again // Not well formed; remove and try again.
$attr = preg_replace('/ $attr = preg_replace('/
^ ^
( (
...@@ -1539,15 +1640,16 @@ function _filter_xss_attributes($attr) { ...@@ -1539,15 +1640,16 @@ function _filter_xss_attributes($attr) {
} }
/** /**
* Processes an HTML attribute value and ensures it does not contain an URL with a disallowed protocol (e.g. javascript:). * Processes an HTML attribute value and strips dangerous protocols from URLs.
* *
* @param $string * @param $string
* The string with the attribute value. * The string with the attribute value.
* @param $decode * @param $decode
* (Deprecated) Whether to decode entities in the $string. Set to FALSE if the * (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 * $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, * is deprecated and will be removed in Drupal 8. To process a plain-text URI,
* call drupal_strip_dangerous_protocols() or check_url() instead. * call drupal_strip_dangerous_protocols() or check_url() instead.
*
* @return * @return
* Cleaned up and HTML-escaped version of $string. * Cleaned up and HTML-escaped version of $string.
*/ */
...@@ -1556,6 +1658,10 @@ function filter_xss_bad_protocol($string, $decode = TRUE) { ...@@ -1556,6 +1658,10 @@ function filter_xss_bad_protocol($string, $decode = TRUE) {
// @todo Remove the $decode parameter in Drupal 8, and always assume an HTML // @todo Remove the $decode parameter in Drupal 8, and always assume an HTML
// string that needs decoding. // string that needs decoding.
if ($decode) { if ($decode) {
if (!function_exists('decode_entities')) {
require_once DRUPAL_ROOT . '/includes/unicode.inc';
}
$string = decode_entities($string); $string = decode_entities($string);
} }
return check_plain(drupal_strip_dangerous_protocols($string)); return check_plain(drupal_strip_dangerous_protocols($string));
...@@ -1597,7 +1703,7 @@ function format_rss_channel($title, $link, $description, $items, $langcode = NUL ...@@ -1597,7 +1703,7 @@ function format_rss_channel($title, $link, $description, $items, $langcode = NUL
} }
/** /**
* Format a single RSS item. * Formats a single RSS item.
* *
* Arbitrary elements may be added using the $args associative array. * Arbitrary elements may be added using the $args associative array.
*/ */
...@@ -1613,7 +1719,7 @@ function format_rss_item($title, $link, $description, $args = array()) { ...@@ -1613,7 +1719,7 @@ function format_rss_item($title, $link, $description, $args = array()) {
} }
/** /**
* Format XML elements. * Formats XML elements.
* *
* @param $array * @param $array
* An array where each item represents an element and is either a: * An array where each item represents an element and is either a:
...@@ -1652,7 +1758,7 @@ function format_xml_elements($array) { ...@@ -1652,7 +1758,7 @@ function format_xml_elements($array) {
} }
/** /**
* Format a string containing a count of items. * Formats a string containing a count of items.
* *
* This function ensures that the string is pluralized correctly. Since t() is * 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 * called by this function, make sure not to pass already-localized strings to
...@@ -1668,37 +1774,33 @@ function format_xml_elements($array) { ...@@ -1668,37 +1774,33 @@ function format_xml_elements($array) {
* $output = format_plural($update_count, * $output = format_plural($update_count,
* 'Changed the content type of 1 post from %old-type to %new-type.', * '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.', * 'Changed the content type of @count posts from %old-type to %new-type.',
* array('%old-type' => $info->old_type, '%new-type' => $info->new_type))); * array('%old-type' => $info->old_type, '%new-type' => $info->new_type));
* @endcode * @endcode
* *
* @param $count * @param $count
* The item count to display. * The item count to display.
* @param $singular * @param $singular
* The string for the singular case. Please make sure it is clear this is * The string for the singular case. Make sure it is clear this is singular,
* singular, to ease translation (e.g. use "1 new comment" instead of "1 new"). * to ease translation (e.g. use "1 new comment" instead of "1 new"). Do not
* Do not use @count in the singular string. * use @count in the singular string.
* @param $plural * @param $plural
* The string for the plural case. Please make sure it is clear this is plural, * The string for the plural case. Make sure it is clear this is plural, to
* to ease translation. Use @count in place of the item count, as in "@count * ease translation. Use @count in place of the item count, as in
* new comments". * "@count new comments".
* @param $args * @param $args
* An associative array of replacements to make after translation. Incidences * An associative array of replacements to make after translation. Instances
* of any key in this array are replaced with the corresponding value. * 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: * Based on the first character of the key, the value is escaped and/or
* - !variable: inserted as is * themed. See format_string(). Note that you do not need to include @count
* - @variable: escape plain text to HTML (check_plain) * in this array; this replacement is done automatically for the plural case.
* - %variable: escape text and theme as a placeholder for user-submitted
* content (check_plain + theme_placeholder)
* Note that you do not need to include @count in this array.
* This replacement is done automatically for the plural case.
* @param $options * @param $options
* An associative array of additional options, with the following keys: * An associative array of additional options. See t() for allowed 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 * @return
* A translated string. * A translated string.
*
* @see t()
* @see format_string()
*/ */
function format_plural($count, $singular, $plural, array $args = array(), array $options = array()) { function format_plural($count, $singular, $plural, array $args = array(), array $options = array()) {
$args['@count'] = $count; $args['@count'] = $count;
...@@ -1708,7 +1810,8 @@ function format_plural($count, $singular, $plural, array $args = array(), array ...@@ -1708,7 +1810,8 @@ function format_plural($count, $singular, $plural, array $args = array(), array
// Get the plural index through the gettext formula. // 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; $index = (function_exists('locale_get_plural')) ? locale_get_plural($count, isset($options['langcode']) ? $options['langcode'] : NULL) : -1;
// Backwards compatibility. // If the index cannot be computed, use the plural as a fallback (which
// allows for most flexiblity with the replaceable @count value).
if ($index < 0) { if ($index < 0) {
return t($plural, $args, $options); return t($plural, $args, $options);
} }
...@@ -1727,11 +1830,12 @@ function format_plural($count, $singular, $plural, array $args = array(), array ...@@ -1727,11 +1830,12 @@ function format_plural($count, $singular, $plural, array $args = array(), array
} }
/** /**
* Parse a given byte count. * Parses a given byte count.
* *
* @param $size * @param $size
* A size expressed as a number of bytes with optional SI or IEC binary unit * 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). * prefix (e.g. 2, 3K, 5MB, 10G, 6GiB, 8 bytes, 9mbytes).
*
* @return * @return
* An integer representation of the size in bytes. * An integer representation of the size in bytes.
*/ */
...@@ -1748,13 +1852,14 @@ function parse_size($size) { ...@@ -1748,13 +1852,14 @@ function parse_size($size) {
} }
/** /**
* Generate a string representation for the given byte count. * Generates a string representation for the given byte count.
* *
* @param $size * @param $size
* A size in bytes. * A size in bytes.
* @param $langcode * @param $langcode
* Optional language code to translate to a language other than what is used * Optional language code to translate to a language other than what is used
* to display the page. * to display the page.
*
* @return * @return
* A translated string representation of the size. * A translated string representation of the size.
*/ */
...@@ -1787,19 +1892,20 @@ function format_size($size, $langcode = NULL) { ...@@ -1787,19 +1892,20 @@ function format_size($size, $langcode = NULL) {
} }
/** /**
* Format a time interval with the requested granularity. * Formats a time interval with the requested granularity.
* *
* @param $timestamp * @param $interval
* The length of the interval in seconds. * The length of the interval in seconds.
* @param $granularity * @param $granularity
* How many different units to display in the string. * How many different units to display in the string.
* @param $langcode * @param $langcode
* Optional language code to translate to a language other than * Optional language code to translate to a language other than
* what is used to display the page. * what is used to display the page.
*
* @return * @return
* A translated string representation of the interval. * A translated string representation of the interval.
*/ */
function format_interval($timestamp, $granularity = 2, $langcode = NULL) { function format_interval($interval, $granularity = 2, $langcode = NULL) {
$units = array( $units = array(
'1 year|@count years' => 31536000, '1 year|@count years' => 31536000,
'1 month|@count months' => 2592000, '1 month|@count months' => 2592000,
...@@ -1812,9 +1918,9 @@ function format_interval($timestamp, $granularity = 2, $langcode = NULL) { ...@@ -1812,9 +1918,9 @@ function format_interval($timestamp, $granularity = 2, $langcode = NULL) {
$output = ''; $output = '';
foreach ($units as $key => $value) { foreach ($units as $key => $value) {
$key = explode('|', $key); $key = explode('|', $key);
if ($timestamp >= $value) { if ($interval >= $value) {
$output .= ($output ? ' ' : '') . format_plural(floor($timestamp / $value), $key[0], $key[1], array(), array('langcode' => $langcode)); $output .= ($output ? ' ' : '') . format_plural(floor($interval / $value), $key[0], $key[1], array(), array('langcode' => $langcode));
$timestamp %= $value; $interval %= $value;
$granularity--; $granularity--;
} }
...@@ -1826,26 +1932,30 @@ function format_interval($timestamp, $granularity = 2, $langcode = NULL) { ...@@ -1826,26 +1932,30 @@ function format_interval($timestamp, $granularity = 2, $langcode = NULL) {
} }
/** /**
* Format a date with the given configured format or a custom format string. * Formats a date, using a date type or a custom date format string.
*
* Drupal allows administrators to select formatting strings for 'short',
* 'medium' and 'long' date formats. This function can handle these formats,
* as well as any custom format.
* *
* @param $timestamp * @param $timestamp
* The exact date to format, as a UNIX timestamp. * A UNIX timestamp to format.
* @param $type * @param $type
* The format to use. Can be "short", "medium" or "long" for the preconfigured * (optional) The format to use, one of:
* date formats. If "custom" is specified, then $format is required as well. * - '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 * @param $format
* A PHP date format string as required by date(). A backslash should be used * (optional) If $type is 'custom', a PHP date format string suitable for
* before a character to avoid interpreting the character as part of a date * input to date(). Use a backslash to escape ordinary text, so it does not
* format. * get interpreted as date format characters.
* @param $timezone * @param $timezone
* Time zone identifier; if omitted, the user's time zone is used. * (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 * @param $langcode
* Optional language code to translate to a language other than what is used * (optional) Language code to translate to. Defaults to the language used to
* to display the page. * display the page.
*
* @return * @return
* A translated date string in the requested format. * A translated date string in the requested format.
*/ */
...@@ -1876,16 +1986,27 @@ function format_date($timestamp, $type = 'medium', $format = '', $timezone = NUL ...@@ -1876,16 +1986,27 @@ function format_date($timestamp, $type = 'medium', $format = '', $timezone = NUL
case 'short': case 'short':
$format = variable_get('date_format_short', 'm/d/Y - H:i'); $format = variable_get('date_format_short', 'm/d/Y - H:i');
break; break;
case 'long': case 'long':
$format = variable_get('date_format_long', 'l, F j, Y - H:i'); $format = variable_get('date_format_long', 'l, F j, Y - H:i');
break; break;
case 'custom': case 'custom':
// No change to format. // No change to format.
break; break;
case 'medium': case 'medium':
default: 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'); $format = variable_get('date_format_medium', 'D, m/d/Y - H:i');
} }
break;
}
// Create a DateTime object from the timestamp. // Create a DateTime object from the timestamp.
$date_time = date_create('@' . $timestamp); $date_time = date_create('@' . $timestamp);
...@@ -1912,10 +2033,11 @@ function format_date($timestamp, $type = 'medium', $format = '', $timezone = NUL ...@@ -1912,10 +2033,11 @@ function format_date($timestamp, $type = 'medium', $format = '', $timezone = NUL
/** /**
* Returns an ISO8601 formatted date based on the given date. * Returns an ISO8601 formatted date based on the given date.
* *
* Can be used as a callback for RDF mappings. * Callback for use within hook_rdf_mapping() implementations.
* *
* @param $date * @param $date
* A UNIX timestamp. * A UNIX timestamp.
*
* @return string * @return string
* An ISO8601 formatted date. * An ISO8601 formatted date.
*/ */
...@@ -1926,7 +2048,9 @@ function date_iso8601($date) { ...@@ -1926,7 +2048,9 @@ function date_iso8601($date) {
} }
/** /**
* Callback function for preg_replace_callback(). * Translates a formatted date string.
*
* Callback for preg_replace_callback() within format_date().
*/ */
function _format_date_callback(array $matches = NULL, $new_langcode = NULL) { function _format_date_callback(array $matches = NULL, $new_langcode = NULL) {
// We cache translations to avoid redundant and rather costly calls to t(). // We cache translations to avoid redundant and rather costly calls to t().
...@@ -1962,7 +2086,7 @@ function _format_date_callback(array $matches = NULL, $new_langcode = NULL) { ...@@ -1962,7 +2086,7 @@ function _format_date_callback(array $matches = NULL, $new_langcode = NULL) {
/** /**
* Format a username. * Format a username.
* *
* By default, the passed in object's 'name' property is used if it exists, or * 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 * else, the site-defined value for the 'anonymous' variable. However, a module
* may override this by implementing hook_username_alter(&$name, $account). * may override this by implementing hook_username_alter(&$name, $account).
* *
...@@ -1993,8 +2117,9 @@ function format_username($account) { ...@@ -1993,8 +2117,9 @@ function format_username($account) {
* alternative than url(). * alternative than url().
* *
* @param $path * @param $path
* The internal path or external URL being linked to, such as "node/34" or * (optional) The internal path or external URL being linked to, such as
* "http://example.com/foo". A few notes: * "node/34" or "http://example.com/foo". The default value is equivalent to
* passing in '<front>'. A few notes:
* - If you provide a full URL, it will be considered an external URL. * - 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 * - 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, * considered an internal link. In this case, it should be a system URL,
...@@ -2010,7 +2135,8 @@ function format_username($account) { ...@@ -2010,7 +2135,8 @@ function format_username($account) {
* include them in $path, or use $options['query'] to let this function * include them in $path, or use $options['query'] to let this function
* URL encode them. * URL encode them.
* @param $options * @param $options
* An associative array of additional options, with the following elements: * (optional) An associative array of additional options, with the following
* elements:
* - 'query': An array of query key/value-pairs (without any URL-encoding) to * - 'query': An array of query key/value-pairs (without any URL-encoding) to
* append to the URL. * append to the URL.
* - 'fragment': A fragment identifier (named anchor) to append to the URL. * - 'fragment': A fragment identifier (named anchor) to append to the URL.
...@@ -2026,7 +2152,7 @@ function format_username($account) { ...@@ -2026,7 +2152,7 @@ function format_username($account) {
* for the URL. If $options['language'] is omitted, the global $language_url * for the URL. If $options['language'] is omitted, the global $language_url
* will be used. * will be used.
* - 'https': Whether this URL should point to a secure location. If not * - '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 * 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 * respectively. TRUE enforces HTTPS and FALSE enforces HTTP, but HTTPS can
* only be enforced when the variable 'https' is set to TRUE. * only be enforced when the variable 'https' is set to TRUE.
* - 'base_url': Only used internally, to modify the base URL when a language * - 'base_url': Only used internally, to modify the base URL when a language
...@@ -2041,8 +2167,8 @@ function format_username($account) { ...@@ -2041,8 +2167,8 @@ function format_username($account) {
* Drupal on a web server that cannot be configured to automatically find * Drupal on a web server that cannot be configured to automatically find
* index.php, then hook_url_outbound_alter() can be implemented to force * index.php, then hook_url_outbound_alter() can be implemented to force
* this value to 'index.php'. * this value to 'index.php'.
* - 'entity_type': The entity type of the object that called url(). Only set if * - 'entity_type': The entity type of the object that called url(). Only
* url() is invoked by entity_uri(). * set if url() is invoked by entity_uri().
* - 'entity': The entity object (such as a node) for which the URL is being * - 'entity': The entity object (such as a node) for which the URL is being
* generated. Only set if url() is invoked by entity_uri(). * generated. Only set if url() is invoked by entity_uri().
* *
...@@ -2170,7 +2296,7 @@ function url($path = NULL, array $options = array()) { ...@@ -2170,7 +2296,7 @@ function url($path = NULL, array $options = array()) {
} }
/** /**
* Return TRUE if a path is external to Drupal (e.g. http://example.com). * Returns TRUE if a path is external to Drupal (e.g. http://example.com).
* *
* If a path cannot be assessed by Drupal's menu handler, then we must * If a path cannot be assessed by Drupal's menu handler, then we must
* treat it as potentially insecure. * treat it as potentially insecure.
...@@ -2178,18 +2304,20 @@ function url($path = NULL, array $options = array()) { ...@@ -2178,18 +2304,20 @@ function url($path = NULL, array $options = array()) {
* @param $path * @param $path
* The internal path or external URL being linked to, such as "node/34" or * The internal path or external URL being linked to, such as "node/34" or
* "http://example.com/foo". * "http://example.com/foo".
*
* @return * @return
* Boolean TRUE or FALSE, where TRUE indicates an external path. * Boolean TRUE or FALSE, where TRUE indicates an external path.
*/ */
function url_is_external($path) { function url_is_external($path) {
$colonpos = strpos($path, ':'); $colonpos = strpos($path, ':');
// Only call the slow drupal_strip_dangerous_protocols() if $path contains a // Avoid calling drupal_strip_dangerous_protocols() if there is any
// ':' before any / ? or #. // slash (/), hash (#) or question_mark (?) before the colon (:)
// occurrence - if any - as this would clearly mean it is not a URL.
return $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path; return $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path;
} }
/** /**
* Format an attribute string for a HTTP header. * Formats an attribute string for an HTTP header.
* *
* @param $attributes * @param $attributes
* An associative array of attributes such as 'rel'. * An associative array of attributes such as 'rel'.
...@@ -2211,22 +2339,44 @@ function drupal_http_header_attributes(array $attributes = array()) { ...@@ -2211,22 +2339,44 @@ function drupal_http_header_attributes(array $attributes = array()) {
} }
/** /**
* Format an attribute string to insert in a tag. * Converts an associative array to an XML/HTML tag attribute string.
*
* Each array key and its value will be formatted into an attribute string.
* If a value is itself an array, then its elements are concatenated to a single
* space-delimited string (for example, a class attribute with multiple values).
*
* Attribute values are sanitized by running them through check_plain().
* Attribute names are not automatically sanitized. When using user-supplied
* attribute names, it is strongly recommended to allow only white-listed names,
* since certain attributes carry security risks and can be abused.
* *
* Each array key and its value will be formatted into an HTML attribute string. * Examples of security aspects when using drupal_attributes:
* If a value is itself an array, then each array element is concatenated with a * @code
* space between each value (e.g. a multi-value class attribute). * // By running the value in the following statement through check_plain,
* // the malicious script is neutralized.
* drupal_attributes(array('title' => t('<script>steal_cookie();</script>')));
*
* // 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 * @param $attributes
* An associative array of HTML attributes. * An associative array of key-value pairs to be converted to attributes.
*
* @return * @return
* An HTML string ready for insertion in a tag. * A string ready for insertion in a tag (starts with a space).
*
* @ingroup sanitization
*/ */
function drupal_attributes(array $attributes = array()) { function drupal_attributes(array $attributes = array()) {
foreach ($attributes as $attribute => &$data) { foreach ($attributes as $attribute => &$data) {
if (is_array($data)) { $data = implode(' ', (array) $data);
$data = implode(' ', $data);
}
$data = $attribute . '="' . check_plain($data) . '"'; $data = $attribute . '="' . check_plain($data) . '"';
} }
return $attributes ? ' ' . implode(' ', $attributes) : ''; return $attributes ? ' ' . implode(' ', $attributes) : '';
...@@ -2235,34 +2385,49 @@ function drupal_attributes(array $attributes = array()) { ...@@ -2235,34 +2385,49 @@ function drupal_attributes(array $attributes = array()) {
/** /**
* Formats an internal or external URL link as an HTML anchor tag. * Formats an internal or external URL link as an HTML anchor tag.
* *
* This function correctly handles aliased paths, and adds an 'active' class * This function correctly handles aliased paths and adds an 'active' class
* attribute to links that point to the current page (for theming), so all * 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 * internal links output by modules should be generated by this function if
* possible. * possible.
* *
* @param $text * However, for links enclosed in translatable text you should use t() and
* The link text for the anchor tag. * embed the HTML anchor tag directly in the translated string. For example:
* @param $path * @code
* t('Visit the <a href="@url">settings</a> page', array('@url' => url('admin')));
* @endcode
* This keeps the context of the link title ('settings' in the example) for
* translators.
*
* @param string $text
* The translated link text for the anchor tag.
* @param string $path
* The internal path or external URL being linked to, such as "node/34" or * 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 * "http://example.com/foo". After the url() function is called to construct
* the URL from $path and $options, the resulting URL is passed through * 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 * check_plain() before it is inserted into the HTML anchor tag, to ensure
* well-formed HTML. See url() for more information and notes. * well-formed HTML. See url() for more information and notes.
* @param array $options * @param array $options
* An associative array of additional options, with the following elements: * An associative array of additional options. Defaults to an empty array. It
* may contain the following elements.
* - 'attributes': An associative array of HTML attributes to apply to the * - 'attributes': An associative array of HTML attributes to apply to the
* anchor tag. * 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 * - '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 * example, to make an image tag into a link, this must be set to TRUE, or
* you will see the escaped HTML image tag. * you will see the escaped HTML image tag. $text is not sanitized if
* 'html' is TRUE. The calling function must ensure that $text is already
* safe.
* - 'language': An optional language object. If the path being linked to is * - 'language': An optional language object. If the path being linked to is
* internal to the site, $options['language'] is used to determine whether * internal to the site, $options['language'] is used to determine whether
* the link is "active", or pointing to the current page (the language as * 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(). * well as the path must match). This element is also used by url().
* - Additional $options elements used by the url() function. * - Additional $options elements used by the url() function.
* *
* @return * @return string
* An HTML string containing a link to the given path. * An HTML string containing a link to the given path.
*
* @see url()
*/ */
function l($text, $path, array $options = array()) { function l($text, $path, array $options = array()) {
global $language_url; global $language_url;
...@@ -2298,7 +2463,7 @@ function l($text, $path, array $options = array()) { ...@@ -2298,7 +2463,7 @@ function l($text, $path, array $options = array()) {
// rendering. // rendering.
if (variable_get('theme_link', TRUE)) { if (variable_get('theme_link', TRUE)) {
drupal_theme_initialize(); drupal_theme_initialize();
$registry = theme_get_registry(); $registry = theme_get_registry(FALSE);
// We don't want to duplicate functionality that's in theme(), so any // 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' // 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 // theme hook should simply result in theme() being called. This includes
...@@ -2350,9 +2515,9 @@ function l($text, $path, array $options = array()) { ...@@ -2350,9 +2515,9 @@ function l($text, $path, array $options = array()) {
* basis in hook_page_delivery_callback_alter(). * basis in hook_page_delivery_callback_alter().
* *
* For example, the same page callback function can be used for an HTML * 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 * 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 * 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 * delivery callback function will send it as an HTML page or an Ajax
* response, as appropriate. * response, as appropriate.
* *
* In order for page callbacks to be reusable in different delivery formats, * In order for page callbacks to be reusable in different delivery formats,
...@@ -2406,7 +2571,7 @@ function drupal_deliver_page($page_callback_result, $default_delivery_callback = ...@@ -2406,7 +2571,7 @@ function drupal_deliver_page($page_callback_result, $default_delivery_callback =
} }
/** /**
* Package and send the result of a page callback to the browser as HTML. * Packages and sends the result of a page callback to the browser as HTML.
* *
* @param $page_callback_result * @param $page_callback_result
* The result of a page callback. Can be one of: * The result of a page callback. Can be one of:
...@@ -2426,6 +2591,10 @@ function drupal_deliver_html_page($page_callback_result) { ...@@ -2426,6 +2591,10 @@ function drupal_deliver_html_page($page_callback_result) {
drupal_add_http_header('Content-Type', 'text/html; charset=utf-8'); drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
} }
// Send appropriate HTTP-Header for browsers and search engines.
global $language;
drupal_add_http_header('Content-Language', $language->language);
// Menu status constants are integers; page content is a string or array. // Menu status constants are integers; page content is a string or array.
if (is_int($page_callback_result)) { if (is_int($page_callback_result)) {
// @todo: Break these up into separate functions? // @todo: Break these up into separate functions?
...@@ -2436,6 +2605,9 @@ function drupal_deliver_html_page($page_callback_result) { ...@@ -2436,6 +2605,9 @@ function drupal_deliver_html_page($page_callback_result) {
watchdog('page not found', check_plain($_GET['q']), NULL, WATCHDOG_WARNING); watchdog('page not found', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);
// Check for and return a fast 404 page if configured.
drupal_fast_404();
// Keep old path for reference, and to allow forms to redirect to it. // Keep old path for reference, and to allow forms to redirect to it.
if (!isset($_GET['destination'])) { if (!isset($_GET['destination'])) {
$_GET['destination'] = $_GET['q']; $_GET['destination'] = $_GET['q'];
...@@ -2452,7 +2624,7 @@ function drupal_deliver_html_page($page_callback_result) { ...@@ -2452,7 +2624,7 @@ function drupal_deliver_html_page($page_callback_result) {
if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) { if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {
// Standard 404 handler. // Standard 404 handler.
drupal_set_title(t('Page not found')); drupal_set_title(t('Page not found'));
$return = t('The requested page could not be found.'); $return = t('The requested page "@path" could not be found.', array('@path' => request_uri()));
} }
drupal_set_page_content($return); drupal_set_page_content($return);
...@@ -2508,7 +2680,7 @@ function drupal_deliver_html_page($page_callback_result) { ...@@ -2508,7 +2680,7 @@ function drupal_deliver_html_page($page_callback_result) {
} }
/** /**
* Perform end-of-request tasks. * Performs end-of-request tasks.
* *
* This function sets the page cache if appropriate, and allows modules to * This function sets the page cache if appropriate, and allows modules to
* react to the closing of the page by calling hook_exit(). * react to the closing of the page by calling hook_exit().
...@@ -2535,7 +2707,7 @@ function drupal_page_footer() { ...@@ -2535,7 +2707,7 @@ function drupal_page_footer() {
} }
/** /**
* Perform end-of-request tasks. * Performs end-of-request tasks.
* *
* In some cases page requests need to end without calling drupal_page_footer(). * 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 * In these cases, call drupal_exit() instead. There should rarely be a reason
...@@ -2557,7 +2729,7 @@ function drupal_exit($destination = NULL) { ...@@ -2557,7 +2729,7 @@ function drupal_exit($destination = NULL) {
} }
/** /**
* Form an associative array from a linear array. * Forms an associative array from a linear array.
* *
* This function walks through the provided array and constructs an associative * 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 * array out of it. The keys of the resulting array will be the values of the
...@@ -2569,7 +2741,8 @@ function drupal_exit($destination = NULL) { ...@@ -2569,7 +2741,8 @@ function drupal_exit($destination = NULL) {
* A linear array. * A linear array.
* @param $function * @param $function
* A name of a function to apply to all values before output. * A name of a function to apply to all values before output.
* @result *
* @return
* An associative array. * An associative array.
*/ */
function drupal_map_assoc($array, $function = NULL) { function drupal_map_assoc($array, $function = NULL) {
...@@ -2625,17 +2798,17 @@ function drupal_set_time_limit($time_limit) { ...@@ -2625,17 +2798,17 @@ function drupal_set_time_limit($time_limit) {
* The name of the item for which the path is requested. * The name of the item for which the path is requested.
* *
* @return * @return
* The path to the requested item. * The path to the requested item or an empty string if the item is not found.
*/ */
function drupal_get_path($type, $name) { function drupal_get_path($type, $name) {
return dirname(drupal_get_filename($type, $name)); return dirname(drupal_get_filename($type, $name));
} }
/** /**
* Return the base URL path (i.e., directory) of the Drupal installation. * Returns 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 * base_path() adds a "/" to the beginning and end of the returned path if the
* not empty. At the very least, this will return "/". * path is not empty. At the very least, this will return "/".
* *
* Examples: * Examples:
* - http://example.com returns "/" because the path is empty. * - http://example.com returns "/" because the path is empty.
...@@ -2646,12 +2819,12 @@ function base_path() { ...@@ -2646,12 +2819,12 @@ function base_path() {
} }
/** /**
* Add a LINK tag with a distinct 'rel' attribute to the page's HEAD. * Adds 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, * This function can be called as long the HTML header hasn't been sent, which
* which on normal pages is up through the preprocess step of theme('html'). * on normal pages is up through the preprocess step of theme('html'). Adding
* Adding a link will overwrite a prior link with the exact same 'rel' and * a link will overwrite a prior link with the exact same 'rel' and 'href'
* 'href' attributes. * attributes.
* *
* @param $attributes * @param $attributes
* Associative array of element attributes including 'href' and 'rel'. * Associative array of element attributes including 'href' and 'rel'.
...@@ -2705,17 +2878,18 @@ function drupal_add_html_head_link($attributes, $header = FALSE) { ...@@ -2705,17 +2878,18 @@ function drupal_add_html_head_link($attributes, $header = FALSE) {
* @param $data * @param $data
* (optional) The stylesheet data to be added, depending on what is passed * (optional) The stylesheet data to be added, depending on what is passed
* through to the $options['type'] parameter: * through to the $options['type'] parameter:
* - 'file': The path to the CSS file relative to the base_path(), e.g., * - 'file': The path to the CSS file relative to the base_path(), or a
* "modules/devel/devel.css". Note that Modules should always prefix the * stream wrapper URI. For example: "modules/devel/devel.css" or
* names of their CSS files with the module name; for example, * "public://generated_css/stylesheet_1.css". Note that Modules should
* system-menus.css rather than simply menus.css. Themes can override * always prefix the names of their CSS files with the module name; for
* module-supplied CSS files based on their filenames, and this prefixing * example, system-menus.css rather than simply menus.css. Themes can
* helps prevent confusing name collisions for theme developers. See * override module-supplied CSS files based on their filenames, and this
* drupal_get_css() where the overrides are performed. Also, if the * 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, * 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 * 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 * 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 * 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 * 'mymodule-name-rtl.css' file added to the list, if exists in the same
* directory. This CSS file should contain overrides for properties which * directory. This CSS file should contain overrides for properties which
* should be reversed or otherwise different in a right-to-left display. * should be reversed or otherwise different in a right-to-left display.
...@@ -2740,7 +2914,7 @@ function drupal_add_html_head_link($attributes, $header = FALSE) { ...@@ -2740,7 +2914,7 @@ function drupal_add_html_head_link($attributes, $header = FALSE) {
* - 'group': A number identifying the group in which to add the stylesheet. * - 'group': A number identifying the group in which to add the stylesheet.
* Available constants are: * Available constants are:
* - CSS_SYSTEM: Any system-layer CSS. * - CSS_SYSTEM: Any system-layer CSS.
* - CSS_DEFAULT: Any module-layer CSS. * - CSS_DEFAULT: (default) Any module-layer CSS.
* - CSS_THEME: Any theme-layer CSS. * - CSS_THEME: Any theme-layer CSS.
* The group number serves as a weight: the markup for loading a stylesheet * 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 * within a lower weight group is output to the page before the markup for
...@@ -2787,6 +2961,8 @@ function drupal_add_html_head_link($attributes, $header = FALSE) { ...@@ -2787,6 +2961,8 @@ function drupal_add_html_head_link($attributes, $header = FALSE) {
* *
* @return * @return
* An array of queued cascading stylesheets. * An array of queued cascading stylesheets.
*
* @see drupal_get_css()
*/ */
function drupal_add_css($data = NULL, $options = NULL) { function drupal_add_css($data = NULL, $options = NULL) {
$css = &drupal_static(__FUNCTION__, array()); $css = &drupal_static(__FUNCTION__, array());
...@@ -2845,7 +3021,7 @@ function drupal_add_css($data = NULL, $options = NULL) { ...@@ -2845,7 +3021,7 @@ function drupal_add_css($data = NULL, $options = NULL) {
} }
/** /**
* Returns a themed representation of all stylesheets that should be attached to the page. * Returns a themed representation of all stylesheets to attach to the page.
* *
* It loads the CSS in order, with 'module' first, then 'theme' afterwards. * It loads the CSS in order, with 'module' first, then 'theme' afterwards.
* This ensures proper cascading of styles so themes can easily override * This ensures proper cascading of styles so themes can easily override
...@@ -2867,8 +3043,11 @@ function drupal_add_css($data = NULL, $options = NULL) { ...@@ -2867,8 +3043,11 @@ function drupal_add_css($data = NULL, $options = NULL) {
* (optional) If set to TRUE, this function skips calling drupal_alter() on * (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 * $css, useful when the calling function passes a $css array that has already
* been altered. * been altered.
*
* @return * @return
* A string of XHTML CSS tags. * A string of XHTML CSS tags.
*
* @see drupal_add_css()
*/ */
function drupal_get_css($css = NULL, $skip_alter = FALSE) { function drupal_get_css($css = NULL, $skip_alter = FALSE) {
if (!isset($css)) { if (!isset($css)) {
...@@ -2883,12 +3062,24 @@ function drupal_get_css($css = NULL, $skip_alter = FALSE) { ...@@ -2883,12 +3062,24 @@ function drupal_get_css($css = NULL, $skip_alter = FALSE) {
// Sort CSS items, so that they appear in the correct order. // Sort CSS items, so that they appear in the correct order.
uasort($css, 'drupal_sort_css_js'); uasort($css, 'drupal_sort_css_js');
// Provide the page with information about the individual CSS files used,
// information not otherwise available when CSS aggregation is enabled. The
// setting is attached later in this function, but is set here, so that CSS
// files removed below are still considered "used" and prevented from being
// added in a later AJAX request.
// Skip if no files were added to the page or jQuery.extend() will overwrite
// the Drupal.settings.ajaxPageState.css object with an empty array.
if (!empty($css)) {
// Cast the array to an object to be on the safe side even if not empty.
$setting['ajaxPageState']['css'] = (object) array_fill_keys(array_keys($css), 1);
}
// Remove the overridden CSS files. Later CSS files override former ones. // Remove the overridden CSS files. Later CSS files override former ones.
$previous_item = array(); $previous_item = array();
foreach ($css as $key => $item) { foreach ($css as $key => $item) {
if ($item['type'] == 'file') { if ($item['type'] == 'file') {
// If defined, force a unique basename for this file. // If defined, force a unique basename for this file.
$basename = isset($item['basename']) ? $item['basename'] : basename($item['data']); $basename = isset($item['basename']) ? $item['basename'] : drupal_basename($item['data']);
if (isset($previous_item[$basename])) { if (isset($previous_item[$basename])) {
// Remove the previous item that shared the same base name. // Remove the previous item that shared the same base name.
unset($css[$previous_item[$basename]]); unset($css[$previous_item[$basename]]);
...@@ -2903,24 +3094,36 @@ function drupal_get_css($css = NULL, $skip_alter = FALSE) { ...@@ -2903,24 +3094,36 @@ function drupal_get_css($css = NULL, $skip_alter = FALSE) {
'#items' => $css, '#items' => $css,
); );
// Provide the page with information about the individual CSS files used, if (!empty($setting)) {
// 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); $styles['#attached']['js'][] = array('type' => 'setting', 'data' => $setting);
}
return drupal_render($styles); return drupal_render($styles);
} }
/** /**
* Function used by uasort to sort the array structures returned by drupal_add_css() and drupal_add_js(). * Sorts CSS and JavaScript resources.
*
* Callback for uasort() within:
* - drupal_get_css()
* - drupal_get_js()
* *
* This sort order helps optimize front-end performance while providing modules * This sort order helps optimize front-end performance while providing modules
* and themes with the necessary control for ordering the CSS and JavaScript * and themes with the necessary control for ordering the CSS and JavaScript
* appearing on a page. * appearing on a page.
*
* @param $a
* First item for comparison. The compared items should be associative arrays
* of member items from drupal_add_css() or drupal_add_js().
* @param $b
* Second item for comparison.
*
* @see drupal_add_css()
* @see drupal_add_js()
*/ */
function drupal_sort_css_js($a, $b) { function drupal_sort_css_js($a, $b) {
// First order by group, so that, for example, all items in the CSS_SYSTEM // 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 // 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 // all items in the CSS_THEME group. Modules may create additional groups by
// defining their own constants. // defining their own constants.
if ($a['group'] < $b['group']) { if ($a['group'] < $b['group']) {
...@@ -2969,7 +3172,7 @@ function drupal_sort_css_js($a, $b) { ...@@ -2969,7 +3172,7 @@ function drupal_sort_css_js($a, $b) {
* are always groupable, and items of the 'external' type are never groupable. * 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 * 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 * their relative order. This requirement may result in multiple groups for the
* same type, media, and browsers, if needed to accomodate other items in * same type, media, and browsers, if needed to accommodate other items in
* between. * between.
* *
* @param $css * @param $css
...@@ -2983,6 +3186,7 @@ function drupal_sort_css_js($a, $b) { ...@@ -2983,6 +3186,7 @@ function drupal_sort_css_js($a, $b) {
* 'items' key, which is the subset of items from $css that are in the group. * 'items' key, which is the subset of items from $css that are in the group.
* *
* @see drupal_pre_render_styles() * @see drupal_pre_render_styles()
* @see system_element_info()
*/ */
function drupal_group_css($css) { function drupal_group_css($css) {
$groups = array(); $groups = array();
...@@ -3065,6 +3269,7 @@ function drupal_group_css($css) { ...@@ -3065,6 +3269,7 @@ function drupal_group_css($css) {
* *
* @see drupal_group_css() * @see drupal_group_css()
* @see drupal_pre_render_styles() * @see drupal_pre_render_styles()
* @see system_element_info()
*/ */
function drupal_aggregate_css(&$css_groups) { function drupal_aggregate_css(&$css_groups) {
$preprocess_css = (variable_get('preprocess_css', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update')); $preprocess_css = (variable_get('preprocess_css', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update'));
...@@ -3166,6 +3371,12 @@ function drupal_pre_render_styles($elements) { ...@@ -3166,6 +3371,12 @@ function drupal_pre_render_styles($elements) {
// URL changed. // URL changed.
$query_string = variable_get('css_js_query_string', '0'); $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<!--/*--><![CDATA[/*><!--*/\n";
$embed_suffix = "\n/*]]>*/-->\n";
// Defaults for LINK and STYLE elements. // Defaults for LINK and STYLE elements.
$link_element_defaults = array( $link_element_defaults = array(
'#type' => 'html_tag', '#type' => 'html_tag',
...@@ -3273,6 +3484,8 @@ function drupal_pre_render_styles($elements) { ...@@ -3273,6 +3484,8 @@ function drupal_pre_render_styles($elements) {
if (isset($group['data'])) { if (isset($group['data'])) {
$element = $style_element_defaults; $element = $style_element_defaults;
$element['#value'] = $group['data']; $element['#value'] = $group['data'];
$element['#value_prefix'] = $embed_prefix;
$element['#value_suffix'] = $embed_suffix;
$element['#attributes']['media'] = $group['media']; $element['#attributes']['media'] = $group['media'];
$element['#browsers'] = $group['browsers']; $element['#browsers'] = $group['browsers'];
$elements[] = $element; $elements[] = $element;
...@@ -3281,6 +3494,8 @@ function drupal_pre_render_styles($elements) { ...@@ -3281,6 +3494,8 @@ function drupal_pre_render_styles($elements) {
foreach ($group['items'] as $item) { foreach ($group['items'] as $item) {
$element = $style_element_defaults; $element = $style_element_defaults;
$element['#value'] = $item['data']; $element['#value'] = $item['data'];
$element['#value_prefix'] = $embed_prefix;
$element['#value_suffix'] = $embed_suffix;
$element['#attributes']['media'] = $item['media']; $element['#attributes']['media'] = $item['media'];
$element['#browsers'] = $group['browsers']; $element['#browsers'] = $group['browsers'];
$elements[] = $element; $elements[] = $element;
...@@ -3316,8 +3531,8 @@ function drupal_pre_render_styles($elements) { ...@@ -3316,8 +3531,8 @@ function drupal_pre_render_styles($elements) {
* in $css while the value is the cache file name. The cache file is generated * 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 * 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 * 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 * variable is emptied to force a rebuild of the cache. Second, the cache file
* file is generated if it is missing on disk. Old cache files are not deleted * 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 * 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 * period by drupal_delete_file_if_stale(). This ensures that files referenced
* by a cached page will still be available. * by a cached page will still be available.
...@@ -3332,7 +3547,13 @@ function drupal_build_css_cache($css) { ...@@ -3332,7 +3547,13 @@ function drupal_build_css_cache($css) {
$data = ''; $data = '';
$uri = ''; $uri = '';
$map = variable_get('drupal_css_cache_files', array()); $map = variable_get('drupal_css_cache_files', array());
$key = hash('sha256', serialize($css)); // Create a new array so that only the file names are used to create the hash.
// This prevents new aggregates from being created unnecessarily.
$css_data = array();
foreach ($css as $css_file) {
$css_data[] = $css_file['data'];
}
$key = hash('sha256', serialize($css_data));
if (isset($map[$key])) { if (isset($map[$key])) {
$uri = $map[$key]; $uri = $map[$key];
} }
...@@ -3343,10 +3564,19 @@ function drupal_build_css_cache($css) { ...@@ -3343,10 +3564,19 @@ function drupal_build_css_cache($css) {
// Only 'file' stylesheets can be aggregated. // Only 'file' stylesheets can be aggregated.
if ($stylesheet['type'] == 'file') { if ($stylesheet['type'] == 'file') {
$contents = drupal_load_stylesheet($stylesheet['data'], TRUE); $contents = drupal_load_stylesheet($stylesheet['data'], TRUE);
// Return the path to where this CSS file originated from.
$base = base_path() . dirname($stylesheet['data']) . '/'; // Build the base URL of this CSS file: start with the full URL.
_drupal_build_css_path(NULL, $base); $css_base_url = file_create_url($stylesheet['data']);
// Prefix all paths within this CSS file, ignoring external and absolute paths. // 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); $data .= preg_replace_callback('/url\(\s*[\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\s*\)/i', '_drupal_build_css_path', $contents);
} }
} }
...@@ -3386,9 +3616,7 @@ function drupal_build_css_cache($css) { ...@@ -3386,9 +3616,7 @@ function drupal_build_css_cache($css) {
} }
/** /**
* Helper function for drupal_build_css_cache(). * Prefixes all paths within a CSS file for drupal_build_css_cache().
*
* This function will prefix all paths within a CSS file.
*/ */
function _drupal_build_css_path($matches, $base = NULL) { function _drupal_build_css_path($matches, $base = NULL) {
$_base = &drupal_static(__FUNCTION__); $_base = &drupal_static(__FUNCTION__);
...@@ -3422,44 +3650,51 @@ function _drupal_build_css_path($matches, $base = NULL) { ...@@ -3422,44 +3650,51 @@ function _drupal_build_css_path($matches, $base = NULL) {
* Name of the stylesheet to be processed. * Name of the stylesheet to be processed.
* @param $optimize * @param $optimize
* Defines if CSS contents should be compressed or not. * Defines if CSS contents should be compressed or not.
* @param $reset_basepath
* Used internally to facilitate recursive resolution of @import commands.
*
* @return * @return
* Contents of the stylesheet, including any resolved @import commands. * Contents of the stylesheet, including any resolved @import commands.
*/ */
function drupal_load_stylesheet($file, $optimize = NULL) { function drupal_load_stylesheet($file, $optimize = NULL, $reset_basepath = TRUE) {
// $_optimize does not use drupal_static as it is set by $optimize. // These statics are not cache variables, so we don't use drupal_static().
static $_optimize; static $_optimize, $basepath;
// Store optimization parameter for preg_replace_callback with nested @import loops. if ($reset_basepath) {
$basepath = '';
}
// Store the value of $optimize for preg_replace_callback with nested
// @import loops.
if (isset($optimize)) { if (isset($optimize)) {
$_optimize = $optimize; $_optimize = $optimize;
} }
$contents = ''; // Stylesheets are relative one to each other. Start by adding a base path
if (file_exists($file)) { // prefix provided by the parent stylesheet (if necessary).
// Load the local CSS stylesheet. if ($basepath && !file_uri_scheme($file)) {
$contents = file_get_contents($file); $file = $basepath . '/' . $file;
}
// Change to the current stylesheet's directory. $basepath = dirname($file);
$cwd = getcwd();
chdir(dirname($file));
// Process the stylesheet.
$contents = drupal_load_stylesheet_content($contents, $_optimize);
// Change back directory. // Load the CSS stylesheet. We suppress errors because themes may specify
chdir($cwd); // 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 $contents; return '';
} }
/** /**
* Process the contents of a stylesheet for aggregation. * Processes the contents of a stylesheet for aggregation.
* *
* @param $contents * @param $contents
* The contents of the stylesheet. * The contents of the stylesheet.
* @param $optimize * @param $optimize
* (optional) Boolean whether CSS contents should be minified. Defaults to * (optional) Boolean whether CSS contents should be minified. Defaults to
* FALSE. * FALSE.
*
* @return * @return
* Contents of the stylesheet including the imported stylesheets. * Contents of the stylesheet including the imported stylesheets.
*/ */
...@@ -3483,12 +3718,9 @@ function drupal_load_stylesheet_content($contents, $optimize = FALSE) { ...@@ -3483,12 +3718,9 @@ function drupal_load_stylesheet_content($contents, $optimize = FALSE) {
); );
// Remove certain whitespace. // Remove certain whitespace.
// There are different conditions for removing leading and trailing // There are different conditions for removing leading and trailing
// whitespace. To be able to use a single backreference in the replacement // whitespace.
// string, the outer pattern uses the ?| modifier, which makes all contained
// subpatterns appear in \1.
// @see http://php.net/manual/en/regexp.reference.subpatterns.php // @see http://php.net/manual/en/regexp.reference.subpatterns.php
$contents = preg_replace('< $contents = preg_replace('<
(?|
# Strip leading and trailing whitespace. # Strip leading and trailing whitespace.
\s*([@{};,])\s* \s*([@{};,])\s*
# Strip only leading whitespace from: # Strip only leading whitespace from:
...@@ -3498,12 +3730,15 @@ function drupal_load_stylesheet_content($contents, $optimize = FALSE) { ...@@ -3498,12 +3730,15 @@ function drupal_load_stylesheet_content($contents, $optimize = FALSE) {
# - Opening parenthesis: Retain "@media (bar) and foo". # - Opening parenthesis: Retain "@media (bar) and foo".
# - Colon: Retain :pseudo-selectors. # - Colon: Retain :pseudo-selectors.
| ([\(:])\s+ | ([\(:])\s+
)
>xS', >xS',
'\1', // 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 $contents
); );
// End the file with a new line. // End the file with a new line.
$contents = trim($contents);
$contents .= "\n"; $contents .= "\n";
} }
...@@ -3522,7 +3757,7 @@ function drupal_load_stylesheet_content($contents, $optimize = FALSE) { ...@@ -3522,7 +3757,7 @@ function drupal_load_stylesheet_content($contents, $optimize = FALSE) {
function _drupal_load_stylesheet($matches) { function _drupal_load_stylesheet($matches) {
$filename = $matches[1]; $filename = $matches[1];
// Load the imported stylesheet and replace @import commands in there as well. // Load the imported stylesheet and replace @import commands in there as well.
$file = drupal_load_stylesheet($filename); $file = drupal_load_stylesheet($filename, NULL, FALSE);
// Determine the file's directory. // Determine the file's directory.
$directory = dirname($filename); $directory = dirname($filename);
...@@ -3555,7 +3790,7 @@ function drupal_delete_file_if_stale($uri) { ...@@ -3555,7 +3790,7 @@ function drupal_delete_file_if_stale($uri) {
} }
/** /**
* Prepare a string for use as a valid CSS identifier (element, class or ID name). * Prepares a string for use as a CSS identifier (element, class, or ID name).
* *
* http://www.w3.org/TR/CSS21/syndata.html#characters shows the syntax for valid * http://www.w3.org/TR/CSS21/syndata.html#characters shows the syntax for valid
* CSS identifiers (including element names, classes, and IDs in selectors.) * CSS identifiers (including element names, classes, and IDs in selectors.)
...@@ -3564,6 +3799,7 @@ function drupal_delete_file_if_stale($uri) { ...@@ -3564,6 +3799,7 @@ function drupal_delete_file_if_stale($uri) {
* The identifier to clean. * The identifier to clean.
* @param $filter * @param $filter
* An array of string replacements to use on the identifier. * An array of string replacements to use on the identifier.
*
* @return * @return
* The cleaned identifier. * The cleaned identifier.
*/ */
...@@ -3585,13 +3821,14 @@ function drupal_clean_css_identifier($identifier, $filter = array(' ' => '-', '_ ...@@ -3585,13 +3821,14 @@ function drupal_clean_css_identifier($identifier, $filter = array(' ' => '-', '_
} }
/** /**
* Prepare a string for use as a valid class name. * Prepares a string for use as a valid class name.
* *
* Do not pass one string containing multiple classes as they will be * Do not pass one string containing multiple classes as they will be
* incorrectly concatenated with dashes, i.e. "one two" will become "one-two". * incorrectly concatenated with dashes, i.e. "one two" will become "one-two".
* *
* @param $class * @param $class
* The class name to clean. * The class name to clean.
*
* @return * @return
* The cleaned class name. * The cleaned class name.
*/ */
...@@ -3600,24 +3837,24 @@ function drupal_html_class($class) { ...@@ -3600,24 +3837,24 @@ function drupal_html_class($class) {
} }
/** /**
* Prepare a string for use as a valid HTML ID and guarantee uniqueness. * Prepares a string for use as a valid HTML ID and guarantees uniqueness.
* *
* This function ensures that each passed HTML ID value only exists once on the * 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, * page. By tracking the already returned ids, this function enables forms,
* blocks, and other content to be output multiple times on the same page, * blocks, and other content to be output multiple times on the same page,
* without breaking (X)HTML validation. * without breaking (X)HTML validation.
* *
* For already existing ids, a counter is appended to the id string. Therefore, * 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 * 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 * this function and instead should rely on manually added CSS classes or
* similarly reliable constructs. * similarly reliable constructs.
* *
* Two consecutive hyphens separate the counter from the original id. To manage * Two consecutive hyphens separate the counter from the original ID. To manage
* uniqueness across multiple AJAX requests on the same page, AJAX requests * 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 * POST an array of all IDs currently present on the page, which are used to
* prime this function's cache upon first invocation. * prime this function's cache upon first invocation.
* *
* To allow reverse-parsing of ids submitted via AJAX, any multiple consecutive * To allow reverse-parsing of IDs submitted via Ajax, any multiple consecutive
* hyphens in the originally passed $id are replaced with a single hyphen. * hyphens in the originally passed $id are replaced with a single hyphen.
* *
* @param $id * @param $id
...@@ -3627,10 +3864,10 @@ function drupal_html_class($class) { ...@@ -3627,10 +3864,10 @@ function drupal_html_class($class) {
* The cleaned ID. * The cleaned ID.
*/ */
function drupal_html_id($id) { function drupal_html_id($id) {
// If this is an AJAX request, then content returned by this page request will // 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 // 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 // unique for the fully merged content. Therefore, initialize $seen_ids to
// take into account ids that are already in use on the base page. // take into account IDs that are already in use on the base page.
$seen_ids_init = &drupal_static(__FUNCTION__ . ':init'); $seen_ids_init = &drupal_static(__FUNCTION__ . ':init');
if (!isset($seen_ids_init)) { if (!isset($seen_ids_init)) {
// Ideally, Drupal would provide an API to persist state information about // Ideally, Drupal would provide an API to persist state information about
...@@ -3638,7 +3875,7 @@ function drupal_html_id($id) { ...@@ -3638,7 +3875,7 @@ function drupal_html_id($id) {
// function's $seen_ids static variable to that state information in order // function's $seen_ids static variable to that state information in order
// to have it properly initialized for this page request. However, no such // 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 // 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 // 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 // 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 // the raw POST data is cast to a number before being returned by this
// function, this usage is safe. // function, this usage is safe.
...@@ -3651,7 +3888,16 @@ function drupal_html_id($id) { ...@@ -3651,7 +3888,16 @@ function drupal_html_id($id) {
// requested id. $_POST['ajax_html_ids'] contains the ids as they were // requested id. $_POST['ajax_html_ids'] contains the ids as they were
// returned by this function, potentially with the appended counter, so // returned by this function, potentially with the appended counter, so
// we parse that to reconstruct the $seen_ids array. // we parse that to reconstruct the $seen_ids array.
foreach ($_POST['ajax_html_ids'] as $seen_id) { if (is_array($_POST['ajax_html_ids'])) {
$ajax_html_ids = $_POST['ajax_html_ids'];
}
else {
// jquery.form.js may send the server a comma-separated string instead
// of an array (see http://drupal.org/node/1575060), so we need to
// convert it to an array in that case.
$ajax_html_ids = explode(',', $_POST['ajax_html_ids']);
}
foreach ($ajax_html_ids as $seen_id) {
// We rely on '--' being used solely for separating a base id from the // We rely on '--' being used solely for separating a base id from the
// counter, which this function ensures when returning an id. // counter, which this function ensures when returning an id.
$parts = explode('--', $seen_id, 2); $parts = explode('--', $seen_id, 2);
...@@ -3685,7 +3931,7 @@ function drupal_html_id($id) { ...@@ -3685,7 +3931,7 @@ function drupal_html_id($id) {
// The counter needs to be appended with a delimiter that does not exist in // 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 // 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 // return unique IDs and also helps us re-create the $seen_ids array during
// AJAX requests. // Ajax requests.
if (isset($seen_ids[$id])) { if (isset($seen_ids[$id])) {
$id = $id . '--' . ++$seen_ids[$id]; $id = $id . '--' . ++$seen_ids[$id];
} }
...@@ -3703,7 +3949,7 @@ function drupal_html_id($id) { ...@@ -3703,7 +3949,7 @@ function drupal_html_id($id) {
* page region that is output by the theme (Drupal core already handles this in * page region that is output by the theme (Drupal core already handles this in
* the standard template preprocess implementation). Standardizing the class * the standard template preprocess implementation). Standardizing the class
* names in this way allows modules to implement certain features, such as * names in this way allows modules to implement certain features, such as
* drag-and-drop or dynamic AJAX loading, in a theme-independent way. * drag-and-drop or dynamic Ajax loading, in a theme-independent way.
* *
* @param $region * @param $region
* The name of the page region (for example, 'page_top' or 'content'). * The name of the page region (for example, 'page_top' or 'content').
...@@ -3751,6 +3997,7 @@ function drupal_region_class($region) { ...@@ -3751,6 +3997,7 @@ function drupal_region_class($region) {
* array('type' => 'inline', 'scope' => 'footer', 'weight' => 5) * array('type' => 'inline', 'scope' => 'footer', 'weight' => 5)
* ); * );
* drupal_add_js('http://example.com/example.js', 'external'); * drupal_add_js('http://example.com/example.js', 'external');
* drupal_add_js(array('myModule' => array('key' => 'value')), 'setting');
* @endcode * @endcode
* *
* Calling drupal_static_reset('drupal_add_js') will clear all JavaScript added * Calling drupal_static_reset('drupal_add_js') will clear all JavaScript added
...@@ -3770,22 +4017,25 @@ function drupal_region_class($region) { ...@@ -3770,22 +4017,25 @@ function drupal_region_class($region) {
* all typical visitors and most pages of a site. It is critical that all * 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 * preprocessed files are added unconditionally on every page, even if the
* files are not needed on a page. This is normally done by calling * files are not needed on a page. This is normally done by calling
* drupal_add_css() in a hook_init() implementation. * drupal_add_js() in a hook_init() implementation.
* *
* Non-preprocessed files should only be added to the page when they are * Non-preprocessed files should only be added to the page when they are
* actually needed. * actually needed.
* *
* @param $data * @param $data
* (optional) If given, the value depends on the $options parameter: * (optional) If given, the value depends on the $options parameter, or
* $options['type'] if $options is passed as an associative array:
* - 'file': Path to the file relative to base_path(). * - 'file': Path to the file relative to base_path().
* - 'inline': The JavaScript code that should be placed in the given scope. * - 'inline': The JavaScript code that should be placed in the given scope.
* - 'external': The absolute path to an external JavaScript file that is not * - 'external': The absolute path to an external JavaScript file that is not
* hosted on the local server. These files will not be aggregated if * hosted on the local server. These files will not be aggregated if
* JavaScript aggregation is enabled. * JavaScript aggregation is enabled.
* - 'setting': An associative array with configuration options. The array is * - 'setting': An associative array with configuration options. The array is
* directly placed in Drupal.settings. All modules should wrap their actual * merged directly into Drupal.settings. All modules should wrap their
* configuration settings in another variable to prevent conflicts in the * actual configuration settings in another variable to prevent conflicts in
* Drupal.settings namespace. * 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 * @param $options
* (optional) A string defining the type of JavaScript that is being added in * (optional) A string defining the type of JavaScript that is being added in
* the $data parameter ('file'/'setting'/'inline'/'external'), or an * the $data parameter ('file'/'setting'/'inline'/'external'), or an
...@@ -3885,12 +4135,17 @@ function drupal_add_js($data = NULL, $options = NULL) { ...@@ -3885,12 +4135,17 @@ function drupal_add_js($data = NULL, $options = NULL) {
if (isset($data)) { if (isset($data)) {
// Add jquery.js and drupal.js, as well as the basePath setting, the // Add jquery.js and drupal.js, as well as the basePath setting, the
// first time a Javascript file is added. // first time a JavaScript file is added.
if (empty($javascript)) { 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( $javascript = array(
'settings' => array( 'settings' => array(
'data' => array( 'data' => array(
array('basePath' => base_path()), array('basePath' => base_path()),
array('pathPrefix' => empty($prefix) ? '' : $prefix),
), ),
'type' => 'setting', 'type' => 'setting',
'scope' => 'header', 'scope' => 'header',
...@@ -3912,7 +4167,7 @@ function drupal_add_js($data = NULL, $options = NULL) { ...@@ -3912,7 +4167,7 @@ function drupal_add_js($data = NULL, $options = NULL) {
); );
// Register all required libraries. // Register all required libraries.
drupal_add_library('system', 'jquery', TRUE); drupal_add_library('system', 'jquery', TRUE);
drupal_add_library('system', 'once', TRUE); drupal_add_library('system', 'jquery.once', TRUE);
} }
switch ($options['type']) { switch ($options['type']) {
...@@ -3928,7 +4183,7 @@ function drupal_add_js($data = NULL, $options = NULL) { ...@@ -3928,7 +4183,7 @@ function drupal_add_js($data = NULL, $options = NULL) {
default: // 'file' and 'external' default: // 'file' and 'external'
// Local and external files must keep their name as the associative key // Local and external files must keep their name as the associative key
// so the same JavaScript file is not be added twice. // so the same JavaScript file is not added twice.
$javascript[$options['data']] = $options; $javascript[$options['data']] = $options;
} }
} }
...@@ -3940,6 +4195,7 @@ function drupal_add_js($data = NULL, $options = NULL) { ...@@ -3940,6 +4195,7 @@ function drupal_add_js($data = NULL, $options = NULL) {
* *
* @param $data * @param $data
* (optional) The default data parameter for the JavaScript item array. * (optional) The default data parameter for the JavaScript item array.
*
* @see drupal_get_js() * @see drupal_get_js()
* @see drupal_add_js() * @see drupal_add_js()
*/ */
...@@ -3983,8 +4239,10 @@ function drupal_js_defaults($data = NULL) { ...@@ -3983,8 +4239,10 @@ function drupal_js_defaults($data = NULL) {
* (optional) If set to TRUE, this function skips calling drupal_alter() on * (optional) If set to TRUE, this function skips calling drupal_alter() on
* $javascript, useful when the calling function passes a $javascript array * $javascript, useful when the calling function passes a $javascript array
* that has already been altered. * that has already been altered.
*
* @return * @return
* All JavaScript code segments and includes for the scope as HTML tags. * All JavaScript code segments and includes for the scope as HTML tags.
*
* @see drupal_add_js() * @see drupal_add_js()
* @see locale_js_alter() * @see locale_js_alter()
* @see drupal_js_defaults() * @see drupal_js_defaults()
...@@ -4026,13 +4284,13 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS ...@@ -4026,13 +4284,13 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
// page request. // page request.
$default_query_string = variable_get('css_js_query_string', '0'); $default_query_string = variable_get('css_js_query_string', '0');
// For inline Javascript to validate as XHTML, all Javascript containing // For inline JavaScript to validate as XHTML, all JavaScript containing
// XHTML needs to be wrapped in CDATA. To make that backwards compatible // XHTML needs to be wrapped in CDATA. To make that backwards compatible
// with HTML 4, we need to comment out the CDATA-tag. // with HTML 4, we need to comment out the CDATA-tag.
$embed_prefix = "\n<!--//--><![CDATA[//><!--\n"; $embed_prefix = "\n<!--//--><![CDATA[//><!--\n";
$embed_suffix = "\n//--><!]]>\n"; $embed_suffix = "\n//--><!]]>\n";
// Since Javascript may look for arguments in the url and act on them, some // 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. // third-party code might require the use of a different query string.
$js_version_string = variable_get('drupal_js_version_query_string', 'v='); $js_version_string = variable_get('drupal_js_version_query_string', 'v=');
...@@ -4070,7 +4328,7 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS ...@@ -4070,7 +4328,7 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
case 'setting': case 'setting':
$js_element = $element; $js_element = $element;
$js_element['#value_prefix'] = $embed_prefix; $js_element['#value_prefix'] = $embed_prefix;
$js_element['#value'] = 'jQuery.extend(Drupal.settings, ' . drupal_json_encode(call_user_func_array('array_merge_recursive', $item['data'])) . ");"; $js_element['#value'] = 'jQuery.extend(Drupal.settings, ' . drupal_json_encode(drupal_array_merge_deep_array($item['data'])) . ");";
$js_element['#value_suffix'] = $embed_suffix; $js_element['#value_suffix'] = $embed_suffix;
$output .= theme('html_tag', array('element' => $js_element)); $output .= theme('html_tag', array('element' => $js_element));
break; break;
...@@ -4143,12 +4401,12 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS ...@@ -4143,12 +4401,12 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
} }
/** /**
* Add to the page all structures attached to a render() structure. * Adds attachments to a render() structure.
* *
* Libraries, JavaScript, CSS and other types of custom structures are attached * Libraries, JavaScript, CSS and other types of custom structures are attached
* to elements using the #attached property. The #attached property contains an * to elements using the #attached property. The #attached property is an
* associative array, where the keys are the the types of the structure, and * associative array, where the keys are the the attachment types and the values
* the value the attached data. For example: * are the attached data. For example:
* @code * @code
* $build['#attached'] = array( * $build['#attached'] = array(
* 'js' => array(drupal_get_path('module', 'taxonomy') . '/taxonomy.js'), * 'js' => array(drupal_get_path('module', 'taxonomy') . '/taxonomy.js'),
...@@ -4159,13 +4417,21 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS ...@@ -4159,13 +4417,21 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
* 'js', 'css', and 'library' are types that get special handling. For any * '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 * 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: * callback function and each value an array of arguments. For example:
*
* @code * @code
* $build['#attached']['drupal_add_http_header'] = array( * $build['#attached']['drupal_add_http_header'] = array(
* array('Content-Type', 'application/rss+xml; charset=utf-8'), * array('Content-Type', 'application/rss+xml; charset=utf-8'),
* ); * );
* @endcode * @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 * @param $elements
* The structured array describing the data being rendered. * The structured array describing the data being rendered.
* @param $group * @param $group
...@@ -4174,12 +4440,16 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS ...@@ -4174,12 +4440,16 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
* assigned to them. * assigned to them.
* @param $dependency_check * @param $dependency_check
* When TRUE, will exit if a given library's dependencies are missing. When * 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 of the * set to FALSE, will continue to add the libraries, even though one or more
* dependencies are missing. Defaults to FALSE. * 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 * @return
* Will return FALSE if there were any missing library dependencies. TRUE will * FALSE if there were any missing library dependencies; TRUE if all library
* be returned if all library dependencies were met. * dependencies were met.
* *
* @see drupal_add_library() * @see drupal_add_library()
* @see drupal_add_js() * @see drupal_add_js()
...@@ -4238,7 +4508,7 @@ function drupal_process_attached($elements, $group = JS_DEFAULT, $dependency_che ...@@ -4238,7 +4508,7 @@ function drupal_process_attached($elements, $group = JS_DEFAULT, $dependency_che
} }
// Add additional types of attachments specified in the render() structure. // Add additional types of attachments specified in the render() structure.
// Libraries, Javascript and CSS have been added already, as they require // Libraries, JavaScript and CSS have been added already, as they require
// special handling. // special handling.
foreach ($elements['#attached'] as $callback => $options) { foreach ($elements['#attached'] as $callback => $options) {
if (function_exists($callback)) { if (function_exists($callback)) {
...@@ -4329,6 +4599,8 @@ function drupal_process_attached($elements, $group = JS_DEFAULT, $dependency_che ...@@ -4329,6 +4599,8 @@ function drupal_process_attached($elements, $group = JS_DEFAULT, $dependency_che
* The following states may be applied to an element: * The following states may be applied to an element:
* - enabled * - enabled
* - disabled * - disabled
* - required
* - optional
* - visible * - visible
* - invisible * - invisible
* - checked * - checked
...@@ -4337,26 +4609,22 @@ function drupal_process_attached($elements, $group = JS_DEFAULT, $dependency_che ...@@ -4337,26 +4609,22 @@ function drupal_process_attached($elements, $group = JS_DEFAULT, $dependency_che
* - collapsed * - collapsed
* *
* The following states may be used in remote conditions: * The following states may be used in remote conditions:
* - enabled * - empty
* - disabled * - filled
* - visible
* - invisible
* - checked * - checked
* - unchecked * - unchecked
* - expanded
* - collapsed
* - value * - value
* *
* The following states exist for both states and remote conditions, but are not * The following states exist for both elements and remote conditions, but are
* fully implemented and may not change anything on the element: * not fully implemented and may not change anything on the element:
* - required
* - optional
* - relevant * - relevant
* - irrelevant * - irrelevant
* - valid * - valid
* - invalid * - invalid
* - touched * - touched
* - untouched * - untouched
* - filled
* - empty
* - readwrite * - readwrite
* - readonly * - readonly
* *
...@@ -4377,7 +4645,7 @@ function drupal_process_attached($elements, $group = JS_DEFAULT, $dependency_che ...@@ -4377,7 +4645,7 @@ function drupal_process_attached($elements, $group = JS_DEFAULT, $dependency_che
* @see form_example_states_form() * @see form_example_states_form()
*/ */
function drupal_process_states(&$elements) { function drupal_process_states(&$elements) {
$elements['#attached']['js']['misc/states.js'] = array('group' => JS_LIBRARY, 'weight' => 1); $elements['#attached']['library'][] = array('system', 'drupal.states');
$elements['#attached']['js'][] = array( $elements['#attached']['js'][] = array(
'type' => 'setting', 'type' => 'setting',
'data' => array('states' => array('#' . $elements['#id'] => $elements['#states'])), 'data' => array('states' => array('#' . $elements['#id'] => $elements['#states'])),
...@@ -4391,16 +4659,20 @@ function drupal_process_states(&$elements) { ...@@ -4391,16 +4659,20 @@ function drupal_process_states(&$elements) {
* settings, and optionally requiring another library. For example, a library * settings, and optionally requiring another library. For example, a library
* can be a jQuery plugin, a JavaScript framework, or a CSS framework. This * 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 * 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. * depending module, without having to add all files of the library separately.
* Each library is only loaded once. * Each library is only loaded once.
* *
* @param $module * @param $module
* The name of the module that registered the library. * The name of the module that registered the library.
* @param $name * @param $name
* The name of the library to add. * 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 * @return
* TRUE when the library was successfully added or FALSE if the library or one * TRUE if the library was successfully added; FALSE if the library or one of
* of its dependencies could not be added. * its dependencies could not be added.
* *
* @see drupal_get_library() * @see drupal_get_library()
* @see hook_library() * @see hook_library()
...@@ -4444,10 +4716,14 @@ function drupal_add_library($module, $name, $every_page = NULL) { ...@@ -4444,10 +4716,14 @@ function drupal_add_library($module, $name, $every_page = NULL) {
* *
* @param $module * @param $module
* The name of a module that registered a library. * The name of a module that registered a library.
* @param $library * @param $name
* The name of a registered library. * (optional) The name of a registered library to retrieve. By default, all
* libraries registered by $module are returned.
*
* @return * @return
* The definition of the requested library, if existent, or FALSE. * 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 drupal_add_library()
* @see hook_library() * @see hook_library()
...@@ -4456,7 +4732,7 @@ function drupal_add_library($module, $name, $every_page = NULL) { ...@@ -4456,7 +4732,7 @@ function drupal_add_library($module, $name, $every_page = NULL) {
* @todo The purpose of drupal_get_*() is completely different to other page * @todo The purpose of drupal_get_*() is completely different to other page
* requisite API functions; find and use a different name. * requisite API functions; find and use a different name.
*/ */
function drupal_get_library($module, $name) { function drupal_get_library($module, $name = NULL) {
$libraries = &drupal_static(__FUNCTION__, array()); $libraries = &drupal_static(__FUNCTION__, array());
if (!isset($libraries[$module])) { if (!isset($libraries[$module])) {
...@@ -4479,24 +4755,26 @@ function drupal_get_library($module, $name) { ...@@ -4479,24 +4755,26 @@ function drupal_get_library($module, $name) {
} }
$libraries[$module] = $module_libraries; $libraries[$module] = $module_libraries;
} }
if (empty($libraries[$module][$name])) { if (isset($name)) {
if (!isset($libraries[$module][$name])) {
$libraries[$module][$name] = FALSE; $libraries[$module][$name] = FALSE;
} }
return $libraries[$module][$name]; return $libraries[$module][$name];
} }
return $libraries[$module];
}
/** /**
* Assist in adding the tableDrag JavaScript behavior to a themed table. * Assists in adding the tableDrag JavaScript behavior to a themed table.
* *
* Draggable tables should be used wherever an outline or list of sortable items * 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 * 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. * 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 * To set up a table to use drag and drop in place of weight select-lists or in
* in place of a form that contains parent relationships, the form must be * place of a form that contains parent relationships, the form must be themed
* themed into a table. The table must have an id attribute set. If using * into a table. The table must have an ID attribute set. If using
* theme_table(), the id may be set as such: * theme_table(), the ID may be set as follows:
* @code * @code
* $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'my-module-table'))); * $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'my-module-table')));
* return $output; * return $output;
...@@ -4511,8 +4789,8 @@ function drupal_get_library($module, $name) { ...@@ -4511,8 +4789,8 @@ function drupal_get_library($module, $name) {
* $form['my_elements'][$delta]['weight']['#attributes']['class'] = array('my-elements-weight'); * $form['my_elements'][$delta]['weight']['#attributes']['class'] = array('my-elements-weight');
* @endcode * @endcode
* *
* Each row of the table must also have a class of "draggable" in order to enable the * Each row of the table must also have a class of "draggable" in order to
* drag handles: * enable the drag handles:
* @code * @code
* $row = array(...); * $row = array(...);
* $rows[] = array( * $rows[] = array(
...@@ -4532,8 +4810,8 @@ function drupal_get_library($module, $name) { ...@@ -4532,8 +4810,8 @@ function drupal_get_library($module, $name) {
* @endcode * @endcode
* *
* In a more complex case where there are several groups in one column (such as * 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 * the block regions on the admin/structure/block page), a separate subgroup
* must also be added to differentiate the groups. * class must also be added to differentiate the groups.
* @code * @code
* $form['my_elements'][$region][$delta]['weight']['#attributes']['class'] = array('my-elements-weight', 'my-elements-weight-' . $region); * $form['my_elements'][$region][$delta]['weight']['#attributes']['class'] = array('my-elements-weight', 'my-elements-weight-' . $region);
* @endcode * @endcode
...@@ -4550,14 +4828,14 @@ function drupal_get_library($module, $name) { ...@@ -4550,14 +4828,14 @@ function drupal_get_library($module, $name) {
* *
* In a situation where tree relationships are present, adding multiple * In a situation where tree relationships are present, adding multiple
* subgroups is not necessary, because the table will contain indentations that * subgroups is not necessary, because the table will contain indentations that
* provide enough information about the sibling and parent relationships. * provide enough information about the sibling and parent relationships. See
* See theme_menu_overview_form() for an example creating a table containing * theme_menu_overview_form() for an example creating a table containing parent
* parent relationships. * relationships.
* *
* Please note that this function should be called from the theme layer, such as * Note that this function should be called from the theme layer, such as in a
* in a .tpl.php file, theme_ function, or in a template_preprocess function, * .tpl.php file, theme_ function, or in a template_preprocess function, not in
* not in a form declaration. Though the same JavaScript could be added to the * a form declaration. Though the same JavaScript could be added to the page
* page using drupal_add_js() directly, this function helps keep template files * using drupal_add_js() directly, this function helps keep template files
* clean and readable. It also prevents tabledrag.js from being added twice * clean and readable. It also prevents tabledrag.js from being added twice
* accidentally. * accidentally.
* *
...@@ -4599,7 +4877,7 @@ function drupal_add_tabledrag($table_id, $action, $relationship, $group, $subgro ...@@ -4599,7 +4877,7 @@ function drupal_add_tabledrag($table_id, $action, $relationship, $group, $subgro
// Add the table drag JavaScript to the page before the module JavaScript // Add the table drag JavaScript to the page before the module JavaScript
// to ensure that table drag behaviors are registered before any module // to ensure that table drag behaviors are registered before any module
// uses it. // uses it.
drupal_add_js('misc/jquery.cookie.js', array('weight' => -2)); drupal_add_library('system', 'jquery.cookie');
drupal_add_js('misc/tabledrag.js', array('weight' => -1)); drupal_add_js('misc/tabledrag.js', array('weight' => -1));
$js_added = TRUE; $js_added = TRUE;
} }
...@@ -4630,8 +4908,8 @@ function drupal_add_tabledrag($table_id, $action, $relationship, $group, $subgro ...@@ -4630,8 +4908,8 @@ function drupal_add_tabledrag($table_id, $action, $relationship, $group, $subgro
* $files while the value is the cache file name. The cache file is generated * $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 * 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 * 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 * variable is emptied to force a rebuild of the cache. Second, the cache file
* file is generated if it is missing on disk. Old cache files are not deleted * 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 * 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 * period by drupal_delete_file_if_stale(). This ensures that files referenced
* by a cached page will still be available. * by a cached page will still be available.
...@@ -4646,7 +4924,13 @@ function drupal_build_js_cache($files) { ...@@ -4646,7 +4924,13 @@ function drupal_build_js_cache($files) {
$contents = ''; $contents = '';
$uri = ''; $uri = '';
$map = variable_get('drupal_js_cache_files', array()); $map = variable_get('drupal_js_cache_files', array());
$key = hash('sha256', serialize($files)); // Create a new array so that only the file names are used to create the hash.
// This prevents new aggregates from being created unnecessarily.
$js_data = array();
foreach ($files as $file) {
$js_data[] = $file['data'];
}
$key = hash('sha256', serialize($js_data));
if (isset($map[$key])) { if (isset($map[$key])) {
$uri = $map[$key]; $uri = $map[$key];
} }
...@@ -4695,16 +4979,31 @@ function drupal_clear_js_cache() { ...@@ -4695,16 +4979,31 @@ function drupal_clear_js_cache() {
} }
/** /**
* Converts a PHP variable into its Javascript equivalent. * Converts a PHP variable into its JavaScript equivalent.
* *
* We use HTML-safe strings, i.e. with <, > and & escaped. * We use HTML-safe strings, with several characters escaped.
* *
* @see drupal_json_decode() * @see drupal_json_decode()
* @see drupal_json_encode_helper()
* @ingroup php_wrappers * @ingroup php_wrappers
*/ */
function drupal_json_encode($var) { function drupal_json_encode($var) {
// json_encode() does not escape <, > and &, so we do it with str_replace(). // The PHP version cannot change within a request.
return str_replace(array('<', '>', '&'), array('\u003c', '\u003e', '\u0026'), json_encode($var)); static $php530;
if (!isset($php530)) {
$php530 = version_compare(PHP_VERSION, '5.3.0', '>=');
}
if ($php530) {
// Encode <, >, ', &, and " using the json_encode() options parameter.
return json_encode($var, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
}
// json_encode() escapes <, >, ', &, and " using its options parameter, but
// does not support this parameter prior to PHP 5.3.0. Use a helper instead.
include_once DRUPAL_ROOT . '/includes/json-encode.inc';
return drupal_json_encode_helper($var);
} }
/** /**
...@@ -4718,7 +5017,7 @@ function drupal_json_decode($var) { ...@@ -4718,7 +5017,7 @@ function drupal_json_decode($var) {
} }
/** /**
* Return data in JSON format. * Returns data in JSON format.
* *
* This function should be used for JavaScript callback functions returning * This function should be used for JavaScript callback functions returning
* data in JSON format. It sets the header for JavaScript output. * data in JSON format. It sets the header for JavaScript output.
...@@ -4736,7 +5035,7 @@ function drupal_json_output($var = NULL) { ...@@ -4736,7 +5035,7 @@ function drupal_json_output($var = NULL) {
} }
/** /**
* Get a salt useful for hardening against SQL injection. * Gets a salt useful for hardening against SQL injection.
* *
* @return * @return
* A salt based on information in settings.php, not in the database. * A salt based on information in settings.php, not in the database.
...@@ -4749,7 +5048,7 @@ function drupal_get_hash_salt() { ...@@ -4749,7 +5048,7 @@ function drupal_get_hash_salt() {
} }
/** /**
* Ensure the private key variable used to generate tokens is set. * Ensures the private key variable used to generate tokens is set.
* *
* @return * @return
* The private key. * The private key.
...@@ -4763,17 +5062,22 @@ function drupal_get_private_key() { ...@@ -4763,17 +5062,22 @@ function drupal_get_private_key() {
} }
/** /**
* Generate a token based on $value, the current user session and private key. * Generates a token based on $value, the user session, and the private key.
* *
* @param $value * @param $value
* An additional value to base the token on. * An additional value to base the token on.
*
* @return string
* A 43-character URL-safe token for validation, based on the user session ID,
* the global $drupal_hash_salt variable from settings.php, and the
* 'drupal_private_key' configuration variable.
*/ */
function drupal_get_token($value = '') { function drupal_get_token($value = '') {
return drupal_hmac_base64($value, session_id() . drupal_get_private_key() . drupal_get_hash_salt()); 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. * Validates a token based on $value, the user session, and the private key.
* *
* @param $token * @param $token
* The token to be validated. * The token to be validated.
...@@ -4781,6 +5085,7 @@ function drupal_get_token($value = '') { ...@@ -4781,6 +5085,7 @@ function drupal_get_token($value = '') {
* An additional value to base the token on. * An additional value to base the token on.
* @param $skip_anonymous * @param $skip_anonymous
* Set to true to skip token validation for anonymous users. * Set to true to skip token validation for anonymous users.
*
* @return * @return
* True for a valid token, false for an invalid token. When $skip_anonymous * 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. * is true, the return value will always be true for anonymous users.
...@@ -4791,12 +5096,12 @@ function drupal_valid_token($token, $value = '', $skip_anonymous = FALSE) { ...@@ -4791,12 +5096,12 @@ function drupal_valid_token($token, $value = '', $skip_anonymous = FALSE) {
} }
function _drupal_bootstrap_full() { function _drupal_bootstrap_full() {
$called = &drupal_static(__FUNCTION__); static $called = FALSE;
if ($called) { if ($called) {
return; return;
} }
$called = 1; $called = TRUE;
require_once DRUPAL_ROOT . '/' . variable_get('path_inc', 'includes/path.inc'); require_once DRUPAL_ROOT . '/' . variable_get('path_inc', 'includes/path.inc');
require_once DRUPAL_ROOT . '/includes/theme.inc'; require_once DRUPAL_ROOT . '/includes/theme.inc';
require_once DRUPAL_ROOT . '/includes/pager.inc'; require_once DRUPAL_ROOT . '/includes/pager.inc';
...@@ -4849,7 +5154,7 @@ function _drupal_bootstrap_full() { ...@@ -4849,7 +5154,7 @@ function _drupal_bootstrap_full() {
} }
/** /**
* Store the current page in the cache. * Stores the current page in the cache.
* *
* If page_compression is enabled, a gzipped version of the page is stored in * 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 * the cache to avoid compressing the output on each request. The cache entry
...@@ -4901,16 +5206,17 @@ function drupal_page_set_cache() { ...@@ -4901,16 +5206,17 @@ function drupal_page_set_cache() {
/** /**
* Executes a cron run when called. * Executes a cron run when called.
* *
* Do not call this function from test, use $this->cronRun() instead. * Do not call this function from a test. Use $this->cronRun() instead.
* *
* @return * @return
* Returns TRUE if ran successfully * TRUE if cron ran successfully.
*/ */
function drupal_cron_run() { function drupal_cron_run() {
// Allow execution to continue even if the request gets canceled. // Allow execution to continue even if the request gets canceled.
@ignore_user_abort(TRUE); @ignore_user_abort(TRUE);
// Prevent session information from being saved while cron is running. // Prevent session information from being saved while cron is running.
$original_session_saving = drupal_save_session();
drupal_save_session(FALSE); drupal_save_session(FALSE);
// Force the current user to anonymous to ensure consistent permissions on // Force the current user to anonymous to ensure consistent permissions on
...@@ -4937,13 +5243,21 @@ function drupal_cron_run() { ...@@ -4937,13 +5243,21 @@ function drupal_cron_run() {
foreach ($queues as $queue_name => $info) { foreach ($queues as $queue_name => $info) {
DrupalQueue::get($queue_name)->createQueue(); DrupalQueue::get($queue_name)->createQueue();
} }
// Register shutdown callback // Register shutdown callback.
drupal_register_shutdown_function('drupal_cron_cleanup'); drupal_register_shutdown_function('drupal_cron_cleanup');
// Iterate through the modules calling their cron handlers (if any): // Iterate through the modules calling their cron handlers (if any):
module_invoke_all('cron'); foreach (module_implements('cron') as $module) {
// Do not let an exception thrown by one module disturb another.
try {
module_invoke($module, 'cron');
}
catch (Exception $e) {
watchdog_exception('cron', $e);
}
}
// Record cron time // Record cron time.
variable_set('cron_last', REQUEST_TIME); variable_set('cron_last', REQUEST_TIME);
watchdog('cron', 'Cron run completed.', array(), WATCHDOG_NOTICE); watchdog('cron', 'Cron run completed.', array(), WATCHDOG_NOTICE);
...@@ -4965,20 +5279,23 @@ function drupal_cron_run() { ...@@ -4965,20 +5279,23 @@ function drupal_cron_run() {
} }
// Restore the user. // Restore the user.
$GLOBALS['user'] = $original_user; $GLOBALS['user'] = $original_user;
drupal_save_session(TRUE); drupal_save_session($original_session_saving);
return $return; return $return;
} }
/** /**
* Shutdown function for cron cleanup. * Shutdown function: Performs cron cleanup.
*
* @see drupal_cron_run()
* @see drupal_register_shutdown_function()
*/ */
function drupal_cron_cleanup() { function drupal_cron_cleanup() {
// See if the semaphore is still locked. // See if the semaphore is still locked.
if (variable_get('cron_semaphore', FALSE)) { if (variable_get('cron_semaphore', FALSE)) {
watchdog('cron', 'Cron run exceeded the time limit and was aborted.', array(), WATCHDOG_WARNING); watchdog('cron', 'Cron run exceeded the time limit and was aborted.', array(), WATCHDOG_WARNING);
// Release cron semaphore // Release cron semaphore.
variable_del('cron_semaphore'); variable_del('cron_semaphore');
} }
} }
...@@ -4994,7 +5311,7 @@ function drupal_cron_cleanup() { ...@@ -4994,7 +5311,7 @@ function drupal_cron_cleanup() {
* drupal_system_listing("/\.module$/", "modules", 'name', 0); * drupal_system_listing("/\.module$/", "modules", 'name', 0);
* @endcode * @endcode
* this function will search the site-wide modules directory (i.e., /modules/), * this function will search the site-wide modules directory (i.e., /modules/),
* your install profile's directory (i.e., * your installation profile's directory (i.e.,
* /profiles/your_site_profile/modules/), the all-sites 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/all/modules/), and your site-specific directory (i.e.,
* /sites/your_site_dir/modules/), in that order, and return information about * /sites/your_site_dir/modules/), in that order, and return information about
...@@ -5034,8 +5351,6 @@ function drupal_cron_cleanup() { ...@@ -5034,8 +5351,6 @@ function drupal_cron_cleanup() {
function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1) { function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1) {
$config = conf_path(); $config = conf_path();
$profile = drupal_get_profile();
$searchdir = array($directory); $searchdir = array($directory);
$files = array(); $files = array();
...@@ -5043,18 +5358,34 @@ function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1) ...@@ -5043,18 +5358,34 @@ function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1)
// themes as organized by a distribution. It is pristine in the same way // themes as organized by a distribution. It is pristine in the same way
// that /modules is pristine for core; users should avoid changing anything // that /modules is pristine for core; users should avoid changing anything
// there in favor of sites/all or sites/<domain> directories. // there in favor of sites/all or sites/<domain> directories.
$profiles = array();
$profile = drupal_get_profile();
// For SimpleTest to be able to test modules packaged together with a
// distribution we need to include the profile of the parent site (in which
// test runs are triggered).
if (drupal_valid_test_ua()) {
$testing_profile = variable_get('simpletest_parent_profile', FALSE);
if ($testing_profile && $testing_profile != $profile) {
$profiles[] = $testing_profile;
}
}
// In case both profile directories contain the same extension, the actual
// profile always has precedence.
$profiles[] = $profile;
foreach ($profiles as $profile) {
if (file_exists("profiles/$profile/$directory")) { if (file_exists("profiles/$profile/$directory")) {
$searchdir[] = "profiles/$profile/$directory"; $searchdir[] = "profiles/$profile/$directory";
} }
}
// Always search sites/all/* as well as the global directories // Always search sites/all/* as well as the global directories.
$searchdir[] = 'sites/all/' . $directory; $searchdir[] = 'sites/all/' . $directory;
if (file_exists("$config/$directory")) { if (file_exists("$config/$directory")) {
$searchdir[] = "$config/$directory"; $searchdir[] = "$config/$directory";
} }
// Get current list of items // Get current list of items.
if (!function_exists('file_scan_directory')) { if (!function_exists('file_scan_directory')) {
require_once DRUPAL_ROOT . '/includes/file.inc'; require_once DRUPAL_ROOT . '/includes/file.inc';
} }
...@@ -5068,7 +5399,7 @@ function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1) ...@@ -5068,7 +5399,7 @@ function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1)
// compatible with Drupal core. This may occur during upgrades of Drupal // compatible with Drupal core. This may occur during upgrades of Drupal
// core when new modules exist in core while older contrib modules with the // core when new modules exist in core while older contrib modules with the
// same name exist in a directory such as sites/all/modules/. // same name exist in a directory such as sites/all/modules/.
foreach (array_intersect_key($files_to_add, $files) as $key => $file) { 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 // If it has no info file, then we just behave liberally and accept the
// new resource on the list for merging. // new resource on the list for merging.
if (file_exists($info_file = dirname($file->uri) . '/' . $file->name . '.info')) { if (file_exists($info_file = dirname($file->uri) . '/' . $file->name . '.info')) {
...@@ -5079,7 +5410,7 @@ function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1) ...@@ -5079,7 +5410,7 @@ function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1)
// from the array for the current search directory, so it is not // from the array for the current search directory, so it is not
// overwritten when merged with the $files array. // overwritten when merged with the $files array.
if (isset($info['core']) && $info['core'] != DRUPAL_CORE_COMPATIBILITY) { if (isset($info['core']) && $info['core'] != DRUPAL_CORE_COMPATIBILITY) {
unset($files_to_add[$key]); unset($files_to_add[$file_key]);
} }
} }
} }
...@@ -5090,7 +5421,7 @@ function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1) ...@@ -5090,7 +5421,7 @@ function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1)
} }
/** /**
* Set the main page content value for later use. * Sets the main page content value for later use.
* *
* Given the nature of the Drupal page handling, this will be called once with * 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 * a string or array. We store that and return it later as the block is being
...@@ -5098,6 +5429,7 @@ function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1) ...@@ -5098,6 +5429,7 @@ function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1)
* *
* @param $content * @param $content
* A string or renderable array representing the body of the page. * A string or renderable array representing the body of the page.
*
* @return * @return
* If called without $content, a renderable array representing the body of * If called without $content, a renderable array representing the body of
* the page. * the page.
...@@ -5145,7 +5477,7 @@ function drupal_set_page_content($content = NULL) { ...@@ -5145,7 +5477,7 @@ function drupal_set_page_content($content = NULL) {
* browsers, '#browsers' can be set to array('IE' => 'gte IE 8'). * browsers, '#browsers' can be set to array('IE' => 'gte IE 8').
* *
* @return * @return
* The passed in element with markup for conditional comments potentially * The passed-in element with markup for conditional comments potentially
* added to '#prefix' and '#suffix'. * added to '#prefix' and '#suffix'.
*/ */
function drupal_pre_render_conditional_comments($elements) { function drupal_pre_render_conditional_comments($elements) {
...@@ -5207,7 +5539,7 @@ function drupal_pre_render_conditional_comments($elements) { ...@@ -5207,7 +5539,7 @@ function drupal_pre_render_conditional_comments($elements) {
* - #options: (optional) An array of options to pass to l(). * - #options: (optional) An array of options to pass to l().
* *
* @return * @return
* The passed in elements containing a rendered link in '#markup'. * The passed-in elements containing a rendered link in '#markup'.
*/ */
function drupal_pre_render_link($element) { function drupal_pre_render_link($element) {
// By default, link options to pass to l() are normally set in #options. // By default, link options to pass to l() are normally set in #options.
...@@ -5237,7 +5569,7 @@ function drupal_pre_render_link($element) { ...@@ -5237,7 +5569,7 @@ function drupal_pre_render_link($element) {
if (!isset($element['#id'])) { if (!isset($element['#id'])) {
$element['#id'] = $element['#options']['attributes']['id'] = drupal_html_id('ajax-link'); $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 #ajax['path] was not specified, use the href as Ajax request URL.
if (!isset($element['#ajax']['path'])) { if (!isset($element['#ajax']['path'])) {
$element['#ajax']['path'] = $element['#href']; $element['#ajax']['path'] = $element['#href'];
$element['#ajax']['options'] = $element['#options']; $element['#ajax']['options'] = $element['#options'];
...@@ -5249,6 +5581,97 @@ function drupal_pre_render_link($element) { ...@@ -5249,6 +5581,97 @@ function drupal_pre_render_link($element) {
return $element; 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. * #pre_render callback to append contents in #markup to #children.
* *
...@@ -5257,13 +5680,13 @@ function drupal_pre_render_link($element) { ...@@ -5257,13 +5680,13 @@ function drupal_pre_render_link($element) {
* Note that if also a #theme is defined for the element, then the result of * Note that if also a #theme is defined for the element, then the result of
* the theme callback will override #children. * the theme callback will override #children.
* *
* @see drupal_render()
*
* @param $elements * @param $elements
* A structured array using the #markup key. * A structured array using the #markup key.
* *
* @return * @return
* The passed in elements, but #markup appended to #children. * The passed-in elements, but #markup appended to #children.
*
* @see drupal_render()
*/ */
function drupal_pre_render_markup($elements) { function drupal_pre_render_markup($elements) {
$elements['#children'] = $elements['#markup']; $elements['#children'] = $elements['#markup'];
...@@ -5276,8 +5699,10 @@ function drupal_pre_render_markup($elements) { ...@@ -5276,8 +5699,10 @@ function drupal_pre_render_markup($elements) {
* @param $page * @param $page
* A string or array representing the content of a page. The array consists of * A string or array representing the content of a page. The array consists of
* the following keys: * the following keys:
* - #type: Value is always 'page'. This pushes the theming through page.tpl.php (required). * - #type: Value is always 'page'. This pushes the theming through
* - #show_messages: Suppress drupal_get_message() items. Used by Batch API (optional). * page.tpl.php (required).
* - #show_messages: Suppress drupal_get_message() items. Used by Batch
* API (optional).
* *
* @see hook_page_alter() * @see hook_page_alter()
* @see element_info() * @see element_info()
...@@ -5317,15 +5742,25 @@ function drupal_render_page($page) { ...@@ -5317,15 +5742,25 @@ function drupal_render_page($page) {
* *
* Recursively iterates over each of the array elements, generating HTML code. * Recursively iterates over each of the array elements, generating HTML code.
* *
* HTML generation is controlled by two properties containing theme functions, * Renderable arrays have two kinds of key/value pairs: properties and
* #theme and #theme_wrappers. * children. Properties have keys starting with '#' and their values influence
* how the array will be rendered. Children are all elements whose keys do not
* start with a '#'. Their values should be renderable arrays themselves,
* which will be rendered during the rendering of the parent array. The markup
* provided by the children is typically inserted into the markup generated by
* the parent array.
*
* HTML generation for a renderable array, and the treatment of any children,
* 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 * #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 * any children, it is the responsibility of the theme function to render
* allowed to have any children, e.g. buttons or textfields, it can be used to * these children. For elements that are not allowed to have any children,
* render the element itself. If #theme is not present and the element has * e.g. buttons or textfields, the theme function can be used to render the
* children, they are rendered and concatenated into a string by * element itself. If #theme is not present and the element has children, each
* drupal_render_children(). * child is itself rendered by a call to drupal_render(), and the results are
* concatenated.
* *
* The #theme_wrappers property contains an array of theme functions which will * 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 * be called, in order, after #theme has run. These can be used to add further
...@@ -5349,8 +5784,8 @@ function drupal_render_page($page) { ...@@ -5349,8 +5784,8 @@ function drupal_render_page($page) {
* is set, the cache ID is created automatically from these keys. See * is set, the cache ID is created automatically from these keys. See
* drupal_render_cid_create(). * drupal_render_cid_create().
* - 'granularity' (optional): Define the cache granularity using binary * - 'granularity' (optional): Define the cache granularity using binary
* combinations of the cache granularity constants, e.g. DRUPAL_CACHE_PER_USER * combinations of the cache granularity constants, e.g.
* to cache for each user separately or * DRUPAL_CACHE_PER_USER to cache for each user separately or
* DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE to cache separately for each * 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 * page and role. If not specified the element is cached globally for each
* theme and language. * theme and language.
...@@ -5375,6 +5810,7 @@ function drupal_render_page($page) { ...@@ -5375,6 +5810,7 @@ function drupal_render_page($page) {
* *
* @param $elements * @param $elements
* The structured array describing the data to be rendered. * The structured array describing the data to be rendered.
*
* @return * @return
* The rendered HTML. * The rendered HTML.
*/ */
...@@ -5390,13 +5826,16 @@ function drupal_render(&$elements) { ...@@ -5390,13 +5826,16 @@ function drupal_render(&$elements) {
} }
// Try to fetch the element's markup from cache and return. // Try to fetch the element's markup from cache and return.
if (isset($elements['#cache']) && $cached_output = drupal_render_cache_get($elements)) { if (isset($elements['#cache'])) {
$cached_output = drupal_render_cache_get($elements);
if ($cached_output !== FALSE) {
return $cached_output; return $cached_output;
} }
}
// If #markup is not empty, set #type. This allows to specify just #markup on // If #markup is set, ensure #type is set. This allows to specify just #markup
// an element without setting #type. // on an element without setting #type.
if (!empty($elements['#markup']) && !isset($elements['#type'])) { if (isset($elements['#markup']) && !isset($elements['#type'])) {
$elements['#type'] = 'markup'; $elements['#type'] = 'markup';
} }
...@@ -5488,7 +5927,7 @@ function drupal_render(&$elements) { ...@@ -5488,7 +5927,7 @@ function drupal_render(&$elements) {
} }
/** /**
* Render children of an element and concatenate them. * Renders children of an element and concatenates them.
* *
* This renders all children of an element using drupal_render() and then * This renders all children of an element using drupal_render() and then
* joins them together into a single string. * joins them together into a single string.
...@@ -5513,13 +5952,17 @@ function drupal_render_children(&$element, $children_keys = NULL) { ...@@ -5513,13 +5952,17 @@ function drupal_render_children(&$element, $children_keys = NULL) {
} }
/** /**
* Render and print an element. * Renders an element.
* *
* This function renders an element using drupal_render(). The top level * This function renders an element using drupal_render(). The top level
* element is always rendered even if hide() had been previously used on it. * 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.
* *
* Any nested elements are only rendered if they haven't been rendered before * @return
* or if they have been re-enabled with show(). * The rendered element.
* *
* @see drupal_render() * @see drupal_render()
* @see show() * @see show()
...@@ -5538,7 +5981,22 @@ function render(&$element) { ...@@ -5538,7 +5981,22 @@ function render(&$element) {
} }
/** /**
* Hide an element from later rendering. * Hides 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 render()
* @see show() * @see show()
...@@ -5549,10 +6007,25 @@ function hide(&$element) { ...@@ -5549,10 +6007,25 @@ function hide(&$element) {
} }
/** /**
* Show a hidden or already printed element from later rendering. * Shows a hidden element for later rendering.
* *
* Alternatively, render($element) could be used which automatically shows the * You can also use render($element), which shows the element while rendering
* element while rendering it. * 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 render()
* @see hide() * @see hide()
...@@ -5563,16 +6036,17 @@ function show(&$element) { ...@@ -5563,16 +6036,17 @@ function show(&$element) {
} }
/** /**
* Get the rendered output of a renderable element from cache. * Gets the rendered output of a renderable element from the cache.
*
* @see drupal_render()
* @see drupal_render_cache_set()
* *
* @param $elements * @param $elements
* A renderable array. * A renderable array.
*
* @return * @return
* A markup string containing the rendered content of the element, or FALSE * A markup string containing the rendered content of the element, or FALSE
* if no cached copy of the element is available. * if no cached copy of the element is available.
*
* @see drupal_render()
* @see drupal_render_cache_set()
*/ */
function drupal_render_cache_get($elements) { function drupal_render_cache_get($elements) {
if (!in_array($_SERVER['REQUEST_METHOD'], array('GET', 'HEAD')) || !$cid = drupal_render_cid_create($elements)) { if (!in_array($_SERVER['REQUEST_METHOD'], array('GET', 'HEAD')) || !$cid = drupal_render_cid_create($elements)) {
...@@ -5593,17 +6067,17 @@ function drupal_render_cache_get($elements) { ...@@ -5593,17 +6067,17 @@ function drupal_render_cache_get($elements) {
} }
/** /**
* Cache the rendered output of a renderable element. * Caches the rendered output of a renderable element.
* *
* This is called by drupal_render() if the #cache property is set on an 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 * @param $markup
* The rendered output string of $elements. * The rendered output string of $elements.
* @param $elements * @param $elements
* A renderable array. * A renderable array.
*
* @see drupal_render_cache_get()
*/ */
function drupal_render_cache_set(&$markup, $elements) { function drupal_render_cache_set(&$markup, $elements) {
// Create the cache ID for the element. // Create the cache ID for the element.
...@@ -5619,8 +6093,9 @@ function drupal_render_cache_set(&$markup, $elements) { ...@@ -5619,8 +6093,9 @@ function drupal_render_cache_set(&$markup, $elements) {
// be retrieved and used. // be retrieved and used.
$data['#markup'] = &$markup; $data['#markup'] = &$markup;
// Persist attached data associated with this element. // Persist attached data associated with this element.
if (isset($elements['#attached'])) { $attached = drupal_render_collect_attached($elements, TRUE);
$data['#attached'] = $elements['#attached']; if ($attached) {
$data['#attached'] = $attached;
} }
$bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'cache'; $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'cache';
$expire = isset($elements['#cache']['expire']) ? $elements['#cache']['expire'] : CACHE_PERMANENT; $expire = isset($elements['#cache']['expire']) ? $elements['#cache']['expire'] : CACHE_PERMANENT;
...@@ -5628,9 +6103,53 @@ function drupal_render_cache_set(&$markup, $elements) { ...@@ -5628,9 +6103,53 @@ function drupal_render_cache_set(&$markup, $elements) {
} }
/** /**
* Prepare an element for caching based on a query. This smart caching strategy * Collects #attached for an element and its children into a single array.
* saves Drupal from querying and rendering to HTML when the underlying query is *
* unchanged. * 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;
}
}
/**
* Prepares 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 * Expensive queries should use the query builder to create the query and then
* call this function. Executing the query and formatting results should happen * call this function. Executing the query and formatting results should happen
...@@ -5649,7 +6168,7 @@ function drupal_render_cache_set(&$markup, $elements) { ...@@ -5649,7 +6168,7 @@ function drupal_render_cache_set(&$markup, $elements) {
* *
* @return * @return
* A renderable array with the following keys and values: * A renderable array with the following keys and values:
* - #query: The passed in $query. * - #query: The passed-in $query.
* - #pre_render: $function with a _pre_render suffix. * - #pre_render: $function with a _pre_render suffix.
* - #cache: An associative array prepared for drupal_render_cache_set(). * - #cache: An associative array prepared for drupal_render_cache_set().
*/ */
...@@ -5668,15 +6187,15 @@ function drupal_render_cache_by_query($query, $function, $expire = CACHE_TEMPORA ...@@ -5668,15 +6187,15 @@ function drupal_render_cache_by_query($query, $function, $expire = CACHE_TEMPORA
} }
/** /**
* Returns cache ID parts for building a cache ID.
/**
* Helper function for building cache ids.
* *
* @param $granularity * @param $granularity
* One or more cache granularity constants, e.g. DRUPAL_CACHE_PER_USER to cache * One or more cache granularity constants. For example, to cache separately
* for each user separately or DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE to * for each user, use DRUPAL_CACHE_PER_USER. To cache separately for each
* cache separately for each page and role. * page and role, use the expression:
* @code
* DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE
* @endcode
* *
* @return * @return
* An array of cache ID parts, always containing the active theme. If the * An array of cache ID parts, always containing the active theme. If the
...@@ -5715,7 +6234,7 @@ function drupal_render_cid_parts($granularity = NULL) { ...@@ -5715,7 +6234,7 @@ function drupal_render_cid_parts($granularity = NULL) {
} }
/** /**
* Create the cache ID for a renderable element. * Creates the cache ID for a renderable element.
* *
* This creates the cache ID string, either by returning the #cache['cid'] * 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'] * property if present or by building the cache ID out of the #cache['keys']
...@@ -5762,7 +6281,10 @@ function element_sort_by_title($a, $b) { ...@@ -5762,7 +6281,10 @@ function element_sort_by_title($a, $b) {
} }
/** /**
* Retrieve the default properties for the defined element type. * Retrieves the default properties for the defined element type.
*
* @param $type
* An element type as defined by hook_element_info().
*/ */
function element_info($type) { function element_info($type) {
// Use the advanced drupal_static() pattern, since this is called very often. // Use the advanced drupal_static() pattern, since this is called very often.
...@@ -5785,7 +6307,34 @@ function element_info($type) { ...@@ -5785,7 +6307,34 @@ function element_info($type) {
} }
/** /**
* Function used by uasort to sort structured arrays by weight, without the property weight prefix. * Retrieves 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;
}
/**
* Sorts a structured array by the 'weight' element.
*
* Note that the sorting is by the 'weight' array element, not by the render
* element property '#weight'.
*
* Callback for uasort() used in various functions.
*
* @param $a
* First item for comparison. The compared items should be associative arrays
* that optionally include a 'weight' element. For items without a 'weight'
* element, a default value of 0 will be used.
* @param $b
* Second item for comparison.
*/ */
function drupal_sort_weight($a, $b) { function drupal_sort_weight($a, $b) {
$a_weight = (is_array($a) && isset($a['weight'])) ? $a['weight'] : 0; $a_weight = (is_array($a) && isset($a['weight'])) ? $a['weight'] : 0;
...@@ -5797,33 +6346,50 @@ function drupal_sort_weight($a, $b) { ...@@ -5797,33 +6346,50 @@ function drupal_sort_weight($a, $b) {
} }
/** /**
* Check if the key is a property. * 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']);
}
/**
* Checks if the key is a property.
*/ */
function element_property($key) { function element_property($key) {
return $key[0] == '#'; return $key[0] == '#';
} }
/** /**
* Get properties of a structured array element. Properties begin with '#'. * Gets properties of a structured array element (keys beginning with '#').
*/ */
function element_properties($element) { function element_properties($element) {
return array_filter(array_keys((array) $element), 'element_property'); return array_filter(array_keys((array) $element), 'element_property');
} }
/** /**
* Check if the key is a child. * Checks if the key is a child.
*/ */
function element_child($key) { function element_child($key) {
return !isset($key[0]) || $key[0] != '#'; return !isset($key[0]) || $key[0] != '#';
} }
/** /**
* Return the children of an element, optionally sorted by weight. * Identifies the children of an element array, optionally sorted by weight.
*
* The children of a element array are those key/value pairs whose key does
* not start with a '#'. See drupal_render() for details.
* *
* @param $elements * @param $elements
* The element to be sorted. * The element array whose children are to be identified.
* @param $sort * @param $sort
* Boolean to indicate whether the children should be sorted by weight. * Boolean to indicate whether the children should be sorted by weight.
*
* @return * @return
* The array keys of the element's children. * The array keys of the element's children.
*/ */
...@@ -5859,10 +6425,11 @@ function element_children(&$elements, $sort = FALSE) { ...@@ -5859,10 +6425,11 @@ function element_children(&$elements, $sort = FALSE) {
} }
/** /**
* Return the visibile children of an element. * Returns the visible children of an element.
* *
* @param $elements * @param $elements
* The parent element. * The parent element.
*
* @return * @return
* The array keys of the element's visible children. * The array keys of the element's visible children.
*/ */
...@@ -5965,14 +6532,22 @@ function element_set_attributes(array &$element, array $map) { ...@@ -5965,14 +6532,22 @@ function element_set_attributes(array &$element, array $map) {
* An array of parent keys, starting with the outermost key. * An array of parent keys, starting with the outermost key.
* @param $value * @param $value
* The value to set. * 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() * @see drupal_array_get_nested_value()
*/ */
function drupal_array_set_nested_value(array &$array, array $parents, $value) { function drupal_array_set_nested_value(array &$array, array $parents, $value, $force = FALSE) {
$ref = &$array; $ref = &$array;
foreach ($parents as $parent) { foreach ($parents as $parent) {
// Note that PHP is fine with referencing a not existing array key - in this // PHP auto-creates container arrays and NULL entries without error if $ref
// case it just creates an entry with NULL as value. // 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 = &$ref[$parent];
} }
$ref = $value; $ref = $value;
...@@ -6020,7 +6595,7 @@ function drupal_array_set_nested_value(array &$array, array $parents, $value) { ...@@ -6020,7 +6595,7 @@ function drupal_array_set_nested_value(array &$array, array $parents, $value) {
* The array from which to get the value. * The array from which to get the value.
* @param $parents * @param $parents
* An array of parent keys of the value, starting with the outermost key. * An array of parent keys of the value, starting with the outermost key.
* @param &$key_exists * @param $key_exists
* (optional) If given, an already defined variable that is altered by * (optional) If given, an already defined variable that is altered by
* reference. * reference.
* *
...@@ -6033,18 +6608,16 @@ function drupal_array_set_nested_value(array &$array, array $parents, $value) { ...@@ -6033,18 +6608,16 @@ function drupal_array_set_nested_value(array &$array, array $parents, $value) {
* *
* @see drupal_array_set_nested_value() * @see drupal_array_set_nested_value()
*/ */
function drupal_array_get_nested_value(array &$array, array $parents, &$key_exists = NULL) { function &drupal_array_get_nested_value(array &$array, array $parents, &$key_exists = NULL) {
$ref = &$array; $ref = &$array;
foreach ($parents as $parent) { foreach ($parents as $parent) {
// array_key_exists() is slower than isset() and triggers notices if the if (is_array($ref) && array_key_exists($parent, $ref)) {
// second argument is not an array, so only call it when absolutely
// necessary.
if (isset($ref[$parent]) || (is_array($ref) && array_key_exists($parent, $ref))) {
$ref = &$ref[$parent]; $ref = &$ref[$parent];
} }
else { else {
$key_exists = FALSE; $key_exists = FALSE;
return NULL; $null = NULL;
return $null;
} }
} }
$key_exists = TRUE; $key_exists = TRUE;
...@@ -6052,7 +6625,7 @@ function drupal_array_get_nested_value(array &$array, array $parents, &$key_exis ...@@ -6052,7 +6625,7 @@ function drupal_array_get_nested_value(array &$array, array $parents, &$key_exis
} }
/** /**
* Determines whether a nested array with variable depth contains all of the requested keys. * Determines whether a nested array contains the requested keys.
* *
* This helper function should be used when the depth of the array element to be * 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 * checked may vary (that is, the number of parent keys is variable). See
...@@ -6088,14 +6661,11 @@ function drupal_array_nested_key_exists(array $array, array $parents) { ...@@ -6088,14 +6661,11 @@ function drupal_array_nested_key_exists(array $array, array $parents) {
} }
/** /**
* Provide theme registration for themes across .inc files. * Provides theme registration for themes across .inc files.
*/ */
function drupal_common_theme() { function drupal_common_theme() {
return array( return array(
// theme.inc // From theme.inc.
'placeholder' => array(
'variables' => array('text' => NULL)
),
'html' => array( 'html' => array(
'render element' => 'page', 'render element' => 'page',
'template' => 'html', 'template' => 'html',
...@@ -6171,7 +6741,7 @@ function drupal_common_theme() { ...@@ -6171,7 +6741,7 @@ function drupal_common_theme() {
'html_tag' => array( 'html_tag' => array(
'render element' => 'element', 'render element' => 'element',
), ),
// from theme.maintenance.inc // From theme.maintenance.inc.
'maintenance_page' => array( 'maintenance_page' => array(
'variables' => array('content' => NULL, 'show_messages' => TRUE), 'variables' => array('content' => NULL, 'show_messages' => TRUE),
'template' => 'maintenance-page', 'template' => 'maintenance-page',
...@@ -6191,7 +6761,7 @@ function drupal_common_theme() { ...@@ -6191,7 +6761,7 @@ function drupal_common_theme() {
'authorize_report' => array( 'authorize_report' => array(
'variables' => array('messages' => array()), 'variables' => array('messages' => array()),
), ),
// from pager.inc // From pager.inc.
'pager' => array( 'pager' => array(
'variables' => array('tags' => array(), 'element' => 0, 'parameters' => array(), 'quantity' => 9), 'variables' => array('tags' => array(), 'element' => 0, 'parameters' => array(), 'quantity' => 9),
), ),
...@@ -6210,11 +6780,7 @@ function drupal_common_theme() { ...@@ -6210,11 +6780,7 @@ function drupal_common_theme() {
'pager_link' => array( 'pager_link' => array(
'variables' => array('text' => NULL, 'page_new' => NULL, 'element' => NULL, 'parameters' => array(), 'attributes' => array()), 'variables' => array('text' => NULL, 'page_new' => NULL, 'element' => NULL, 'parameters' => array(), 'attributes' => array()),
), ),
// from locale.inc // From menu.inc.
'locale_admin_manage_screen' => array(
'render element' => 'form',
),
// from menu.inc
'menu_link' => array( 'menu_link' => array(
'render element' => 'element', 'render element' => 'element',
), ),
...@@ -6228,9 +6794,9 @@ function drupal_common_theme() { ...@@ -6228,9 +6794,9 @@ function drupal_common_theme() {
'render element' => 'element', 'render element' => 'element',
), ),
'menu_local_tasks' => array( 'menu_local_tasks' => array(
'variables' => array(), 'variables' => array('primary' => array(), 'secondary' => array()),
), ),
// from form.inc // From form.inc.
'select' => array( 'select' => array(
'render element' => 'element', 'render element' => 'element',
), ),
...@@ -6286,7 +6852,7 @@ function drupal_common_theme() { ...@@ -6286,7 +6852,7 @@ function drupal_common_theme() {
'render element' => 'element', 'render element' => 'element',
), ),
'form_required_marker' => array( 'form_required_marker' => array(
'arguments' => array('element' => NULL), 'render element' => 'element',
), ),
'form_element_label' => array( 'form_element_label' => array(
'render element' => 'element', 'render element' => 'element',
...@@ -6301,12 +6867,12 @@ function drupal_common_theme() { ...@@ -6301,12 +6867,12 @@ function drupal_common_theme() {
} }
/** /**
* @ingroup schemaapi * @addtogroup schemaapi
* @{ * @{
*/ */
/** /**
* Creates all tables in a module's hook_schema() implementation. * Creates all tables defined in a module's hook_schema().
* *
* Note: This function does not pass the module's schema through * Note: This function does not pass the module's schema through
* hook_schema_alter(). The module's tables will be created exactly as the * hook_schema_alter(). The module's tables will be created exactly as the
...@@ -6325,7 +6891,7 @@ function drupal_install_schema($module) { ...@@ -6325,7 +6891,7 @@ function drupal_install_schema($module) {
} }
/** /**
* Remove all tables that a module defines in its hook_schema(). * Removes all tables defined in a module's hook_schema().
* *
* Note: This function does not pass the module's schema through * Note: This function does not pass the module's schema through
* hook_schema_alter(). The module's tables will be created exactly as the * hook_schema_alter(). The module's tables will be created exactly as the
...@@ -6333,6 +6899,7 @@ function drupal_install_schema($module) { ...@@ -6333,6 +6899,7 @@ function drupal_install_schema($module) {
* *
* @param $module * @param $module
* The module for which the tables will be removed. * The module for which the tables will be removed.
*
* @return * @return
* An array of arrays with the following key/value pairs: * An array of arrays with the following key/value pairs:
* - success: a boolean indicating whether the query succeeded. * - success: a boolean indicating whether the query succeeded.
...@@ -6388,7 +6955,7 @@ function drupal_get_schema_unprocessed($module, $table = NULL) { ...@@ -6388,7 +6955,7 @@ function drupal_get_schema_unprocessed($module, $table = NULL) {
} }
/** /**
* Fill in required default values for table definitions returned by hook_schema(). * Fills in required default values for table definitions from hook_schema().
* *
* @param $schema * @param $schema
* The schema definition array as it was returned by the module's * The schema definition array as it was returned by the module's
...@@ -6419,7 +6986,9 @@ function _drupal_schema_initialize(&$schema, $module, $remove_descriptions = TRU ...@@ -6419,7 +6986,9 @@ function _drupal_schema_initialize(&$schema, $module, $remove_descriptions = TRU
} }
/** /**
* Retrieve a list of fields from a table schema. The list is suitable for use in a SQL query. * Retrieves a list of fields from a table schema.
*
* The returned list is suitable for use in an SQL query.
* *
* @param $table * @param $table
* The name of the table from which to retrieve fields. * The name of the table from which to retrieve fields.
...@@ -6427,7 +6996,7 @@ function _drupal_schema_initialize(&$schema, $module, $remove_descriptions = TRU ...@@ -6427,7 +6996,7 @@ function _drupal_schema_initialize(&$schema, $module, $remove_descriptions = TRU
* An optional prefix to to all fields. * An optional prefix to to all fields.
* *
* @return An array of fields. * @return An array of fields.
**/ */
function drupal_schema_fields_sql($table, $prefix = NULL) { function drupal_schema_fields_sql($table, $prefix = NULL) {
$schema = drupal_get_schema($table); $schema = drupal_get_schema($table);
$fields = array_keys($schema['fields']); $fields = array_keys($schema['fields']);
...@@ -6444,30 +7013,31 @@ function drupal_schema_fields_sql($table, $prefix = NULL) { ...@@ -6444,30 +7013,31 @@ function drupal_schema_fields_sql($table, $prefix = NULL) {
} }
/** /**
* Saves a record to the database based upon the schema. * Saves (inserts or updates) a record to the database based upon the schema.
* *
* Default values are filled in for missing items, and 'serial' (auto increment) * Do not use drupal_write_record() within hook_update_N() functions, since the
* types are filled in with IDs. * database schema cannot be relied upon when a user is running a series of
* updates. Instead, use db_insert() or db_update() to save the record.
* *
* @param $table * @param $table
* The name of the table; this must be defined by a hook_schema() * The name of the table; this must be defined by a hook_schema()
* implementation. * implementation.
* @param $record * @param $record
* An object or array representing the record to write, passed in by * An object or array representing the record to write, passed in by
* reference. The function will fill in defaults from the schema and add an * reference. If inserting a new record, values not provided in $record will
* ID value to serial fields. * 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 * @param $primary_keys
* If this is an update, specify the primary keys' field names. If this is a * To indicate that this is a new record to be inserted, omit this argument.
* new record, you must not provide this value. If there is only 1 field in * If this is an update, this argument specifies the primary keys' field
* the key, you may pass in a string; if there are multiple fields in the key, * names. If there is only 1 field in the key, you may pass in a string; if
* pass in an array. * there are multiple fields in the key, pass in an array.
* *
* @return * @return
* Failure to write a record will return FALSE. Otherwise SAVED_NEW or * If the record insert or update failed, returns FALSE. If it succeeded,
* SAVED_UPDATED is returned depending on the operation performed. The $object * returns SAVED_NEW or SAVED_UPDATED, depending on the operation performed.
* parameter will contain values for any serial fields defined by the $table.
* For example, $record->nid or $record['nid'] will be populated after
* inserting a new a new node.
*/ */
function drupal_write_record($table, &$record, $primary_keys = array()) { function drupal_write_record($table, &$record, $primary_keys = array()) {
// Standardize $primary_keys to an array. // Standardize $primary_keys to an array.
...@@ -6604,11 +7174,11 @@ function drupal_write_record($table, &$record, $primary_keys = array()) { ...@@ -6604,11 +7174,11 @@ function drupal_write_record($table, &$record, $primary_keys = array()) {
} }
/** /**
* @} End of "ingroup schemaapi". * @} End of "addtogroup schemaapi".
*/ */
/** /**
* Parse Drupal module and theme info file format. * Parses Drupal module and theme .info files.
* *
* Info files are NOT for placing arbitrary theme and module-specific settings. * Info files are NOT for placing arbitrary theme and module-specific settings.
* Use variable_get() and variable_set() for that. * Use variable_get() and variable_set() for that.
...@@ -6619,39 +7189,46 @@ function drupal_write_record($table, &$record, $primary_keys = array()) { ...@@ -6619,39 +7189,46 @@ function drupal_write_record($table, &$record, $primary_keys = array()) {
* - dependencies: An array of shortnames of other modules this module requires. * - dependencies: An array of shortnames of other modules this module requires.
* - package: The name of the package of modules this module belongs to. * - package: The name of the package of modules this module belongs to.
* *
* @see forum.info * See forum.info for an example of a module .info file.
* *
* Information stored in a theme .info file: * Information stored in a theme .info file:
* - name: The real name of the theme for display purposes * - name: The real name of the theme for display purposes.
* - description: Brief description * - description: Brief description.
* - screenshot: Path to screenshot relative to the theme's .info file. * - screenshot: Path to screenshot relative to the theme's .info file.
* - engine: Theme engine, typically: engine = phptemplate * - engine: Theme engine; typically phptemplate.
* - base: Name of a base theme, if applicable, eg: base = zen * - base: Name of a base theme, if applicable; e.g., base = zen.
* - regions: Listed regions eg: region[left] = Left sidebar * - regions: Listed regions; e.g., region[left] = Left sidebar.
* - features: Features available eg: features[] = logo * - features: Features available; e.g., features[] = logo.
* - stylesheets: Theme stylesheets eg: stylesheets[all][] = my-style.css * - stylesheets: Theme stylesheets; e.g., stylesheets[all][] = my-style.css.
* - scripts: Theme scripts eg: scripts[] = my-script.css * - scripts: Theme scripts; e.g., scripts[] = my-script.js.
* *
* @see bartik.info * See bartik.info for an example of a theme .info file.
* *
* @param $filename * @param $filename
* The file we are parsing. Accepts file with relative or absolute path. * The file we are parsing. Accepts file with relative or absolute path.
*
* @return * @return
* The info array. * The info array.
* *
* @see drupal_parse_info_format() * @see drupal_parse_info_format()
*/ */
function drupal_parse_info_file($filename) { function drupal_parse_info_file($filename) {
$info = &drupal_static(__FUNCTION__, array());
if (!isset($info[$filename])) {
if (!file_exists($filename)) { if (!file_exists($filename)) {
return array(); $info[$filename] = array();
} }
else {
$data = file_get_contents($filename); $data = file_get_contents($filename);
return drupal_parse_info_format($data); $info[$filename] = drupal_parse_info_format($data);
}
}
return $info[$filename];
} }
/** /**
* Parse data in Drupal's .info format. * Parses data in Drupal's .info format.
* *
* Data should be in an .ini-like format to specify values. White-space * Data should be in an .ini-like format to specify values. White-space
* generally doesn't matter, except inside values: * generally doesn't matter, except inside values:
...@@ -6681,6 +7258,7 @@ function drupal_parse_info_file($filename) { ...@@ -6681,6 +7258,7 @@ function drupal_parse_info_file($filename) {
* *
* @param $data * @param $data
* A string to parse. * A string to parse.
*
* @return * @return
* The info array. * The info array.
* *
...@@ -6704,19 +7282,19 @@ function drupal_parse_info_format($data) { ...@@ -6704,19 +7282,19 @@ function drupal_parse_info_format($data) {
)\s*$ # Stop at the next end of a line, ignoring trailing whitespace )\s*$ # Stop at the next end of a line, ignoring trailing whitespace
@msx', $data, $matches, PREG_SET_ORDER)) { @msx', $data, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) { foreach ($matches as $match) {
// Fetch the key and value string // Fetch the key and value string.
$i = 0; $i = 0;
foreach (array('key', 'value1', 'value2', 'value3') as $var) { foreach (array('key', 'value1', 'value2', 'value3') as $var) {
$$var = isset($match[++$i]) ? $match[$i] : ''; $$var = isset($match[++$i]) ? $match[$i] : '';
} }
$value = stripslashes(substr($value1, 1, -1)) . stripslashes(substr($value2, 1, -1)) . $value3; $value = stripslashes(substr($value1, 1, -1)) . stripslashes(substr($value2, 1, -1)) . $value3;
// Parse array syntax // Parse array syntax.
$keys = preg_split('/\]?\[/', rtrim($key, ']')); $keys = preg_split('/\]?\[/', rtrim($key, ']'));
$last = array_pop($keys); $last = array_pop($keys);
$parent = &$info; $parent = &$info;
// Create nested arrays // Create nested arrays.
foreach ($keys as $key) { foreach ($keys as $key) {
if ($key == '') { if ($key == '') {
$key = count($parent); $key = count($parent);
...@@ -6732,7 +7310,7 @@ function drupal_parse_info_format($data) { ...@@ -6732,7 +7310,7 @@ function drupal_parse_info_format($data) {
$value = $constants[$value]; $value = $constants[$value];
} }
// Insert actual value // Insert actual value.
if ($last == '') { if ($last == '') {
$last = count($parent); $last = count($parent);
} }
...@@ -6744,12 +7322,14 @@ function drupal_parse_info_format($data) { ...@@ -6744,12 +7322,14 @@ function drupal_parse_info_format($data) {
} }
/** /**
* Severity levels, as defined in RFC 3164: http://www.ietf.org/rfc/rfc3164.txt. * Returns a list of severity levels, as defined in RFC 3164.
* *
* @return * @return
* Array of the possible severity levels for log messages. * Array of the possible severity levels for log messages.
* *
* @see http://www.ietf.org/rfc/rfc3164.txt
* @see watchdog() * @see watchdog()
* @ingroup logging_severity_levels
*/ */
function watchdog_severity_levels() { function watchdog_severity_levels() {
return array( return array(
...@@ -6766,7 +7346,7 @@ function watchdog_severity_levels() { ...@@ -6766,7 +7346,7 @@ function watchdog_severity_levels() {
/** /**
* Explode a string of given tags into an array. * Explodes a string of tags into an array.
* *
* @see drupal_implode_tags() * @see drupal_implode_tags()
*/ */
...@@ -6792,7 +7372,7 @@ function drupal_explode_tags($tags) { ...@@ -6792,7 +7372,7 @@ function drupal_explode_tags($tags) {
} }
/** /**
* Implode an array of tags into a string. * Implodes an array of tags into a string.
* *
* @see drupal_explode_tags() * @see drupal_explode_tags()
*/ */
...@@ -6810,7 +7390,7 @@ function drupal_implode_tags($tags) { ...@@ -6810,7 +7390,7 @@ function drupal_implode_tags($tags) {
} }
/** /**
* Flush all cached data on the site. * Flushes all cached data on the site.
* *
* Empties cache tables, rebuilds the menu cache and theme registries, and * 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. * invokes a hook so that other modules' cache data can be cleared as well.
...@@ -6828,6 +7408,7 @@ function drupal_flush_all_caches() { ...@@ -6828,6 +7408,7 @@ function drupal_flush_all_caches() {
system_rebuild_theme_data(); system_rebuild_theme_data();
drupal_theme_rebuild(); drupal_theme_rebuild();
entity_info_cache_clear();
node_types_rebuild(); node_types_rebuild();
// node_menu() defines menu items based on node types so it needs to come // node_menu() defines menu items based on node types so it needs to come
// after node types are rebuilt. // after node types are rebuilt.
...@@ -6838,7 +7419,7 @@ function drupal_flush_all_caches() { ...@@ -6838,7 +7419,7 @@ function drupal_flush_all_caches() {
// Don't clear cache_form - in-progress form submissions may break. // Don't clear cache_form - in-progress form submissions may break.
// Ordered so clearing the page cache will always be the last action. // Ordered so clearing the page cache will always be the last action.
$core = array('cache', 'cache_filter', 'cache_bootstrap', 'cache_page'); $core = array('cache', 'cache_path', 'cache_filter', 'cache_bootstrap', 'cache_page');
$cache_tables = array_merge(module_invoke_all('flush_caches'), $core); $cache_tables = array_merge(module_invoke_all('flush_caches'), $core);
foreach ($cache_tables as $table) { foreach ($cache_tables as $table) {
cache_clear_all('*', $table, TRUE); cache_clear_all('*', $table, TRUE);
...@@ -6851,10 +7432,10 @@ function drupal_flush_all_caches() { ...@@ -6851,10 +7432,10 @@ function drupal_flush_all_caches() {
} }
/** /**
* Helper function to change query-strings on css/js files. * Changes the dummy query string added to all CSS and JavaScript files.
* *
* Changes the character added to all css/js files as dummy query-string, so * Changing the dummy query string appended to CSS and JavaScript files forces
* that all browsers are forced to reload fresh files. * all browsers to reload fresh files.
*/ */
function _drupal_flush_css_js() { function _drupal_flush_css_js() {
// The timestamp is converted to base 36 in order to make it more compact. // The timestamp is converted to base 36 in order to make it more compact.
...@@ -6862,7 +7443,7 @@ function _drupal_flush_css_js() { ...@@ -6862,7 +7443,7 @@ function _drupal_flush_css_js() {
} }
/** /**
* Debug function used for outputting debug information. * Outputs debug information.
* *
* The debug information is passed on to trigger_error() after being converted * The debug information is passed on to trigger_error() after being converted
* to a string using _drupal_debug_message(). * to a string using _drupal_debug_message().
...@@ -6887,10 +7468,11 @@ function debug($data, $label = NULL, $print_r = FALSE) { ...@@ -6887,10 +7468,11 @@ function debug($data, $label = NULL, $print_r = FALSE) {
} }
/** /**
* Parse a dependency for comparison by drupal_check_incompatibility(). * Parses a dependency for comparison by drupal_check_incompatibility().
* *
* @param $dependency * @param $dependency
* A dependency string, for example 'foo (>=7.x-4.5-beta5, 3.x)'. * A dependency string, for example 'foo (>=7.x-4.5-beta5, 3.x)'.
*
* @return * @return
* An associative array with three keys: * An associative array with three keys:
* - 'name' includes the name of the thing to depend on (e.g. 'foo'). * - 'name' includes the name of the thing to depend on (e.g. 'foo').
...@@ -6944,12 +7526,13 @@ function drupal_parse_dependency($dependency) { ...@@ -6944,12 +7526,13 @@ function drupal_parse_dependency($dependency) {
} }
/** /**
* Check whether a version is compatible with a given dependency. * Checks whether a version is compatible with a given dependency.
* *
* @param $v * @param $v
* The parsed dependency structure from drupal_parse_dependency(). * The parsed dependency structure from drupal_parse_dependency().
* @param $current_version * @param $current_version
* The version to check against (like 4.2). * The version to check against (like 4.2).
*
* @return * @return
* NULL if compatible, otherwise the original dependency version string that * NULL if compatible, otherwise the original dependency version string that
* caused the incompatibility. * caused the incompatibility.
...@@ -6969,12 +7552,12 @@ function drupal_check_incompatibility($v, $current_version) { ...@@ -6969,12 +7552,12 @@ function drupal_check_incompatibility($v, $current_version) {
/** /**
* Get the entity info array of an entity type. * Get the entity info array of an entity type.
* *
* @see hook_entity_info()
* @see hook_entity_info_alter()
*
* @param $entity_type * @param $entity_type
* The entity type, e.g. node, for which the info shall be returned, or NULL * The entity type, e.g. node, for which the info shall be returned, or NULL
* to return an array with info about all types. * to return an array with info about all types.
*
* @see hook_entity_info()
* @see hook_entity_info_alter()
*/ */
function entity_get_info($entity_type = NULL) { function entity_get_info($entity_type = NULL) {
global $language; global $language;
...@@ -7062,21 +7645,34 @@ function entity_info_cache_clear() { ...@@ -7062,21 +7645,34 @@ function entity_info_cache_clear() {
* The entity type; e.g. 'node' or 'user'. * The entity type; e.g. 'node' or 'user'.
* @param $entity * @param $entity
* The entity from which to extract values. * The entity from which to extract values.
*
* @return * @return
* A numerically indexed array (not a hash table) containing these * A numerically indexed array (not a hash table) containing these
* elements: * elements:
* 0: primary id of the entity * - 0: Primary ID of the entity.
* 1: revision id of the entity, or NULL if $entity_type is not versioned * - 1: Revision ID of the entity, or NULL if $entity_type is not versioned.
* 2: bundle name of the entity * - 2: Bundle name of the entity, or NULL if $entity_type has no bundles.
*/ */
function entity_extract_ids($entity_type, $entity) { function entity_extract_ids($entity_type, $entity) {
$info = entity_get_info($entity_type); $info = entity_get_info($entity_type);
// Objects being created might not have id/vid yet. // Objects being created might not have id/vid yet.
$id = isset($entity->{$info['entity keys']['id']}) ? $entity->{$info['entity keys']['id']} : NULL; $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; $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. if (!empty($info['entity keys']['bundle'])) {
$bundle = $info['entity keys']['bundle'] ? $entity->{$info['entity keys']['bundle']} : $entity_type; // Explicitly fail for malformed entities missing the bundle property.
if (!isset($entity->{$info['entity keys']['bundle']}) || $entity->{$info['entity keys']['bundle']} === '') {
throw new EntityMalformedException(t('Missing bundle property on entity of type @entity_type.', array('@entity_type' => $entity_type)));
}
$bundle = $entity->{$info['entity keys']['bundle']};
}
else {
// The entity type provides no bundle key: assume a single bundle, named
// after the entity type.
$bundle = $entity_type;
}
return array($id, $vid, $bundle); return array($id, $vid, $bundle);
} }
...@@ -7088,13 +7684,12 @@ function entity_extract_ids($entity_type, $entity) { ...@@ -7088,13 +7684,12 @@ function entity_extract_ids($entity_type, $entity) {
* @param $entity_type * @param $entity_type
* The entity type; e.g. 'node' or 'user'. * The entity type; e.g. 'node' or 'user'.
* @param $ids * @param $ids
* A numerically indexed array, as returned by entity_extract_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 * @return
* An entity structure, initialized with the ids provided. * An entity structure, initialized with the ids provided.
*
* @see entity_extract_ids()
*/ */
function entity_create_stub_entity($entity_type, $ids) { function entity_create_stub_entity($entity_type, $ids) {
$entity = new stdClass(); $entity = new stdClass();
...@@ -7112,8 +7707,7 @@ function entity_create_stub_entity($entity_type, $ids) { ...@@ -7112,8 +7707,7 @@ function entity_create_stub_entity($entity_type, $ids) {
/** /**
* Load entities from the database. * Load entities from the database.
* *
* This function should be used whenever you need to load more than one entity * The entities are stored in a static memory cache, and will not require
* from the database. The entities are loaded into memory and will not require
* database access if loaded again during the same page request. * database access if loaded again during the same page request.
* *
* The actual loading is done through a class that has to implement the * The actual loading is done through a class that has to implement the
...@@ -7125,21 +7719,28 @@ function entity_create_stub_entity($entity_type, $ids) { ...@@ -7125,21 +7719,28 @@ function entity_create_stub_entity($entity_type, $ids) {
* DrupalDefaultEntityController class. See node_entity_info() and the * DrupalDefaultEntityController class. See node_entity_info() and the
* NodeController in node.module as an example. * NodeController in node.module as an example.
* *
* @see hook_entity_info()
* @see DrupalEntityControllerInterface
* @see DrupalDefaultEntityController
*
* @param $entity_type * @param $entity_type
* The entity type to load, e.g. node or user. * The entity type to load, e.g. node or user.
* @param $ids * @param $ids
* An array of entity IDs, or FALSE to load all entities. * An array of entity IDs, or FALSE to load all entities.
* @param $conditions * @param $conditions
* An array of conditions in the form 'field' => $value. * (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 * @param $reset
* Whether to reset the internal cache for the requested entity type. * Whether to reset the internal cache for the requested entity type.
* *
* @return * @return
* An array of entity objects indexed by their ids. * 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.
*
* @see hook_entity_info()
* @see DrupalEntityControllerInterface
* @see DrupalDefaultEntityController
* @see EntityFieldQuery
*/ */
function entity_load($entity_type, $ids = FALSE, $conditions = array(), $reset = FALSE) { function entity_load($entity_type, $ids = FALSE, $conditions = array(), $reset = FALSE) {
if ($reset) { if ($reset) {
...@@ -7148,6 +7749,28 @@ function entity_load($entity_type, $ids = FALSE, $conditions = array(), $reset = ...@@ -7148,6 +7749,28 @@ function entity_load($entity_type, $ids = FALSE, $conditions = array(), $reset =
return entity_get_controller($entity_type)->load($ids, $conditions); 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. * Get the entity controller class for an entity type.
*/ */
...@@ -7174,14 +7797,22 @@ function entity_get_controller($entity_type) { ...@@ -7174,14 +7797,22 @@ function entity_get_controller($entity_type) {
* recursion. By convention, entity_prepare_view() is called after * recursion. By convention, entity_prepare_view() is called after
* field_attach_prepare_view() to allow entity level hooks to act on content * field_attach_prepare_view() to allow entity level hooks to act on content
* loaded by field API. * loaded by field API.
* @see hook_entity_prepare_view()
* *
* @param $entity_type * @param $entity_type
* The type of entity, i.e. 'node', 'user'. * The type of entity, i.e. 'node', 'user'.
* @param $entities * @param $entities
* The entity objects which are being prepared for view, keyed by object ID. * 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.
*
* @see hook_entity_prepare_view()
*/ */
function entity_prepare_view($entity_type, $entities) { 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 // To ensure hooks are only run once per entity, check for an
// entity_view_prepared flag and only process items without it. // entity_view_prepared flag and only process items without it.
// @todo: resolve this more generally for both entity and field level hooks. // @todo: resolve this more generally for both entity and field level hooks.
...@@ -7197,28 +7828,23 @@ function entity_prepare_view($entity_type, $entities) { ...@@ -7197,28 +7828,23 @@ function entity_prepare_view($entity_type, $entities) {
} }
if (!empty($prepare)) { if (!empty($prepare)) {
module_invoke_all('entity_prepare_view', $prepare, $entity_type); module_invoke_all('entity_prepare_view', $prepare, $entity_type, $langcode);
} }
} }
/** /**
* Returns the uri elements of an entity. * Returns the URI elements of an entity.
* *
* @param $entity_type * @param $entity_type
* The entity type; e.g. 'node' or 'user'. * The entity type; e.g. 'node' or 'user'.
* @param $entity * @param $entity
* The entity for which to generate a path. * The entity for which to generate a path.
* @return * @return
* An array containing the 'path' and 'options' keys used to build the uri of * 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 * the entity, and matching the signature of url(). NULL if the entity has no
* uri of its own. * URI of its own.
*/ */
function entity_uri($entity_type, $entity) { 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); $info = entity_get_info($entity_type);
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
...@@ -7231,45 +7857,39 @@ function entity_uri($entity_type, $entity) { ...@@ -7231,45 +7857,39 @@ function entity_uri($entity_type, $entity) {
$uri_callback = $info['uri callback']; $uri_callback = $info['uri callback'];
} }
else { else {
$uri_callback = NULL; return NULL;
} }
// Invoke the callback to get the URI. If there is no callback, set the // Invoke the callback to get the URI. If there is no callback, return NULL.
// 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)) { if (isset($uri_callback) && function_exists($uri_callback)) {
$entity->uri = $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 // Pass the entity data to url() so that alter functions do not need to
// lookup this entity again. // lookup this entity again.
$entity->uri['options']['entity_type'] = $entity_type; $uri['options']['entity_type'] = $entity_type;
$entity->uri['options']['entity'] = $entity; $uri['options']['entity'] = $entity;
} return $uri;
else {
$entity->uri = FALSE;
}
} }
return $entity->uri ? $entity->uri : NULL;
} }
/** /**
* Returns the label of an entity. * Returns the label of an entity.
* *
* See the 'label callback' component of the hook_entity_info() return value
* for more information.
*
* @param $entity_type * @param $entity_type
* The entity type; e.g. 'node' or 'user'. * The entity type; e.g., 'node' or 'user'.
* @param $entity * @param $entity
* The entity for which to generate a path. * The entity for which to generate the label.
* *
* @return * @return
* A string with the entity label (e.g. node title), or FALSE if not found. * The entity label, or FALSE if not found.
*/ */
function entity_label($entity_type, $entity) { function entity_label($entity_type, $entity) {
$label = FALSE; $label = FALSE;
$info = entity_get_info($entity_type); $info = entity_get_info($entity_type);
if (isset($info['label callback']) && function_exists($info['label callback'])) { if (isset($info['label callback']) && function_exists($info['label callback'])) {
$label = $info['label callback']($entity); $label = $info['label callback']($entity, $entity_type);
} }
elseif (!empty($info['entity keys']['label']) && isset($entity->{$info['entity keys']['label']})) { elseif (!empty($info['entity keys']['label']) && isset($entity->{$info['entity keys']['label']})) {
$label = $entity->{$info['entity keys']['label']}; $label = $entity->{$info['entity keys']['label']};
...@@ -7279,7 +7899,45 @@ function entity_label($entity_type, $entity) { ...@@ -7279,7 +7899,45 @@ function entity_label($entity_type, $entity) {
} }
/** /**
* Helper function for attaching field API validation to entity forms. * Returns the language of an entity.
*
* @param $entity_type
* The entity type; e.g., 'node' or 'user'.
* @param $entity
* The entity for which to get the language.
*
* @return
* A valid language code or NULL if the entity has no language support.
*/
function entity_language($entity_type, $entity) {
$info = entity_get_info($entity_type);
// Invoke the callback to get the language. If there is no callback, try to
// get it from a property of the entity, otherwise NULL.
if (isset($info['language callback']) && function_exists($info['language callback'])) {
$langcode = $info['language callback']($entity_type, $entity);
}
elseif (!empty($info['entity keys']['language']) && isset($entity->{$info['entity keys']['language']})) {
$langcode = $entity->{$info['entity keys']['language']};
}
else {
// The value returned in D8 would be LANGUAGE_NONE, we cannot use it here to
// preserve backward compatibility. In fact this function has been
// introduced very late in the D7 life cycle, mainly as the proper default
// for field_attach_form(). By returning LANGUAGE_NONE when no language
// information is available, we would introduce a potentially BC-breaking
// API change, since field_attach_form() defaults to the default language
// instead of LANGUAGE_NONE. Moreover this allows us to distinguish between
// entities that have no language specified from ones that do not have
// language support at all.
$langcode = NULL;
}
return $langcode;
}
/**
* Attaches field API validation to entity forms.
*/ */
function entity_form_field_validate($entity_type, $form, &$form_state) { function entity_form_field_validate($entity_type, $form, &$form_state) {
// All field attach API functions act on an entity object, but during form // All field attach API functions act on an entity object, but during form
...@@ -7292,20 +7950,24 @@ function entity_form_field_validate($entity_type, $form, &$form_state) { ...@@ -7292,20 +7950,24 @@ function entity_form_field_validate($entity_type, $form, &$form_state) {
} }
/** /**
* Helper function for copying submitted values to entity properties for simple entity forms. * Copies submitted values to entity properties for simple entity forms.
* *
* During the submission handling of an entity form's "Save", "Preview", and * 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 * possibly other buttons, the form state's entity needs to be updated with the
* submitted form values. Each entity form implements its own * submitted form values. Each entity form implements its own builder function
* $form['#builder_function'] for doing this, appropriate for the particular * for doing this, appropriate for the particular entity and form, whereas
* entity and form. Many of these entity builder functions can call this helper * modules may specify additional builder functions in $form['#entity_builders']
* function to re-use its logic of copying $form_state['values'][PROPERTY] * for copying the form values of added form elements to entity properties.
* values to $entity->PROPERTY for all entries in $form_state['values'] that are * Many of the main entity builder functions can call this helper function to
* not field data, and calling field_attach_submit() to copy field data. * 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 * For some entity forms (e.g., forms with complex non-field data and forms that
* simultaneously edit multiple entities), this behavior may be inappropriate, * simultaneously edit multiple entities), this behavior may be inappropriate,
* so the #builder_function for such forms needs to implement the required * so the builder function for such forms needs to implement the required
* functionality instead of calling this function. * functionality instead of calling this function.
*/ */
function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_state) { function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_state) {
...@@ -7320,6 +7982,13 @@ function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_st ...@@ -7320,6 +7982,13 @@ function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_st
$entity->$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. // Copy field values to the entity.
if ($info['fieldable']) { if ($info['fieldable']) {
field_attach_submit($entity_type, $entity, $form, $form_state); field_attach_submit($entity_type, $entity, $form, $form_state);
...@@ -7407,11 +8076,12 @@ function archiver_get_extensions() { ...@@ -7407,11 +8076,12 @@ function archiver_get_extensions() {
} }
/** /**
* Create the appropriate archiver for the specified file. * Creates the appropriate archiver for the specified file.
* *
* @param $file * @param $file
* The full path of the archive file. Note that stream wrapper * The full path of the archive file. Note that stream wrapper paths are
* paths are supported, but not remote ones. * supported, but not remote ones.
*
* @return * @return
* A newly created instance of the archiver class appropriate * A newly created instance of the archiver class appropriate
* for the specified file, already bound to that file. * for the specified file, already bound to that file.
...@@ -7440,14 +8110,14 @@ function archiver_get_archiver($file) { ...@@ -7440,14 +8110,14 @@ function archiver_get_archiver($file) {
} }
/** /**
* Drupal Updater registry. * Assembles the Drupal Updater registry.
* *
* An Updater is a class that knows how to update various parts of the Drupal * 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 * file system, for example to update modules that have newer releases, or to
* install a new theme. * install a new theme.
* *
* @return * @return
* Returns the Drupal Updater class registry. * The Drupal Updater class registry.
* *
* @see hook_updater_info() * @see hook_updater_info()
* @see hook_updater_info_alter() * @see hook_updater_info_alter()
...@@ -7461,3 +8131,39 @@ function drupal_get_updaters() { ...@@ -7461,3 +8131,39 @@ function drupal_get_updaters() {
} }
return $updaters; return $updaters;
} }
/**
* Assembles the Drupal FileTransfer registry.
*
* @return
* 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;
}
<?php <?php
// $Id: database.inc,v 1.141 2010/10/15 18:03:43 webchick Exp $
/** /**
* @file * @file
...@@ -42,7 +41,7 @@ ...@@ -42,7 +41,7 @@
* $result = db_query_range('SELECT n.nid, n.title, n.created * $result = db_query_range('SELECT n.nid, n.title, n.created
* FROM {node} n WHERE n.uid = :uid', 0, 10, array(':uid' => $uid)); * FROM {node} n WHERE n.uid = :uid', 0, 10, array(':uid' => $uid));
* foreach ($result as $record) { * foreach ($result as $record) {
* // Perform operations on $node->title, etc. here. * // Perform operations on $record->title, etc. here.
* } * }
* @endcode * @endcode
* Curly braces are used around "node" to provide table prefixing via * Curly braces are used around "node" to provide table prefixing via
...@@ -153,7 +152,7 @@ ...@@ -153,7 +152,7 @@
* } * }
* *
* // $txn goes out of scope here. Unless the transaction was rolled back, it * // $txn goes out of scope here. Unless the transaction was rolled back, it
* // gets automatically commited here. * // gets automatically committed here.
* } * }
* *
* function my_other_function($id) { * function my_other_function($id) {
...@@ -168,7 +167,7 @@ ...@@ -168,7 +167,7 @@
* } * }
* @endcode * @endcode
* *
* @link http://drupal.org/developing/api/database * @see http://drupal.org/developing/api/database
*/ */
...@@ -193,6 +192,17 @@ abstract class DatabaseConnection extends PDO { ...@@ -193,6 +192,17 @@ abstract class DatabaseConnection extends PDO {
*/ */
protected $target = NULL; 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. * The current database logging object for this connection.
* *
...@@ -263,20 +273,25 @@ abstract class DatabaseConnection extends PDO { ...@@ -263,20 +273,25 @@ abstract class DatabaseConnection extends PDO {
protected $schema = NULL; protected $schema = NULL;
/** /**
* The default prefix used by this database connection. * The prefixes used by this database connection.
* *
* Separated from the other prefixes for performance reasons. * @var array
*/
protected $prefixes = array();
/**
* List of search values for use in prefixTables().
* *
* @var string * @var array
*/ */
protected $defaultPrefix = ''; protected $prefixSearch = array();
/** /**
* The non-default prefixes used by this database connection. * List of replacement values for use in prefixTables().
* *
* @var array * @var array
*/ */
protected $prefixes = array(); protected $prefixReplace = array();
function __construct($dsn, $username, $password, $driver_options = array()) { function __construct($dsn, $username, $password, $driver_options = array()) {
// Initialize and prepare the connection prefix. // Initialize and prepare the connection prefix.
...@@ -288,12 +303,28 @@ abstract class DatabaseConnection extends PDO { ...@@ -288,12 +303,28 @@ abstract class DatabaseConnection extends PDO {
// Call PDO::__construct and PDO::setAttribute. // Call PDO::__construct and PDO::setAttribute.
parent::__construct($dsn, $username, $password, $driver_options); parent::__construct($dsn, $username, $password, $driver_options);
// Set a specific PDOStatement class if the driver requires that. // Set a Statement class, unless the driver opted out.
if (!empty($this->statementClass)) { if (!empty($this->statementClass)) {
$this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this))); $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this)));
} }
} }
/**
* Destroys this Connection object.
*
* PHP does not destruct an object if it is still referenced in other
* variables. In case of PDO database connection objects, PHP only closes the
* connection when the PDO object is destructed, so any references to this
* object may cause the number of maximum allowed connections to be exceeded.
*/
public function destroy() {
// Destroy all references to this connection by setting them to NULL.
// The Statement class attribute only accepts a new value that presents a
// proper callable, so we reset it to PDOStatement.
$this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));
$this->schema = NULL;
}
/** /**
* Returns the default query options for any given query. * Returns the default query options for any given query.
* *
...@@ -310,7 +341,7 @@ abstract class DatabaseConnection extends PDO { ...@@ -310,7 +341,7 @@ abstract class DatabaseConnection extends PDO {
* PDO::FETCH_OBJ, PDO::FETCH_NUM, or a string representing the name of a * 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 * 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. * object of that class. The behavior of all other values is defined by PDO.
* See http://www.php.net/PDOStatement-fetch * See http://php.net/manual/pdostatement.fetch.php
* - return: Depending on the type of query, different return values may be * - return: Depending on the type of query, different return values may be
* meaningful. This directive instructs the system which type of return * meaningful. This directive instructs the system which type of return
* value is desired. The system will generally set the correct value * value is desired. The system will generally set the correct value
...@@ -365,7 +396,7 @@ abstract class DatabaseConnection extends PDO { ...@@ -365,7 +396,7 @@ abstract class DatabaseConnection extends PDO {
} }
/** /**
* Preprocess the prefixes used by this database connection. * Set the list of prefixes used by this database connection.
* *
* @param $prefix * @param $prefix
* The prefixes, in any of the multiple forms documented in * The prefixes, in any of the multiple forms documented in
...@@ -373,14 +404,27 @@ abstract class DatabaseConnection extends PDO { ...@@ -373,14 +404,27 @@ abstract class DatabaseConnection extends PDO {
*/ */
protected function setPrefix($prefix) { protected function setPrefix($prefix) {
if (is_array($prefix)) { if (is_array($prefix)) {
$this->defaultPrefix = isset($prefix['default']) ? $prefix['default'] : ''; $this->prefixes = $prefix + array('default' => '');
unset($prefix['default']);
$this->prefixes = $prefix;
} }
else { else {
$this->defaultPrefix = $prefix; $this->prefixes = array('default' => $prefix);
$this->prefixes = array();
} }
// Set up variables for use in prefixTables(). Replace table-specific
// prefixes first.
$this->prefixSearch = array();
$this->prefixReplace = array();
foreach ($this->prefixes as $key => $val) {
if ($key != 'default') {
$this->prefixSearch[] = '{' . $key . '}';
$this->prefixReplace[] = $val . $key;
}
}
// Then replace remaining tables with the default prefix.
$this->prefixSearch[] = '{';
$this->prefixReplace[] = $this->prefixes['default'];
$this->prefixSearch[] = '}';
$this->prefixReplace[] = '';
} }
/** /**
...@@ -398,12 +442,7 @@ abstract class DatabaseConnection extends PDO { ...@@ -398,12 +442,7 @@ abstract class DatabaseConnection extends PDO {
* The properly-prefixed string. * The properly-prefixed string.
*/ */
public function prefixTables($sql) { public function prefixTables($sql) {
// Replace specific table prefixes first. return str_replace($this->prefixSearch, $this->prefixReplace, $sql);
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 , '}' => ''));
} }
/** /**
...@@ -417,7 +456,7 @@ abstract class DatabaseConnection extends PDO { ...@@ -417,7 +456,7 @@ abstract class DatabaseConnection extends PDO {
return $this->prefixes[$table]; return $this->prefixes[$table];
} }
else { else {
return $this->defaultPrefix; return $this->prefixes['default'];
} }
} }
...@@ -469,6 +508,28 @@ abstract class DatabaseConnection extends PDO { ...@@ -469,6 +508,28 @@ abstract class DatabaseConnection extends PDO {
return $this->target; 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. * Associates a logging object with this connection.
* *
...@@ -508,6 +569,63 @@ abstract class DatabaseConnection extends PDO { ...@@ -508,6 +569,63 @@ abstract class DatabaseConnection extends PDO {
return $this->prefixTables('{' . $table . '}_' . $field . '_seq'); 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. * Executes a query string against the database.
* *
...@@ -648,13 +766,20 @@ abstract class DatabaseConnection extends PDO { ...@@ -648,13 +766,20 @@ abstract class DatabaseConnection extends PDO {
* *
* @param string $class * @param string $class
* The class for which we want the potentially driver-specific 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 * @return string
* The name of the class that should be used for this driver. * The name of the class that should be used for this driver.
*/ */
public function getDriverClass($class) { public function getDriverClass($class, array $files = array(), $use_autoload = FALSE) {
if (empty($this->driverClasses[$class])) { if (empty($this->driverClasses[$class])) {
$this->driverClasses[$class] = $class . '_' . $this->driver(); $driver = $this->driver();
if (!class_exists($this->driverClasses[$class])) { $this->driverClasses[$class] = $class . '_' . $driver;
Database::loadDriverFile($driver, $files);
if (!class_exists($this->driverClasses[$class], $use_autoload)) {
$this->driverClasses[$class] = $class; $this->driverClasses[$class] = $class;
} }
} }
...@@ -662,7 +787,7 @@ abstract class DatabaseConnection extends PDO { ...@@ -662,7 +787,7 @@ abstract class DatabaseConnection extends PDO {
} }
/** /**
* Prepares and returns a SELECT query object with the specified ID. * Prepares and returns a SELECT query object.
* *
* @param $table * @param $table
* The base table for this query, that is, the first table in the FROM * The base table for this query, that is, the first table in the FROM
...@@ -681,12 +806,12 @@ abstract class DatabaseConnection extends PDO { ...@@ -681,12 +806,12 @@ abstract class DatabaseConnection extends PDO {
* @see SelectQuery * @see SelectQuery
*/ */
public function select($table, $alias = NULL, array $options = array()) { public function select($table, $alias = NULL, array $options = array()) {
$class = $this->getDriverClass('SelectQuery'); $class = $this->getDriverClass('SelectQuery', array('query.inc', 'select.inc'));
return new $class($table, $alias, $this, $options); return new $class($table, $alias, $this, $options);
} }
/** /**
* Prepares and returns an INSERT query object with the specified ID. * Prepares and returns an INSERT query object.
* *
* @param $options * @param $options
* An array of options on the query. * An array of options on the query.
...@@ -697,12 +822,12 @@ abstract class DatabaseConnection extends PDO { ...@@ -697,12 +822,12 @@ abstract class DatabaseConnection extends PDO {
* @see InsertQuery * @see InsertQuery
*/ */
public function insert($table, array $options = array()) { public function insert($table, array $options = array()) {
$class = $this->getDriverClass('InsertQuery'); $class = $this->getDriverClass('InsertQuery', array('query.inc'));
return new $class($this, $table, $options); return new $class($this, $table, $options);
} }
/** /**
* Prepares and returns a MERGE query object with the specified ID. * Prepares and returns a MERGE query object.
* *
* @param $options * @param $options
* An array of options on the query. * An array of options on the query.
...@@ -713,13 +838,13 @@ abstract class DatabaseConnection extends PDO { ...@@ -713,13 +838,13 @@ abstract class DatabaseConnection extends PDO {
* @see MergeQuery * @see MergeQuery
*/ */
public function merge($table, array $options = array()) { public function merge($table, array $options = array()) {
$class = $this->getDriverClass('MergeQuery'); $class = $this->getDriverClass('MergeQuery', array('query.inc'));
return new $class($this, $table, $options); return new $class($this, $table, $options);
} }
/** /**
* Prepares and returns an UPDATE query object with the specified ID. * Prepares and returns an UPDATE query object.
* *
* @param $options * @param $options
* An array of options on the query. * An array of options on the query.
...@@ -730,12 +855,12 @@ abstract class DatabaseConnection extends PDO { ...@@ -730,12 +855,12 @@ abstract class DatabaseConnection extends PDO {
* @see UpdateQuery * @see UpdateQuery
*/ */
public function update($table, array $options = array()) { public function update($table, array $options = array()) {
$class = $this->getDriverClass('UpdateQuery'); $class = $this->getDriverClass('UpdateQuery', array('query.inc'));
return new $class($this, $table, $options); return new $class($this, $table, $options);
} }
/** /**
* Prepares and returns a DELETE query object with the specified ID. * Prepares and returns a DELETE query object.
* *
* @param $options * @param $options
* An array of options on the query. * An array of options on the query.
...@@ -746,7 +871,7 @@ abstract class DatabaseConnection extends PDO { ...@@ -746,7 +871,7 @@ abstract class DatabaseConnection extends PDO {
* @see DeleteQuery * @see DeleteQuery
*/ */
public function delete($table, array $options = array()) { public function delete($table, array $options = array()) {
$class = $this->getDriverClass('DeleteQuery'); $class = $this->getDriverClass('DeleteQuery', array('query.inc'));
return new $class($this, $table, $options); return new $class($this, $table, $options);
} }
...@@ -762,7 +887,7 @@ abstract class DatabaseConnection extends PDO { ...@@ -762,7 +887,7 @@ abstract class DatabaseConnection extends PDO {
* @see TruncateQuery * @see TruncateQuery
*/ */
public function truncate($table, array $options = array()) { public function truncate($table, array $options = array()) {
$class = $this->getDriverClass('TruncateQuery'); $class = $this->getDriverClass('TruncateQuery', array('query.inc'));
return new $class($this, $table, $options); return new $class($this, $table, $options);
} }
...@@ -776,7 +901,7 @@ abstract class DatabaseConnection extends PDO { ...@@ -776,7 +901,7 @@ abstract class DatabaseConnection extends PDO {
*/ */
public function schema() { public function schema() {
if (empty($this->schema)) { if (empty($this->schema)) {
$class = $this->getDriverClass('DatabaseSchema'); $class = $this->getDriverClass('DatabaseSchema', array('schema.inc'));
if (class_exists($class)) { if (class_exists($class)) {
$this->schema = new $class($this); $this->schema = new $class($this);
} }
...@@ -879,6 +1004,9 @@ abstract class DatabaseConnection extends PDO { ...@@ -879,6 +1004,9 @@ abstract class DatabaseConnection extends PDO {
* @param $name * @param $name
* Optional name of the savepoint. * Optional name of the savepoint.
* *
* @return DatabaseTransaction
* A DatabaseTransaction object.
*
* @see DatabaseTransaction * @see DatabaseTransaction
*/ */
public function startTransaction($name = '') { public function startTransaction($name = '') {
...@@ -907,13 +1035,15 @@ abstract class DatabaseConnection extends PDO { ...@@ -907,13 +1035,15 @@ abstract class DatabaseConnection extends PDO {
throw new DatabaseTransactionNoActiveException(); throw new DatabaseTransactionNoActiveException();
} }
// A previous rollback to an earlier savepoint may mean that the savepoint // A previous rollback to an earlier savepoint may mean that the savepoint
// in question has already been rolled back. // in question has already been accidentally committed.
if (!in_array($savepoint_name, $this->transactionLayers)) { if (!isset($this->transactionLayers[$savepoint_name])) {
return; throw new DatabaseTransactionNoActiveException();
} }
// We need to find the point we're rolling back to, all other savepoints // We need to find the point we're rolling back to, all other savepoints
// before are no longer needed. // before are no longer needed. If we rolled back other active savepoints,
// we need to throw an exception.
$rolled_back_other_active_savepoints = FALSE;
while ($savepoint = array_pop($this->transactionLayers)) { while ($savepoint = array_pop($this->transactionLayers)) {
if ($savepoint == $savepoint_name) { if ($savepoint == $savepoint_name) {
// If it is the last the transaction in the stack, then it is not a // If it is the last the transaction in the stack, then it is not a
...@@ -923,10 +1053,20 @@ abstract class DatabaseConnection extends PDO { ...@@ -923,10 +1053,20 @@ abstract class DatabaseConnection extends PDO {
break; break;
} }
$this->query('ROLLBACK TO SAVEPOINT ' . $savepoint); $this->query('ROLLBACK TO SAVEPOINT ' . $savepoint);
$this->popCommittableTransactions();
if ($rolled_back_other_active_savepoints) {
throw new DatabaseTransactionOutOfOrderException();
}
return; return;
} }
else {
$rolled_back_other_active_savepoints = TRUE;
}
} }
parent::rollBack(); parent::rollBack();
if ($rolled_back_other_active_savepoints) {
throw new DatabaseTransactionOutOfOrderException();
}
} }
/** /**
...@@ -975,15 +1115,32 @@ abstract class DatabaseConnection extends PDO { ...@@ -975,15 +1115,32 @@ abstract class DatabaseConnection extends PDO {
if (!$this->supportsTransactions()) { if (!$this->supportsTransactions()) {
return; return;
} }
if (!$this->inTransaction()) { // The transaction has already been committed earlier. There is nothing we
throw new DatabaseTransactionNoActiveException(); // need to do. If this transaction was part of an earlier out-of-order
// rollback, an exception would already have been thrown by
// Database::rollback().
if (!isset($this->transactionLayers[$name])) {
return;
} }
// Commit everything since SAVEPOINT $name. // Mark this layer as committable.
while($savepoint = array_pop($this->transactionLayers)) { $this->transactionLayers[$name] = FALSE;
if ($savepoint != $name) continue; $this->popCommittableTransactions();
}
/**
* Internal function: commit all the transaction layers that can commit.
*/
protected function popCommittableTransactions() {
// Commit all the committable layers.
foreach (array_reverse($this->transactionLayers) as $name => $active) {
// Stop once we found an active transaction.
if ($active) {
break;
}
// If there are no more layers left then we should commit. // If there are no more layers left then we should commit.
unset($this->transactionLayers[$name]);
if (empty($this->transactionLayers)) { if (empty($this->transactionLayers)) {
if (!parent::commit()) { if (!parent::commit()) {
throw new DatabaseTransactionCommitFailedException(); throw new DatabaseTransactionCommitFailedException();
...@@ -991,7 +1148,6 @@ abstract class DatabaseConnection extends PDO { ...@@ -991,7 +1148,6 @@ abstract class DatabaseConnection extends PDO {
} }
else { else {
$this->query('RELEASE SAVEPOINT ' . $name); $this->query('RELEASE SAVEPOINT ' . $name);
break;
} }
} }
} }
...@@ -1097,7 +1253,7 @@ abstract class DatabaseConnection extends PDO { ...@@ -1097,7 +1253,7 @@ abstract class DatabaseConnection extends PDO {
} }
/** /**
* Returns the type of the database being accessed. * Returns the name of the PDO driver for this connection.
*/ */
abstract public function databaseType(); abstract public function databaseType();
...@@ -1293,9 +1449,6 @@ abstract class Database { ...@@ -1293,9 +1449,6 @@ abstract class Database {
/** /**
* Gets the connection object for the specified database key and target. * 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 * @param $target
* The database target name. * The database target name.
* @param $key * @param $key
...@@ -1490,8 +1643,8 @@ abstract class Database { ...@@ -1490,8 +1643,8 @@ abstract class Database {
*/ */
final public static function removeConnection($key) { final public static function removeConnection($key) {
if (isset(self::$databaseInfo[$key])) { if (isset(self::$databaseInfo[$key])) {
self::closeConnection(NULL, $key);
unset(self::$databaseInfo[$key]); unset(self::$databaseInfo[$key]);
unset(self::$connections[$key]);
return TRUE; return TRUE;
} }
else { else {
...@@ -1532,6 +1685,7 @@ abstract class Database { ...@@ -1532,6 +1685,7 @@ abstract class Database {
require_once DRUPAL_ROOT . '/includes/database/' . $driver . '/database.inc'; require_once DRUPAL_ROOT . '/includes/database/' . $driver . '/database.inc';
$new_connection = new $driver_class(self::$databaseInfo[$key][$target]); $new_connection = new $driver_class(self::$databaseInfo[$key][$target]);
$new_connection->setTarget($target); $new_connection->setTarget($target);
$new_connection->setKey($key);
// If we have any active logging objects for this connection key, we need // If we have any active logging objects for this connection key, we need
// to associate them with the connection we just opened. // to associate them with the connection we just opened.
...@@ -1556,11 +1710,24 @@ abstract class Database { ...@@ -1556,11 +1710,24 @@ abstract class Database {
if (!isset($key)) { if (!isset($key)) {
$key = self::$activeKey; $key = self::$activeKey;
} }
// To close the connection, we need to unset the static variable. // To close a connection, it needs to be set to NULL and removed from the
// static variable. In all cases, closeConnection() might be called for a
// connection that was not opened yet, in which case the key is not defined
// yet and we just ensure that the connection key is undefined.
if (isset($target)) { if (isset($target)) {
if (isset(self::$connections[$key][$target])) {
self::$connections[$key][$target]->destroy();
self::$connections[$key][$target] = NULL;
}
unset(self::$connections[$key][$target]); unset(self::$connections[$key][$target]);
} }
else { else {
if (isset(self::$connections[$key])) {
foreach (self::$connections[$key] as $target => $connection) {
self::$connections[$key][$target]->destroy();
self::$connections[$key][$target] = NULL;
}
}
unset(self::$connections[$key]); unset(self::$connections[$key]);
} }
} }
...@@ -1581,6 +1748,34 @@ abstract class Database { ...@@ -1581,6 +1748,34 @@ abstract class Database {
self::$ignoreTargets[$key][$target] = TRUE; 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;
}
}
}
}
} }
/** /**
...@@ -1606,6 +1801,11 @@ class DatabaseTransactionCommitFailedException extends Exception { } ...@@ -1606,6 +1801,11 @@ class DatabaseTransactionCommitFailedException extends Exception { }
*/ */
class DatabaseTransactionExplicitCommitNotAllowedException extends Exception { } class DatabaseTransactionExplicitCommitNotAllowedException extends Exception { }
/**
* Exception thrown when a rollback() resulted in other active transactions being rolled-back.
*/
class DatabaseTransactionOutOfOrderException extends Exception { }
/** /**
* Exception thrown for merge queries that do not make semantic sense. * Exception thrown for merge queries that do not make semantic sense.
* *
...@@ -1681,8 +1881,8 @@ class DatabaseTransaction { ...@@ -1681,8 +1881,8 @@ class DatabaseTransaction {
*/ */
protected $name; protected $name;
public function __construct(DatabaseConnection &$connection, $name = NULL) { public function __construct(DatabaseConnection $connection, $name = NULL) {
$this->connection = &$connection; $this->connection = $connection;
// If there is no transaction depth, then no transaction has started. Name // If there is no transaction depth, then no transaction has started. Name
// the transaction 'drupal_transaction'. // the transaction 'drupal_transaction'.
if (!$depth = $connection->transactionDepth()) { if (!$depth = $connection->transactionDepth()) {
...@@ -1701,7 +1901,7 @@ class DatabaseTransaction { ...@@ -1701,7 +1901,7 @@ class DatabaseTransaction {
public function __destruct() { public function __destruct() {
// If we rolled back then the transaction would have already been popped. // If we rolled back then the transaction would have already been popped.
if ($this->connection->inTransaction() && !$this->rolledBack) { if (!$this->rolledBack) {
$this->connection->popTransaction($this->name); $this->connection->popTransaction($this->name);
} }
} }
...@@ -1732,21 +1932,19 @@ class DatabaseTransaction { ...@@ -1732,21 +1932,19 @@ class DatabaseTransaction {
} }
/** /**
* A prepared statement. * Represents a prepared statement.
* *
* Some methods in that class are purposely commented out. Due to a change in * Some methods in that class are purposefully commented out. Due to a change in
* how PHP defines PDOStatement, we can't define a signature for those methods * 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 * that will work the same way between versions older than 5.2.6 and later
* versions. * versions. See http://bugs.php.net/bug.php?id=42452 for more details.
*
* Please refer to http://bugs.php.net/bug.php?id=42452 for more details.
* *
* Child implementations should either extend PDOStatement: * Child implementations should either extend PDOStatement:
* @code * @code
* class DatabaseStatement_oracle extends PDOStatement implements DatabaseStatementInterface {} * class DatabaseStatement_oracle extends PDOStatement implements DatabaseStatementInterface {}
* @endcode * @endcode
* or implement their own class, but in that case they will also have to * or define their own class. If defining their own class, they will also have
* implement the Iterator or IteratorArray interfaces before * to implement either the Iterator or IteratorAggregate interface before
* DatabaseStatementInterface: * DatabaseStatementInterface:
* @code * @code
* class DatabaseStatement_oracle implements Iterator, DatabaseStatementInterface {} * class DatabaseStatement_oracle implements Iterator, DatabaseStatementInterface {}
...@@ -1830,7 +2028,7 @@ interface DatabaseStatementInterface extends Traversable { ...@@ -1830,7 +2028,7 @@ interface DatabaseStatementInterface extends Traversable {
* The numeric index of the field to return. Defaults to the first field. * The numeric index of the field to return. Defaults to the first field.
* *
* @return * @return
* A single field from the next record. * A single field from the next record, or FALSE if there is no next record.
*/ */
public function fetchField($index = 0); public function fetchField($index = 0);
...@@ -1850,7 +2048,7 @@ interface DatabaseStatementInterface extends Traversable { ...@@ -1850,7 +2048,7 @@ interface DatabaseStatementInterface extends Traversable {
* helper method, so one is added. * helper method, so one is added.
* *
* @return * @return
* An associative array. * An associative array, or FALSE if there is no next row.
*/ */
public function fetchAssoc(); public function fetchAssoc();
...@@ -1878,7 +2076,7 @@ interface DatabaseStatementInterface extends Traversable { ...@@ -1878,7 +2076,7 @@ interface DatabaseStatementInterface extends Traversable {
* The index of the column number to fetch. * The index of the column number to fetch.
* *
* @return * @return
* An indexed array. * An indexed array, or an empty array if there is no result set.
*/ */
public function fetchCol($index = 0); public function fetchCol($index = 0);
...@@ -1898,7 +2096,7 @@ interface DatabaseStatementInterface extends Traversable { ...@@ -1898,7 +2096,7 @@ interface DatabaseStatementInterface extends Traversable {
* The numeric index of the field to use as the array value. * The numeric index of the field to use as the array value.
* *
* @return * @return
* An associative array. * An associative array, or an empty array if there is no result set.
*/ */
public function fetchAllKeyed($key_index = 0, $value_index = 1); public function fetchAllKeyed($key_index = 0, $value_index = 1);
...@@ -1917,7 +2115,7 @@ interface DatabaseStatementInterface extends Traversable { ...@@ -1917,7 +2115,7 @@ interface DatabaseStatementInterface extends Traversable {
* set for the query will be used. * set for the query will be used.
* *
* @return * @return
* An associative array. * An associative array, or an empty array if there is no result set.
*/ */
public function fetchAllAssoc($key, $fetch = NULL); public function fetchAllAssoc($key, $fetch = NULL);
} }
...@@ -2108,82 +2306,6 @@ class DatabaseStatementEmpty implements Iterator, DatabaseStatementInterface { ...@@ -2108,82 +2306,6 @@ class DatabaseStatementEmpty implements Iterator, DatabaseStatementInterface {
} }
} }
/**
* Autoload callback for the database system.
*/
function db_autoload($class) {
static $base_path = '';
static $checked = array();
static $files = array(
'query.inc' => array(
'QueryPlaceholderInterface',
'QueryConditionInterface', 'DatabaseCondition',
'Query', 'DeleteQuery', 'InsertQuery', 'UpdateQuery', 'MergeQuery', 'TruncateQuery',
'QueryAlterableInterface',
),
'select.inc' => array('QueryAlterableInterface', 'SelectQueryInterface', 'SelectQuery', 'SelectQueryExtender'),
'database.inc' => array('DatabaseConnection'),
'log.inc' => array('DatabaseLog'),
'prefetch.inc' => array('DatabaseStatementPrefetch'),
'schema.inc' => array('DatabaseSchema'),
);
// If a class doesn't exist, it may get checked a second time
// by class_exists(). If so, just bail out now.
if (isset($checked[$class])) {
return;
}
$checked[$class] = TRUE;
if (empty($base_path)) {
$base_path = dirname(realpath(__FILE__));
}
// If there is an underscore in the class name, we know it's a
// driver-specific file so check for those. If not, it's a generic.
// Note that we use require_once here instead of require because of a
// quirk in class_exists(). By default, class_exists() will try to
// autoload a class if it's not found. However, we cannot tell
// at this point whether or not the class is going to exist, only
// the file that it would be in if it does exist. That means we may
// try to include a file that was already included by another
// autoload call, which would break. Using require_once() neatly
// avoids that issue.
if (strpos($class, '_') !== FALSE) {
list($base, $driver) = explode('_', $class);
// Drivers have an extra file, and may put their SelectQuery implementation
// in the main query file since it's so small.
$driver_files = $files;
$driver_files['query.inc'][] = 'SelectQuery';
$driver_files['install.inc'] = array('DatabaseTasks');
foreach ($driver_files as $file => $classes) {
if (in_array($base, $classes)) {
$filename = "{$base_path}/{$driver}/{$file}";
// We might end up looking in a file that doesn't exist, so check that.
if (file_exists($filename)) {
require_once $filename;
// If the class now exists, we're done. Otherwise keep searching in
// additional files.
if (class_exists($class, FALSE) || interface_exists($class, FALSE)) {
return;
}
}
}
}
}
else {
foreach ($files as $file => $classes) {
if (in_array($class, $classes)) {
require_once $base_path . '/' . $file;
return;
}
}
}
}
/** /**
* The following utility functions are simply convenience wrappers. * The following utility functions are simply convenience wrappers.
* *
...@@ -2579,7 +2701,7 @@ function db_condition($conjunction) { ...@@ -2579,7 +2701,7 @@ function db_condition($conjunction) {
/** /**
* @ingroup schemaapi * @addtogroup schemaapi
* @{ * @{
*/ */
...@@ -2676,7 +2798,7 @@ function _db_create_keys_sql($spec) { ...@@ -2676,7 +2798,7 @@ function _db_create_keys_sql($spec) {
* Renames a table. * Renames a table.
* *
* @param $table * @param $table
* The table to be renamed. * The current name of the table to be renamed.
* @param $new_name * @param $new_name
* The new name for the table. * The new name for the table.
*/ */
...@@ -2896,7 +3018,7 @@ function db_change_field($table, $field, $field_new, $spec, $keys_new = array()) ...@@ -2896,7 +3018,7 @@ function db_change_field($table, $field, $field_new, $spec, $keys_new = array())
} }
/** /**
* @} End of "ingroup schemaapi". * @} End of "addtogroup schemaapi".
*/ */
/** /**
...@@ -2915,4 +3037,3 @@ function db_ignore_slave() { ...@@ -2915,4 +3037,3 @@ function db_ignore_slave() {
$_SESSION['ignore_slave_server'] = REQUEST_TIME + $duration; $_SESSION['ignore_slave_server'] = REQUEST_TIME + $duration;
} }
} }
<?php <?php
// $Id: log.inc,v 1.8 2010/03/31 15:09:21 dries Exp $
/** /**
* @file * @file
...@@ -129,9 +128,10 @@ class DatabaseLog { ...@@ -129,9 +128,10 @@ class DatabaseLog {
* Determine the routine that called this query. * Determine the routine that called this query.
* *
* We define "the routine that called this query" as the first entry in * We define "the routine that called this query" as the first entry in
* the call stack that is not inside includes/database. That makes the * the call stack that is not inside includes/database and does have a file
* climbing logic very simple, and handles the variable stack depth caused * (which excludes call_user_func_array(), anonymous functions and similar).
* by the query builders. * That makes the climbing logic very simple, and handles the variable stack
* depth caused by the query builders.
* *
* @link http://www.php.net/debug_backtrace * @link http://www.php.net/debug_backtrace
* @return * @return
...@@ -145,7 +145,8 @@ class DatabaseLog { ...@@ -145,7 +145,8 @@ class DatabaseLog {
$stack = debug_backtrace(); $stack = debug_backtrace();
$stack_count = count($stack); $stack_count = count($stack);
for ($i = 0; $i < $stack_count; ++$i) { for ($i = 0; $i < $stack_count; ++$i) {
if (strpos($stack[$i]['file'], 'includes' . DIRECTORY_SEPARATOR . 'database') === FALSE) { if (!empty($stack[$i]['file']) && strpos($stack[$i]['file'], 'includes' . DIRECTORY_SEPARATOR . 'database') === FALSE) {
$stack[$i] += array('args' => array());
return array( return array(
'file' => $stack[$i]['file'], 'file' => $stack[$i]['file'],
'line' => $stack[$i]['line'], 'line' => $stack[$i]['line'],
......
<?php <?php
// $Id: database.inc,v 1.34 2010/10/03 01:29:41 dries Exp $
/** /**
* @file * @file
...@@ -7,18 +6,18 @@ ...@@ -7,18 +6,18 @@
*/ */
/** /**
* @ingroup database * @addtogroup database
* @{ * @{
*/ */
class DatabaseConnection_mysql extends DatabaseConnection { class DatabaseConnection_mysql extends DatabaseConnection {
/** /**
* Flag to indicate if we have registered the nextID cleanup function. * Flag to indicate if the cleanup function in __destruct() should run.
* *
* @var boolean * @var boolean
*/ */
protected $shutdownRegistered = FALSE; protected $needsCleanup = FALSE;
public function __construct(array $connection_options = array()) { public function __construct(array $connection_options = array()) {
// This driver defaults to transaction support, except if explicitly passed FALSE. // This driver defaults to transaction support, except if explicitly passed FALSE.
...@@ -38,14 +37,18 @@ class DatabaseConnection_mysql extends DatabaseConnection { ...@@ -38,14 +37,18 @@ class DatabaseConnection_mysql extends DatabaseConnection {
$dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . (empty($connection_options['port']) ? 3306 : $connection_options['port']); $dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . (empty($connection_options['port']) ? 3306 : $connection_options['port']);
} }
$dsn .= ';dbname=' . $connection_options['database']; $dsn .= ';dbname=' . $connection_options['database'];
parent::__construct($dsn, $connection_options['username'], $connection_options['password'], array( // Allow PDO options to be overridden.
$connection_options += array(
'pdo' => array(),
);
$connection_options['pdo'] += array(
// So we don't have to mess around with cursors and unbuffered queries by default. // So we don't have to mess around with cursors and unbuffered queries by default.
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE, PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE,
// Because MySQL's prepared statements skip the query cache, because it's dumb. // Because MySQL's prepared statements skip the query cache, because it's dumb.
PDO::ATTR_EMULATE_PREPARES => TRUE, PDO::ATTR_EMULATE_PREPARES => TRUE,
// Force column names to lower case. );
PDO::ATTR_CASE => PDO::CASE_LOWER,
)); parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
// Force MySQL to use the UTF-8 character set. Also set the collation, if a // Force MySQL to use the UTF-8 character set. Also set the collation, if a
// certain one has been set; otherwise, MySQL defaults to 'utf8_general_ci' // certain one has been set; otherwise, MySQL defaults to 'utf8_general_ci'
...@@ -57,12 +60,28 @@ class DatabaseConnection_mysql extends DatabaseConnection { ...@@ -57,12 +60,28 @@ class DatabaseConnection_mysql extends DatabaseConnection {
$this->exec('SET NAMES utf8'); $this->exec('SET NAMES utf8');
} }
// Force MySQL's behavior to conform more closely to SQL standards. // Set MySQL init_commands if not already defined. Default Drupal's MySQL
// This allows Drupal to run almost seamlessly on many different // behavior to conform more closely to SQL standards. This allows Drupal
// kinds of database systems. These settings force MySQL to behave // to run almost seamlessly on many different kinds of database systems.
// the same as postgresql, or sqlite in regards to syntax interpretation // These settings force MySQL to behave the same as postgresql, or sqlite
// and invalid data handling. See http://drupal.org/node/344575 for further discussion. // in regards to syntax interpretation and invalid data handling. See
$this->exec("SET sql_mode='ANSI,TRADITIONAL'"); // http://drupal.org/node/344575 for further discussion. Also, as MySQL 5.5
// changed the meaning of TRADITIONAL we need to spell out the modes one by
// one.
$connection_options += array(
'init_commands' => array(),
);
$connection_options['init_commands'] += array(
'sql_mode' => "SET sql_mode = 'ANSI,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER'",
);
// Set connection options.
$this->exec(implode('; ', $connection_options['init_commands']));
}
public function __destruct() {
if ($this->needsCleanup) {
$this->nextIdDelete();
}
} }
public function queryRange($query, $from, $count, array $args = array(), array $options = array()) { public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
...@@ -102,12 +121,7 @@ class DatabaseConnection_mysql extends DatabaseConnection { ...@@ -102,12 +121,7 @@ class DatabaseConnection_mysql extends DatabaseConnection {
$this->query('INSERT INTO {sequences} (value) VALUES (:value) ON DUPLICATE KEY UPDATE value = value', array(':value' => $existing_id)); $this->query('INSERT INTO {sequences} (value) VALUES (:value) ON DUPLICATE KEY UPDATE value = value', array(':value' => $existing_id));
$new_id = $this->query('INSERT INTO {sequences} () VALUES ()', array(), array('return' => Database::RETURN_INSERT_ID)); $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', array(), array('return' => Database::RETURN_INSERT_ID));
} }
if (!$this->shutdownRegistered) { $this->needsCleanup = TRUE;
// Use register_shutdown_function() here to keep the database system
// independent of Drupal.
register_shutdown_function(array($this, 'nextIdDelete'));
$shutdownRegistered = TRUE;
}
return $new_id; return $new_id;
} }
...@@ -134,9 +148,56 @@ class DatabaseConnection_mysql extends DatabaseConnection { ...@@ -134,9 +148,56 @@ class DatabaseConnection_mysql extends DatabaseConnection {
catch (PDOException $e) { catch (PDOException $e) {
} }
} }
/**
* Overridden to work around issues to MySQL not supporting transactional DDL.
*/
protected function popCommittableTransactions() {
// Commit all the committable layers.
foreach (array_reverse($this->transactionLayers) as $name => $active) {
// Stop once we found an active transaction.
if ($active) {
break;
}
// If there are no more layers left then we should commit.
unset($this->transactionLayers[$name]);
if (empty($this->transactionLayers)) {
if (!PDO::commit()) {
throw new DatabaseTransactionCommitFailedException();
}
}
else {
// Attempt to release this savepoint in the standard way.
try {
$this->query('RELEASE SAVEPOINT ' . $name);
}
catch (PDOException $e) {
// However, in MySQL (InnoDB), savepoints are automatically committed
// when tables are altered or created (DDL transactions are not
// supported). This can cause exceptions due to trying to release
// savepoints which no longer exist.
//
// To avoid exceptions when no actual error has occurred, we silently
// succeed for MySQL error code 1305 ("SAVEPOINT does not exist").
if ($e->errorInfo[1] == '1305') {
// If one SAVEPOINT was released automatically, then all were.
// Therefore, clean the transaction stack.
$this->transactionLayers = array();
// We also have to explain to PDO that the transaction stack has
// been cleaned-up.
PDO::commit();
}
else {
throw $e;
}
}
}
}
}
} }
/** /**
* @} End of "ingroup database". * @} End of "addtogroup database".
*/ */
<?php <?php
// $Id: install.inc,v 1.4 2010/07/30 01:59:14 dries Exp $
/** /**
* @file * @file
...@@ -10,7 +9,6 @@ ...@@ -10,7 +9,6 @@
* Specifies installation tasks for MySQL and equivalent databases. * Specifies installation tasks for MySQL and equivalent databases.
*/ */
class DatabaseTasks_mysql extends DatabaseTasks { class DatabaseTasks_mysql extends DatabaseTasks {
/** /**
* The PDO driver name for MySQL and equivalent databases. * The PDO driver name for MySQL and equivalent databases.
* *
...@@ -22,7 +20,14 @@ class DatabaseTasks_mysql extends DatabaseTasks { ...@@ -22,7 +20,14 @@ class DatabaseTasks_mysql extends DatabaseTasks {
* Returns a human-readable name string for MySQL and equivalent databases. * Returns a human-readable name string for MySQL and equivalent databases.
*/ */
public function name() { public function name() {
return 'MySQL, MariaDB, or equivalent'; return st('MySQL, MariaDB, or equivalent');
}
/**
* Returns the minimum version for MySQL.
*/
public function minimumVersion() {
return '5.0.15';
} }
} }
<?php <?php
// $Id: query.inc,v 1.18 2010/06/26 01:40:05 dries Exp $
/** /**
* @ingroup database * @addtogroup database
* @{ * @{
*/ */
...@@ -43,8 +42,8 @@ class InsertQuery_mysql extends InsertQuery { ...@@ -43,8 +42,8 @@ class InsertQuery_mysql extends InsertQuery {
} }
public function __toString() { public function __toString() {
// Create a comments string to prepend to the query. // Create a sanitized comment string to prepend to the query.
$comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; $comments = $this->connection->makeComment($this->comments);
// Default fields are always placed first for consistency. // Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields); $insert_fields = array_merge($this->defaultFields, $this->insertFields);
...@@ -93,8 +92,8 @@ class TruncateQuery_mysql extends TruncateQuery { ...@@ -93,8 +92,8 @@ class TruncateQuery_mysql extends TruncateQuery {
// not transactional, and result in an implicit COMMIT. When we are in a // not transactional, and result in an implicit COMMIT. When we are in a
// transaction, fallback to the slower, but transactional, DELETE. // transaction, fallback to the slower, but transactional, DELETE.
if ($this->connection->inTransaction()) { if ($this->connection->inTransaction()) {
// Create a comments string to prepend to the query. // Create a comment string to prepend to the query.
$comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; $comments = $this->connection->makeComment($this->comments);
return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '}'; return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '}';
} }
else { else {
...@@ -104,5 +103,5 @@ class TruncateQuery_mysql extends TruncateQuery { ...@@ -104,5 +103,5 @@ class TruncateQuery_mysql extends TruncateQuery {
} }
/** /**
* @} End of "ingroup database". * @} End of "addtogroup database".
*/ */
<?php <?php
// $Id: schema.inc,v 1.43 2010/10/22 15:18:56 webchick Exp $
/** /**
* @file * @file
...@@ -8,7 +7,7 @@ ...@@ -8,7 +7,7 @@
/** /**
* @ingroup schemaapi * @addtogroup schemaapi
* @{ * @{
*/ */
...@@ -132,9 +131,14 @@ class DatabaseSchema_mysql extends DatabaseSchema { ...@@ -132,9 +131,14 @@ class DatabaseSchema_mysql extends DatabaseSchema {
protected function createFieldSql($name, $spec) { protected function createFieldSql($name, $spec) {
$sql = "`" . $name . "` " . $spec['mysql_type']; $sql = "`" . $name . "` " . $spec['mysql_type'];
if (in_array($spec['mysql_type'], array('VARCHAR', 'CHAR', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT', 'TEXT')) && isset($spec['length'])) { if (in_array($spec['mysql_type'], array('VARCHAR', 'CHAR', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT', 'TEXT'))) {
if (isset($spec['length'])) {
$sql .= '(' . $spec['length'] . ')'; $sql .= '(' . $spec['length'] . ')';
} }
if (!empty($spec['binary'])) {
$sql .= ' BINARY';
}
}
elseif (isset($spec['precision']) && isset($spec['scale'])) { elseif (isset($spec['precision']) && isset($spec['scale'])) {
$sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')'; $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
} }
...@@ -337,7 +341,7 @@ class DatabaseSchema_mysql extends DatabaseSchema { ...@@ -337,7 +341,7 @@ class DatabaseSchema_mysql extends DatabaseSchema {
$this->connection->query($query); $this->connection->query($query);
if (isset($spec['initial'])) { if (isset($spec['initial'])) {
$this->connection->update($table) $this->connection->update($table)
->fields(array($field, $spec['initial'])) ->fields(array($field => $spec['initial']))
->execute(); ->execute();
} }
if ($fixnull) { if ($fixnull) {
...@@ -382,7 +386,7 @@ class DatabaseSchema_mysql extends DatabaseSchema { ...@@ -382,7 +386,7 @@ class DatabaseSchema_mysql extends DatabaseSchema {
// Returns one row for each column in the index. Result is string or FALSE. // Returns one row for each column in the index. Result is string or FALSE.
// Details at http://dev.mysql.com/doc/refman/5.0/en/show-index.html // Details at http://dev.mysql.com/doc/refman/5.0/en/show-index.html
$row = $this->connection->query('SHOW INDEX FROM {' . $table . "} WHERE key_name = '$name'")->fetchAssoc(); $row = $this->connection->query('SHOW INDEX FROM {' . $table . "} WHERE key_name = '$name'")->fetchAssoc();
return isset($row['key_name']); return isset($row['Key_name']);
} }
public function addPrimaryKey($table, $fields) { public function addPrimaryKey($table, $fields) {
...@@ -528,5 +532,5 @@ class DatabaseSchema_mysql extends DatabaseSchema { ...@@ -528,5 +532,5 @@ class DatabaseSchema_mysql extends DatabaseSchema {
} }
/** /**
* @} End of "ingroup schemaapi". * @} End of "addtogroup schemaapi".
*/ */
<?php <?php
// $Id: database.inc,v 1.43 2010/10/03 01:29:41 dries Exp $
/** /**
* @file * @file
...@@ -7,7 +6,7 @@ ...@@ -7,7 +6,7 @@
*/ */
/** /**
* @ingroup database * @addtogroup database
* @{ * @{
*/ */
...@@ -35,11 +34,25 @@ class DatabaseConnection_pgsql extends DatabaseConnection { ...@@ -35,11 +34,25 @@ class DatabaseConnection_pgsql extends DatabaseConnection {
if (empty($connection_options['password'])) { if (empty($connection_options['password'])) {
$connection_options['password'] = NULL; $connection_options['password'] = NULL;
} }
// If the password contains a backslash it is treated as an escape character
// http://bugs.php.net/bug.php?id=53217
// so backslashes in the password need to be doubled up.
// The bug was reported against pdo_pgsql 1.0.2, backslashes in passwords
// will break on this doubling up when the bug is fixed, so check the version
//elseif (phpversion('pdo_pgsql') < 'version_this_was_fixed_in') {
else {
$connection_options['password'] = str_replace('\\', '\\\\', $connection_options['password']);
}
$this->connectionOptions = $connection_options; $this->connectionOptions = $connection_options;
$dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port']; $dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port'];
parent::__construct($dsn, $connection_options['username'], $connection_options['password'], array(
// Allow PDO options to be overridden.
$connection_options += array(
'pdo' => array(),
);
$connection_options['pdo'] += array(
// Prepared statements are most effective for performance when queries // Prepared statements are most effective for performance when queries
// are recycled (used several times). However, if they are not re-used, // are recycled (used several times). However, if they are not re-used,
// prepared statements become ineffecient. Since most of Drupal's // prepared statements become ineffecient. Since most of Drupal's
...@@ -49,12 +62,27 @@ class DatabaseConnection_pgsql extends DatabaseConnection { ...@@ -49,12 +62,27 @@ class DatabaseConnection_pgsql extends DatabaseConnection {
PDO::ATTR_EMULATE_PREPARES => TRUE, PDO::ATTR_EMULATE_PREPARES => TRUE,
// Convert numeric values to strings when fetching. // Convert numeric values to strings when fetching.
PDO::ATTR_STRINGIFY_FETCHES => TRUE, PDO::ATTR_STRINGIFY_FETCHES => TRUE,
// Force column names to lower case. );
PDO::ATTR_CASE => PDO::CASE_LOWER, parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
));
// Force PostgreSQL to use the UTF-8 character set by default. // Force PostgreSQL to use the UTF-8 character set by default.
$this->exec("SET NAMES 'UTF8'"); $this->exec("SET NAMES 'UTF8'");
// Execute PostgreSQL init_commands.
if (isset($connection_options['init_commands'])) {
$this->exec(implode('; ', $connection_options['init_commands']));
}
}
public function prepareQuery($query) {
// mapConditionOperator converts LIKE operations to ILIKE for consistency
// with MySQL. However, Postgres does not support ILIKE on bytea (blobs)
// fields.
// To make the ILIKE operator work, we type-cast bytea fields into text.
// @todo This workaround only affects bytea fields, but the involved field
// types involved in the query are unknown, so there is no way to
// conditionally execute this for affected queries only.
return parent::prepareQuery(preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE) /i', ' ${1}::text ${2} ', $query));
} }
public function query($query, array $args = array(), $options = array()) { public function query($query, array $args = array(), $options = array()) {
...@@ -137,10 +165,9 @@ class DatabaseConnection_pgsql extends DatabaseConnection { ...@@ -137,10 +165,9 @@ class DatabaseConnection_pgsql extends DatabaseConnection {
if (!isset($specials)) { if (!isset($specials)) {
$specials = array( $specials = array(
// In PostgreSQL, 'LIKE' is case-sensitive. For case-insensitive LIKE // In PostgreSQL, 'LIKE' is case-sensitive. For case-insensitive LIKE
// statements, we need to use ILIKE instead. Use backslash for escaping // statements, we need to use ILIKE instead.
// wildcard characters. 'LIKE' => array('operator' => 'ILIKE'),
'LIKE' => array('operator' => 'ILIKE', 'postfix' => ' ESCAPE ' . $this->quote("\\")), 'NOT LIKE' => array('operator' => 'NOT ILIKE'),
'NOT LIKE' => array('operator' => 'NOT ILIKE', 'postfix' => ' ESCAPE ' . $this->quote("\\")),
); );
} }
...@@ -192,5 +219,5 @@ class DatabaseConnection_pgsql extends DatabaseConnection { ...@@ -192,5 +219,5 @@ class DatabaseConnection_pgsql extends DatabaseConnection {
} }
/** /**
* @} End of "ingroup database". * @} End of "addtogroup database".
*/ */
<?php <?php
// $Id: install.inc,v 1.10 2010/08/16 21:01:23 dries Exp $
/** /**
* @file * @file
...@@ -21,6 +20,10 @@ class DatabaseTasks_pgsql extends DatabaseTasks { ...@@ -21,6 +20,10 @@ class DatabaseTasks_pgsql extends DatabaseTasks {
'function' => 'checkPHPVersion', 'function' => 'checkPHPVersion',
'arguments' => array(), 'arguments' => array(),
); );
$this->tasks[] = array(
'function' => 'checkBinaryOutput',
'arguments' => array(),
);
$this->tasks[] = array( $this->tasks[] = array(
'function' => 'initializeDatabase', 'function' => 'initializeDatabase',
'arguments' => array(), 'arguments' => array(),
...@@ -28,7 +31,11 @@ class DatabaseTasks_pgsql extends DatabaseTasks { ...@@ -28,7 +31,11 @@ class DatabaseTasks_pgsql extends DatabaseTasks {
} }
public function name() { public function name() {
return 'PostgreSQL'; return st('PostgreSQL');
}
public function minimumVersion() {
return '8.3';
} }
/** /**
...@@ -49,7 +56,8 @@ class DatabaseTasks_pgsql extends DatabaseTasks { ...@@ -49,7 +56,8 @@ class DatabaseTasks_pgsql extends DatabaseTasks {
$text .= 'Recreate the database with %encoding encoding. See !link for more details.'; $text .= 'Recreate the database with %encoding encoding. See !link for more details.';
$this->fail(st($text, $replacements)); $this->fail(st($text, $replacements));
} }
} catch (Exception $e) { }
catch (Exception $e) {
$this->fail(st('Drupal could not determine the encoding of the database was set to UTF-8')); $this->fail(st('Drupal could not determine the encoding of the database was set to UTF-8'));
} }
} }
...@@ -71,6 +79,60 @@ class DatabaseTasks_pgsql extends DatabaseTasks { ...@@ -71,6 +79,60 @@ class DatabaseTasks_pgsql extends DatabaseTasks {
}; };
} }
/**
* Check Binary Output.
*
* Unserializing does not work on Postgresql 9 when bytea_output is 'hex'.
*/
function checkBinaryOutput() {
// PostgreSQL < 9 doesn't support bytea_output, so verify we are running
// at least PostgreSQL 9.
$database_connection = Database::getConnection();
if (version_compare($database_connection->version(), '9') >= 0) {
if (!$this->checkBinaryOutputSuccess()) {
// First try to alter the database. If it fails, raise an error telling
// the user to do it themselves.
$connection_options = $database_connection->getConnectionOptions();
// It is safe to include the database name directly here, because this
// code is only called when a connection to the database is already
// established, thus the database name is guaranteed to be a correct
// value.
$query = "ALTER DATABASE \"" . $connection_options['database'] . "\" SET bytea_output = 'escape';";
try {
db_query($query);
}
catch (Exception $e) {
// Ignore possible errors when the user doesn't have the necessary
// privileges to ALTER the database.
}
// Close the database connection so that the configuration parameter
// is applied to the current connection.
db_close();
// Recheck, if it fails, finally just rely on the end user to do the
// right thing.
if (!$this->checkBinaryOutputSuccess()) {
$replacements = array(
'%setting' => 'bytea_output',
'%current_value' => 'hex',
'%needed_value' => 'escape',
'!query' => "<code>" . $query . "</code>",
);
$this->fail(st("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: !query", $replacements));
}
}
}
}
/**
* Verify that a binary data roundtrip returns the original string.
*/
protected function checkBinaryOutputSuccess() {
$bytea_output = db_query("SELECT 'encoding'::bytea AS output")->fetchField();
return ($bytea_output == 'encoding');
}
/** /**
* Make PostgreSQL Drupal friendly. * Make PostgreSQL Drupal friendly.
*/ */
......
<?php <?php
// $Id: query.inc,v 1.23 2010/09/24 21:24:13 webchick Exp $
/** /**
* @ingroup database * @ingroup database
...@@ -104,8 +103,8 @@ class InsertQuery_pgsql extends InsertQuery { ...@@ -104,8 +103,8 @@ class InsertQuery_pgsql extends InsertQuery {
} }
public function __toString() { public function __toString() {
// Create a comments string to prepend to the query. // Create a sanitized comment string to prepend to the query.
$comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; $comments = $this->connection->makeComment($this->comments);
// Default fields are always placed first for consistency. // Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields); $insert_fields = array_merge($this->defaultFields, $this->insertFields);
...@@ -208,95 +207,3 @@ class UpdateQuery_pgsql extends UpdateQuery { ...@@ -208,95 +207,3 @@ class UpdateQuery_pgsql extends UpdateQuery {
return $stmt->rowCount(); return $stmt->rowCount();
} }
} }
class SelectQuery_pgsql extends SelectQuery {
public function orderRandom() {
$alias = $this->addExpression('RANDOM()', 'random_field');
$this->orderBy($alias);
return $this;
}
/**
* Overrides SelectQuery::orderBy().
*
* PostgreSQL adheres strictly to the SQL-92 standard and requires that when
* using DISTINCT or GROUP BY conditions, fields and expressions that are
* ordered on also need to be selected. This is a best effort implementation
* to handle the cases that can be automated by adding the field if it is not
* yet selected.
*
* @code
* $query = db_select('node', 'n');
* $query->join('node_revision', 'nr', 'n.vid = nr.vid');
* $query
* ->distinct()
* ->fields('n')
* ->orderBy('timestamp');
* @endcode
*
* In this query, it is not possible (without relying on the schema) to know
* whether timestamp belongs to node_revisions and needs to be added or
* belongs to node and is already selected. Queries like this will need to be
* corrected in the original query by adding an explicit call to
* SelectQuery::addField() or SelectQuery::fields().
*
* Since this has a small performance impact, both by the additional
* processing in this function and in the database that needs to return the
* additional fields, this is done as an override instead of implementing it
* directly in SelectQuery::orderBy().
*/
public function orderBy($field, $direction = 'ASC') {
// Call parent function to order on this.
$return = parent::orderBy($field, $direction);
// If there is a table alias specified, split it up.
if (strpos($field, '.') !== FALSE) {
list($table, $table_field) = explode('.', $field);
}
// Figure out if the field has already been added.
foreach ($this->fields as $existing_field) {
if (!empty($table)) {
// If table alias is given, check if field and table exists.
if ($existing_field['table'] == $table && $existing_field['field'] == $table_field) {
return $return;
}
}
else {
// If there is no table, simply check if the field exists as a field or
// an aliased field.
if ($existing_field['alias'] == $field) {
return $return;
}
}
}
// Also check expression aliases.
foreach ($this->expressions as $expression) {
if ($expression['alias'] == $field) {
return $return;
}
}
// If a table loads all fields, it can not be added again. It would
// result in an ambigious alias error because that field would be loaded
// twice: Once through table_alias.* and once directly. If the field
// actually belongs to a different table, it must be added manually.
foreach ($this->tables as $table) {
if (!empty($table['all_fields'])) {
return $return;
}
}
// If $field contains an characters which are not allowed in a field name
// it is considered an expression, these can't be handeld automatically
// either.
if ($this->connection->escapeField($field) != $field) {
return $return;
}
// This is a case that can be handled automatically, add the field.
$this->addField(NULL, $field);
return $return;
}
}
<?php <?php
// $Id: schema.inc,v 1.39 2010/10/22 15:18:56 webchick Exp $
/** /**
* @file * @file
...@@ -74,6 +73,39 @@ class DatabaseSchema_pgsql extends DatabaseSchema { ...@@ -74,6 +73,39 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
return $this->tableInformation[$key]; return $this->tableInformation[$key];
} }
/**
* Fetch the list of CHECK constraints used on a field.
*
* We introspect the database to collect the information required by field
* alteration.
*
* @param $table
* The non-prefixed name of the table.
* @param $field
* The name of the field.
* @return
* An array of all the checks for the field.
*/
public function queryFieldInformation($table, $field) {
$prefixInfo = $this->getPrefixInfo($table, TRUE);
// Split the key into schema and table for querying.
$schema = $prefixInfo['schema'];
$table_name = $prefixInfo['table'];
$field_information = (object) array(
'checks' => array(),
);
$checks = $this->connection->query("SELECT conname FROM pg_class cl INNER JOIN pg_constraint co ON co.conrelid = cl.oid INNER JOIN pg_attribute attr ON attr.attrelid = cl.oid AND attr.attnum = ANY (co.conkey) INNER JOIN pg_namespace ns ON cl.relnamespace = ns.oid WHERE co.contype = 'c' AND ns.nspname = :schema AND cl.relname = :table AND attr.attname = :column", array(
':schema' => $schema,
':table' => $table_name,
':column' => $field,
));
$field_information = $checks->fetchCol();
return $field_information;
}
/** /**
* Generate SQL to create a new table from a Drupal schema definition. * Generate SQL to create a new table from a Drupal schema definition.
* *
...@@ -296,9 +328,9 @@ class DatabaseSchema_pgsql extends DatabaseSchema { ...@@ -296,9 +328,9 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
// rename them when renaming the table. // rename them when renaming the table.
$indexes = $this->connection->query('SELECT indexname FROM pg_indexes WHERE schemaname = :schema AND tablename = :table', array(':schema' => $old_schema, ':table' => $old_table_name)); $indexes = $this->connection->query('SELECT indexname FROM pg_indexes WHERE schemaname = :schema AND tablename = :table', array(':schema' => $old_schema, ':table' => $old_table_name));
foreach ($indexes as $index) { foreach ($indexes as $index) {
if (preg_match('/^' . preg_quote($old_full_name) . '_(.*)_idx$/', $index->indexname, $matches)) { if (preg_match('/^' . preg_quote($old_full_name) . '_(.*)$/', $index->indexname, $matches)) {
$index_name = $matches[1]; $index_name = $matches[1];
$this->connection->query('ALTER INDEX ' . $index->indexname . ' RENAME TO {' . $new_name . '}_' . $index_name . '_idx'); $this->connection->query('ALTER INDEX ' . $index->indexname . ' RENAME TO {' . $new_name . '}_' . $index_name);
} }
} }
...@@ -335,7 +367,7 @@ class DatabaseSchema_pgsql extends DatabaseSchema { ...@@ -335,7 +367,7 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
$this->connection->query($query); $this->connection->query($query);
if (isset($spec['initial'])) { if (isset($spec['initial'])) {
$this->connection->update($table) $this->connection->update($table)
->fields(array($field, $spec['initial'])) ->fields(array($field => $spec['initial']))
->execute(); ->execute();
} }
if ($fixnull) { if ($fixnull) {
...@@ -469,15 +501,7 @@ class DatabaseSchema_pgsql extends DatabaseSchema { ...@@ -469,15 +501,7 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
throw new DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new))); throw new DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new)));
} }
if (!array_key_exists('size', $spec)) { $spec = $this->processField($spec);
$spec['size'] = 'normal';
}
// Map type definition to the PostgreSQL type.
if (!isset($spec['pgsql_type'])) {
$map = $this->getFieldTypeMap();
$spec['pgsql_type'] = $map[$spec['type'] . ':' . $spec['size']];
}
// We need to typecast the new column to best be able to transfer the data // We need to typecast the new column to best be able to transfer the data
// Schema_pgsql::getFieldTypeMap() will return possibilities that are not // Schema_pgsql::getFieldTypeMap() will return possibilities that are not
...@@ -489,8 +513,35 @@ class DatabaseSchema_pgsql extends DatabaseSchema { ...@@ -489,8 +513,35 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
$typecast = $spec['pgsql_type']; $typecast = $spec['pgsql_type'];
} }
if (in_array($spec['pgsql_type'], array('varchar', 'character', 'text')) && isset($spec['length'])) {
$typecast .= '(' . $spec['length'] . ')';
}
elseif (isset($spec['precision']) && isset($spec['scale'])) {
$typecast .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
}
// Remove old check constraints.
$field_info = $this->queryFieldInformation($table, $field);
foreach ($field_info as $check) {
$this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $check . '"');
}
// Remove old default.
$this->fieldSetNoDefault($table, $field);
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $typecast . ' USING "' . $field . '"::' . $typecast); $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $typecast . ' USING "' . $field . '"::' . $typecast);
if (isset($spec['not null'])) {
if ($spec['not null']) {
$nullaction = 'SET NOT NULL';
}
else {
$nullaction = 'DROP NOT NULL';
}
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" ' . $nullaction);
}
if (in_array($spec['pgsql_type'], array('serial', 'bigserial'))) { if (in_array($spec['pgsql_type'], array('serial', 'bigserial'))) {
// Type "serial" is known to PostgreSQL, but *only* during table creation, // Type "serial" is known to PostgreSQL, but *only* during table creation,
// not when altering. Because of that, the sequence needs to be created // not when altering. Because of that, the sequence needs to be created
...@@ -508,6 +559,16 @@ class DatabaseSchema_pgsql extends DatabaseSchema { ...@@ -508,6 +559,16 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
$this->connection->query('ALTER TABLE {' . $table . '} RENAME "' . $field . '" TO "' . $field_new . '"'); $this->connection->query('ALTER TABLE {' . $table . '} RENAME "' . $field . '" TO "' . $field_new . '"');
} }
// Add unsigned check if necessary.
if (!empty($spec['unsigned'])) {
$this->connection->query('ALTER TABLE {' . $table . '} ADD CHECK ("' . $field_new . '" >= 0)');
}
// Add default if necessary.
if (isset($spec['default'])) {
$this->fieldSetDefault($table, $field_new, $spec['default']);
}
// Change description if necessary. // Change description if necessary.
if (!empty($spec['description'])) { if (!empty($spec['description'])) {
$this->connection->query('COMMENT ON COLUMN {' . $table . '}."' . $field_new . '" IS ' . $this->prepareComment($spec['description'])); $this->connection->query('COMMENT ON COLUMN {' . $table . '}."' . $field_new . '" IS ' . $this->prepareComment($spec['description']));
......
<?php
/**
* @file
* Select builder for PostgreSQL database engine.
*/
/**
* @addtogroup database
* @{
*/
class SelectQuery_pgsql extends SelectQuery {
public function orderRandom() {
$alias = $this->addExpression('RANDOM()', 'random_field');
$this->orderBy($alias);
return $this;
}
/**
* Overrides SelectQuery::orderBy().
*
* PostgreSQL adheres strictly to the SQL-92 standard and requires that when
* using DISTINCT or GROUP BY conditions, fields and expressions that are
* ordered on also need to be selected. This is a best effort implementation
* to handle the cases that can be automated by adding the field if it is not
* yet selected.
*
* @code
* $query = db_select('node', 'n');
* $query->join('node_revision', 'nr', 'n.vid = nr.vid');
* $query
* ->distinct()
* ->fields('n')
* ->orderBy('timestamp');
* @endcode
*
* In this query, it is not possible (without relying on the schema) to know
* whether timestamp belongs to node_revisions and needs to be added or
* belongs to node and is already selected. Queries like this will need to be
* corrected in the original query by adding an explicit call to
* SelectQuery::addField() or SelectQuery::fields().
*
* Since this has a small performance impact, both by the additional
* processing in this function and in the database that needs to return the
* additional fields, this is done as an override instead of implementing it
* directly in SelectQuery::orderBy().
*/
public function orderBy($field, $direction = 'ASC') {
// Call parent function to order on this.
$return = parent::orderBy($field, $direction);
// If there is a table alias specified, split it up.
if (strpos($field, '.') !== FALSE) {
list($table, $table_field) = explode('.', $field);
}
// Figure out if the field has already been added.
foreach ($this->fields as $existing_field) {
if (!empty($table)) {
// If table alias is given, check if field and table exists.
if ($existing_field['table'] == $table && $existing_field['field'] == $table_field) {
return $return;
}
}
else {
// If there is no table, simply check if the field exists as a field or
// an aliased field.
if ($existing_field['alias'] == $field) {
return $return;
}
}
}
// Also check expression aliases.
foreach ($this->expressions as $expression) {
if ($expression['alias'] == $field) {
return $return;
}
}
// If a table loads all fields, it can not be added again. It would
// result in an ambigious alias error because that field would be loaded
// twice: Once through table_alias.* and once directly. If the field
// actually belongs to a different table, it must be added manually.
foreach ($this->tables as $table) {
if (!empty($table['all_fields'])) {
return $return;
}
}
// If $field contains an characters which are not allowed in a field name
// it is considered an expression, these can't be handeld automatically
// either.
if ($this->connection->escapeField($field) != $field) {
return $return;
}
// This is a case that can be handled automatically, add the field.
$this->addField(NULL, $field);
return $return;
}
}
/**
* @} End of "addtogroup database".
*/
<?php <?php
// $Id: prefetch.inc,v 1.10 2010/04/30 13:47:46 dries Exp $
/** /**
* @file * @file
...@@ -10,7 +9,7 @@ ...@@ -10,7 +9,7 @@
*/ */
/** /**
* @ingroup database * @addtogroup database
* @{ * @{
*/ */
...@@ -371,7 +370,7 @@ class DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface ...@@ -371,7 +370,7 @@ class DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface
} }
} }
public function fetchField($index = 0) { public function fetchColumn($index = 0) {
if (isset($this->currentRow) && isset($this->columnNames[$index])) { if (isset($this->currentRow) && isset($this->columnNames[$index])) {
// We grab the value directly from $this->data, and format it. // We grab the value directly from $this->data, and format it.
$return = $this->currentRow[$this->columnNames[$index]]; $return = $this->currentRow[$this->columnNames[$index]];
...@@ -383,6 +382,10 @@ class DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface ...@@ -383,6 +382,10 @@ class DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface
} }
} }
public function fetchField($index = 0) {
return $this->fetchColumn($index);
}
public function fetchObject($class_name = NULL, $constructor_args = array()) { public function fetchObject($class_name = NULL, $constructor_args = array()) {
if (isset($this->currentRow)) { if (isset($this->currentRow)) {
if (!isset($class_name)) { if (!isset($class_name)) {
...@@ -499,6 +502,6 @@ class DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface ...@@ -499,6 +502,6 @@ class DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface
} }
/** /**
* @} End of "ingroup database". * @} End of "addtogroup database".
*/ */
<?php <?php
// $Id: query.inc,v 1.57 2010/10/18 00:50:36 dries Exp $
/** /**
* @ingroup database * @addtogroup database
* @{ * @{
*/ */
...@@ -17,61 +16,93 @@ ...@@ -17,61 +16,93 @@
interface QueryConditionInterface { interface QueryConditionInterface {
/** /**
* Helper function to build most common conditional clauses. * Helper function: builds the most common conditional clauses.
* *
* This method can take a variable number of parameters. If called with two * This method can take a variable number of parameters. If called with two
* parameters, they are taken as $field and $value with $operator having a value * parameters, they are taken as $field and $value with $operator having a
* of IN if $value is an array and = otherwise. * value of IN if $value is an array and = otherwise.
*
* Do not use this method to test for NULL values. Instead, use
* QueryConditionInterface::isNull() or QueryConditionInterface::isNotNull().
* *
* @param $field * @param $field
* The name of the field to check. If you would like to add a more complex * The name of the field to check. If you would like to add a more complex
* condition involving operators or functions, use where(). * condition involving operators or functions, use where().
* @param $value * @param $value
* The value to test the field against. In most cases, this is a scalar. For more * The value to test the field against. In most cases, this is a scalar.
* complex options, it is an array. The meaning of each element in the array is * For more complex options, it is an array. The meaning of each element in
* dependent on the $operator. * the array is dependent on the $operator.
* @param $operator * @param $operator
* The comparison operator, such as =, <, or >=. It also accepts more complex * The comparison operator, such as =, <, or >=. It also accepts more
* options such as IN, LIKE, or BETWEEN. Defaults to IN if $value is an array * complex options such as IN, LIKE, or BETWEEN. Defaults to IN if $value is
* = otherwise. * an array, and = otherwise.
*
* @return QueryConditionInterface * @return QueryConditionInterface
* The called object. * The called object.
*
* @see QueryConditionInterface::isNull()
* @see QueryConditionInterface::isNotNull()
*/ */
public function condition($field, $value = NULL, $operator = NULL); public function condition($field, $value = NULL, $operator = NULL);
/** /**
* Add an arbitrary WHERE clause to the query. * Adds an arbitrary WHERE clause to the query.
* *
* @param $snippet * @param $snippet
* A portion of a WHERE clause as a prepared statement. It must use named placeholders, * A portion of a WHERE clause as a prepared statement. It must use named
* not ? placeholders. * placeholders, not ? placeholders.
* @param $args * @param $args
* An associative array of arguments. * An associative array of arguments.
*
* @return QueryConditionInterface * @return QueryConditionInterface
* The called object. * The called object.
*/ */
public function where($snippet, $args = array()); public function where($snippet, $args = array());
/** /**
* Set a condition that the specified field be NULL. * Sets a condition that the specified field be NULL.
* *
* @param $field * @param $field
* The name of the field to check. * The name of the field to check.
*
* @return QueryConditionInterface * @return QueryConditionInterface
* The called object. * The called object.
*/ */
public function isNull($field); public function isNull($field);
/** /**
* Set a condition that the specified field be NOT NULL. * Sets a condition that the specified field be NOT NULL.
* *
* @param $field * @param $field
* The name of the field to check. * The name of the field to check.
*
* @return QueryConditionInterface * @return QueryConditionInterface
* The called object. * The called object.
*/ */
public function isNotNull($field); public function isNotNull($field);
/**
* Sets a condition that the specified subquery returns values.
*
* @param SelectQueryInterface $select
* The subquery that must contain results.
*
* @return QueryConditionInterface
* The called object.
*/
public function exists(SelectQueryInterface $select);
/**
* Sets a condition that the specified subquery returns no values.
*
* @param SelectQueryInterface $select
* The subquery that must not contain results.
*
* @return QueryConditionInterface
* The called object.
*/
public function notExists(SelectQueryInterface $select);
/** /**
* Gets a complete list of all conditions in this conditional clause. * Gets a complete list of all conditions in this conditional clause.
* *
...@@ -80,12 +111,13 @@ interface QueryConditionInterface { ...@@ -80,12 +111,13 @@ interface QueryConditionInterface {
* *
* The data structure that is returned is an indexed array of entries, where * The data structure that is returned is an indexed array of entries, where
* each entry looks like the following: * each entry looks like the following:
* * @code
* array( * array(
* 'field' => $field, * 'field' => $field,
* 'value' => $value, * 'value' => $value,
* 'operator' => $operator, * 'operator' => $operator,
* ); * );
* @endcode
* *
* In the special case that $operator is NULL, the $field is taken as a raw * In the special case that $operator is NULL, the $field is taken as a raw
* SQL snippet (possibly containing a function) and $value is an associative * SQL snippet (possibly containing a function) and $value is an associative
...@@ -112,11 +144,19 @@ interface QueryConditionInterface { ...@@ -112,11 +144,19 @@ interface QueryConditionInterface {
* *
* @param $connection * @param $connection
* The database connection for which to compile the conditionals. * The database connection for which to compile the conditionals.
* @param $query * @param $queryPlaceholder
* The query this condition belongs to. If not given, the current query is * The query this condition belongs to. If not given, the current query is
* used. * used.
*/ */
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder = NULL); public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder);
/**
* Check whether a condition has been previously compiled.
*
* @return
* TRUE if the condition has been previously compiled.
*/
public function compiled();
} }
...@@ -130,12 +170,13 @@ interface QueryAlterableInterface { ...@@ -130,12 +170,13 @@ interface QueryAlterableInterface {
* *
* Tags are strings that identify a query. A query may have any number of * Tags are strings that identify a query. A query may have any number of
* tags. Tags are used to mark a query so that alter hooks may decide if they * tags. Tags are used to mark a query so that alter hooks may decide if they
* wish to take action. Tags should be all lower-case and contain only letters, * wish to take action. Tags should be all lower-case and contain only
* numbers, and underscore, and start with a letter. That is, they should * letters, numbers, and underscore, and start with a letter. That is, they
* follow the same rules as PHP identifiers in general. * should follow the same rules as PHP identifiers in general.
* *
* @param $tag * @param $tag
* The tag to add. * The tag to add.
*
* @return QueryAlterableInterface * @return QueryAlterableInterface
* The called object. * The called object.
*/ */
...@@ -146,6 +187,7 @@ interface QueryAlterableInterface { ...@@ -146,6 +187,7 @@ interface QueryAlterableInterface {
* *
* @param $tag * @param $tag
* The tag to check. * The tag to check.
*
* @return * @return
* TRUE if this query has been marked with this tag, FALSE otherwise. * TRUE if this query has been marked with this tag, FALSE otherwise.
*/ */
...@@ -156,8 +198,10 @@ interface QueryAlterableInterface { ...@@ -156,8 +198,10 @@ interface QueryAlterableInterface {
* *
* @param $tags * @param $tags
* A variable number of arguments, one for each tag to check. * A variable number of arguments, one for each tag to check.
*
* @return * @return
* TRUE if this query has been marked with all specified tags, FALSE otherwise. * TRUE if this query has been marked with all specified tags, FALSE
* otherwise.
*/ */
public function hasAllTags(); public function hasAllTags();
...@@ -166,6 +210,7 @@ interface QueryAlterableInterface { ...@@ -166,6 +210,7 @@ interface QueryAlterableInterface {
* *
* @param $tags * @param $tags
* A variable number of arguments, one for each tag to check. * A variable number of arguments, one for each tag to check.
*
* @return * @return
* TRUE if this query has been marked with at least one of the specified * TRUE if this query has been marked with at least one of the specified
* tags, FALSE otherwise. * tags, FALSE otherwise.
...@@ -184,6 +229,7 @@ interface QueryAlterableInterface { ...@@ -184,6 +229,7 @@ interface QueryAlterableInterface {
* follows the same rules as any other PHP identifier. * follows the same rules as any other PHP identifier.
* @param $object * @param $object
* The additional data to add to the query. May be any valid PHP variable. * The additional data to add to the query. May be any valid PHP variable.
*
* @return QueryAlterableInterface * @return QueryAlterableInterface
* The called object. * The called object.
*/ */
...@@ -194,6 +240,7 @@ interface QueryAlterableInterface { ...@@ -194,6 +240,7 @@ interface QueryAlterableInterface {
* *
* @param $key * @param $key
* The unique identifier for the piece of metadata to retrieve. * The unique identifier for the piece of metadata to retrieve.
*
* @return * @return
* The previously attached metadata object, or NULL if one doesn't exist. * The previously attached metadata object, or NULL if one doesn't exist.
*/ */
...@@ -205,19 +252,25 @@ interface QueryAlterableInterface { ...@@ -205,19 +252,25 @@ interface QueryAlterableInterface {
*/ */
interface QueryPlaceholderInterface { interface QueryPlaceholderInterface {
/**
* Returns a unique identifier for this object.
*/
public function uniqueIdentifier();
/** /**
* Returns the next placeholder ID for the query. * Returns the next placeholder ID for the query.
* *
* @return * @return
* The next available placeholder ID as an integer. * The next available placeholder ID as an integer.
*/ */
function nextPlaceholder(); public function nextPlaceholder();
} }
/** /**
* Base class for the query builders. * Base class for query builders.
* *
* All query builders inherit from a common base class. * Note that query builders use PHP's magic __toString() method to compile the
* query object into a prepared statement.
*/ */
abstract class Query implements QueryPlaceholderInterface { abstract class Query implements QueryPlaceholderInterface {
...@@ -228,6 +281,20 @@ abstract class Query implements QueryPlaceholderInterface { ...@@ -228,6 +281,20 @@ abstract class Query implements QueryPlaceholderInterface {
*/ */
protected $connection; protected $connection;
/**
* The target of the connection object.
*
* @var string
*/
protected $connectionTarget;
/**
* The key of the connection object.
*
* @var string
*/
protected $connectionKey;
/** /**
* The query options to pass on to the connection object. * The query options to pass on to the connection object.
* *
...@@ -235,6 +302,11 @@ abstract class Query implements QueryPlaceholderInterface { ...@@ -235,6 +302,11 @@ abstract class Query implements QueryPlaceholderInterface {
*/ */
protected $queryOptions; protected $queryOptions;
/**
* A unique identifier for this query object.
*/
protected $uniqueIdentifier;
/** /**
* The placeholder counter. * The placeholder counter.
*/ */
...@@ -247,26 +319,76 @@ abstract class Query implements QueryPlaceholderInterface { ...@@ -247,26 +319,76 @@ abstract class Query implements QueryPlaceholderInterface {
*/ */
protected $comments = array(); protected $comments = array();
/**
* Constructs a Query object.
*
* @param DatabaseConnection $connection
* Database connection object.
* @param array $options
* Array of query options.
*/
public function __construct(DatabaseConnection $connection, $options) { public function __construct(DatabaseConnection $connection, $options) {
$this->uniqueIdentifier = uniqid('', TRUE);
$this->connection = $connection; $this->connection = $connection;
$this->connectionKey = $this->connection->getKey();
$this->connectionTarget = $this->connection->getTarget();
$this->queryOptions = $options; $this->queryOptions = $options;
} }
/** /**
* Run the query against the database. * Implements the magic __sleep function to disconnect from the database.
*/
public function __sleep() {
$keys = get_object_vars($this);
unset($keys['connection']);
return array_keys($keys);
}
/**
* Implements the magic __wakeup function to reconnect to the database.
*/
public function __wakeup() {
$this->connection = Database::getConnection($this->connectionTarget, $this->connectionKey);
}
/**
* Implements the magic __clone function.
*/
public function __clone() {
$this->uniqueIdentifier = uniqid('', TRUE);
}
/**
* Runs the query against the database.
*/ */
abstract protected function execute(); abstract protected function execute();
/** /**
* __toString() magic method. * Implements PHP magic __toString method to convert the query to a string.
* *
* The toString operation is how we compile a query object to a prepared statement. * The toString operation is how we compile a query object to a prepared
* statement.
* *
* @return * @return
* A prepared statement query string for this object. * A prepared statement query string for this object.
*/ */
abstract public function __toString(); abstract public function __toString();
/**
* Returns a unique identifier for this object.
*/
public function uniqueIdentifier() {
return $this->uniqueIdentifier;
}
/**
* Gets the next placeholder value for this query object.
*
* @return int
* Next placeholder value.
*/
public function nextPlaceholder() { public function nextPlaceholder() {
return $this->nextPlaceholder++; return $this->nextPlaceholder++;
} }
...@@ -275,12 +397,16 @@ abstract class Query implements QueryPlaceholderInterface { ...@@ -275,12 +397,16 @@ abstract class Query implements QueryPlaceholderInterface {
* Adds a comment to the query. * Adds a comment to the query.
* *
* By adding a comment to a query, you can more easily find it in your * By adding a comment to a query, you can more easily find it in your
* query log or the list of active queries on an sql server. This allows * query log or the list of active queries on an SQL server. This allows
* for easier debugging and allows you to more easily find where a query * for easier debugging and allows you to more easily find where a query
* with a performance problem is being generated. * with a performance problem is being generated.
* *
* The comment string will be sanitized to remove * / and other characters
* that may terminate the string early so as to avoid SQL injection attacks.
*
* @param $comment * @param $comment
* The comment string to be inserted into the query. * The comment string to be inserted into the query.
*
* @return Query * @return Query
* The called object. * The called object.
*/ */
...@@ -297,7 +423,6 @@ abstract class Query implements QueryPlaceholderInterface { ...@@ -297,7 +423,6 @@ abstract class Query implements QueryPlaceholderInterface {
* use of comment() is preferred. * use of comment() is preferred.
* *
* Note that this method must be called by reference as well: * Note that this method must be called by reference as well:
*
* @code * @code
* $comments =& $query->getComments(); * $comments =& $query->getComments();
* @endcode * @endcode
...@@ -311,7 +436,7 @@ abstract class Query implements QueryPlaceholderInterface { ...@@ -311,7 +436,7 @@ abstract class Query implements QueryPlaceholderInterface {
} }
/** /**
* General class for an abstracted INSERT operation. * General class for an abstracted INSERT query.
*/ */
class InsertQuery extends Query { class InsertQuery extends Query {
...@@ -330,7 +455,7 @@ class InsertQuery extends Query { ...@@ -330,7 +455,7 @@ class InsertQuery extends Query {
protected $insertFields = array(); protected $insertFields = array();
/** /**
* An array of fields which should be set to their database-defined defaults. * An array of fields that should be set to their database-defined defaults.
* *
* @var array * @var array
*/ */
...@@ -339,13 +464,17 @@ class InsertQuery extends Query { ...@@ -339,13 +464,17 @@ class InsertQuery extends Query {
/** /**
* A nested array of values to insert. * A nested array of values to insert.
* *
* $insertValues itself is an array of arrays. Each sub-array is an array of * $insertValues is an array of arrays. Each sub-array is either an
* field names to values to insert. Whether multiple insert sets * associative array whose keys are field names and whose values are field
* will be run in a single query or multiple queries is left to individual drivers * values to insert, or a non-associative array of values in the same order
* to implement in whatever manner is most efficient. The order of values in each * as $insertFields.
* sub-array must match the order of fields in $insertFields.
* *
* @var string * Whether multiple insert sets will be run in a single query or multiple
* queries is left to individual drivers to implement in whatever manner is
* most appropriate. The order of values in each sub-array must match the
* order of fields in $insertFields.
*
* @var array
*/ */
protected $insertValues = array(); protected $insertValues = array();
...@@ -356,6 +485,16 @@ class InsertQuery extends Query { ...@@ -356,6 +485,16 @@ class InsertQuery extends Query {
*/ */
protected $fromQuery; protected $fromQuery;
/**
* Constructs an InsertQuery object.
*
* @param DatabaseConnection $connection
* A DatabaseConnection object.
* @param string $table
* Name of the table to associate with this query.
* @param array $options
* Array of database options.
*/
public function __construct($connection, $table, array $options = array()) { public function __construct($connection, $table, array $options = array()) {
if (!isset($options['return'])) { if (!isset($options['return'])) {
$options['return'] = Database::RETURN_INSERT_ID; $options['return'] = Database::RETURN_INSERT_ID;
...@@ -365,7 +504,7 @@ class InsertQuery extends Query { ...@@ -365,7 +504,7 @@ class InsertQuery extends Query {
} }
/** /**
* Add a set of field->value pairs to be inserted. * Adds a set of field->value pairs to be inserted.
* *
* This method may only be called once. Calling it a second time will be * This method may only be called once. Calling it a second time will be
* ignored. To queue up multiple sets of values to be inserted at once, * ignored. To queue up multiple sets of values to be inserted at once,
...@@ -380,6 +519,7 @@ class InsertQuery extends Query { ...@@ -380,6 +519,7 @@ class InsertQuery extends Query {
* @param $values * @param $values
* An array of fields to insert into the database. The values must be * An array of fields to insert into the database. The values must be
* specified in the same order as the $fields array. * specified in the same order as the $fields array.
*
* @return InsertQuery * @return InsertQuery
* The called object. * The called object.
*/ */
...@@ -401,15 +541,16 @@ class InsertQuery extends Query { ...@@ -401,15 +541,16 @@ class InsertQuery extends Query {
} }
/** /**
* Add another set of values to the query to be inserted. * Adds another set of values to the query to be inserted.
* *
* If $values is a numeric array, it will be assumed to be in the same * If $values is a numeric-keyed array, it will be assumed to be in the same
* order as the original fields() call. If it is associative, it may be * order as the original fields() call. If it is associative, it may be
* in any order as long as the keys of the array match the names of the * in any order as long as the keys of the array match the names of the
* fields. * fields.
* *
* @param $values * @param $values
* An array of values to add to the query. * An array of values to add to the query.
*
* @return InsertQuery * @return InsertQuery
* The called object. * The called object.
*/ */
...@@ -429,7 +570,7 @@ class InsertQuery extends Query { ...@@ -429,7 +570,7 @@ class InsertQuery extends Query {
} }
/** /**
* Specify fields for which the database-defaults should be used. * Specifies fields for which the database defaults should be used.
* *
* If you want to force a given field to use the database-defined default, * If you want to force a given field to use the database-defined default,
* not NULL or undefined, use this method to instruct the database to use * not NULL or undefined, use this method to instruct the database to use
...@@ -443,6 +584,7 @@ class InsertQuery extends Query { ...@@ -443,6 +584,7 @@ class InsertQuery extends Query {
* @param $fields * @param $fields
* An array of values for which to use the default values * An array of values for which to use the default values
* specified in the table definition. * specified in the table definition.
*
* @return InsertQuery * @return InsertQuery
* The called object. * The called object.
*/ */
...@@ -451,6 +593,15 @@ class InsertQuery extends Query { ...@@ -451,6 +593,15 @@ class InsertQuery extends Query {
return $this; return $this;
} }
/**
* Sets the fromQuery on this InsertQuery object.
*
* @param SelectQueryInterface $query
* The query to fetch the rows that should be inserted.
*
* @return InsertQuery
* The called object.
*/
public function from(SelectQueryInterface $query) { public function from(SelectQueryInterface $query) {
$this->fromQuery = $query; $this->fromQuery = $query;
return $this; return $this;
...@@ -466,8 +617,8 @@ class InsertQuery extends Query { ...@@ -466,8 +617,8 @@ class InsertQuery extends Query {
* return NULL. That makes it safe to use in multi-insert loops. * return NULL. That makes it safe to use in multi-insert loops.
*/ */
public function execute() { public function execute() {
// If validation fails, simply return NULL. // If validation fails, simply return NULL. Note that validation routines
// Note that validation routines in preExecute() may throw exceptions instead. // in preExecute() may throw exceptions instead.
if (!$this->preExecute()) { if (!$this->preExecute()) {
return NULL; return NULL;
} }
...@@ -508,10 +659,15 @@ class InsertQuery extends Query { ...@@ -508,10 +659,15 @@ class InsertQuery extends Query {
return $last_insert_id; return $last_insert_id;
} }
/**
* Implements PHP magic __toString method to convert the query to a string.
*
* @return string
* The prepared statement.
*/
public function __toString() { public function __toString() {
// Create a sanitized comment string to prepend to the query.
// Create a comments string to prepend to the query. $comments = $this->connection->makeComment($this->comments);
$comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : '';
// Default fields are always placed first for consistency. // Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields); $insert_fields = array_merge($this->defaultFields, $this->insertFields);
...@@ -531,7 +687,7 @@ class InsertQuery extends Query { ...@@ -531,7 +687,7 @@ class InsertQuery extends Query {
} }
/** /**
* Generic preparation and validation for an INSERT query. * Preprocesses and validates the query.
* *
* @return * @return
* TRUE if the validation was successful, FALSE if not. * TRUE if the validation was successful, FALSE if not.
...@@ -583,13 +739,24 @@ class DeleteQuery extends Query implements QueryConditionInterface { ...@@ -583,13 +739,24 @@ class DeleteQuery extends Query implements QueryConditionInterface {
protected $table; protected $table;
/** /**
* The condition object for this query. Condition handling is handled via * The condition object for this query.
* composition. *
* Condition handling is handled via composition.
* *
* @var DatabaseCondition * @var DatabaseCondition
*/ */
protected $condition; protected $condition;
/**
* Constructs a DeleteQuery object.
*
* @param DatabaseConnection $connection
* A DatabaseConnection object.
* @param string $table
* Name of the table to associate with this query.
* @param array $options
* Array of database options.
*/
public function __construct(DatabaseConnection $connection, $table, array $options = array()) { public function __construct(DatabaseConnection $connection, $table, array $options = array()) {
$options['return'] = Database::RETURN_AFFECTED; $options['return'] = Database::RETURN_AFFECTED;
parent::__construct($connection, $options); parent::__construct($connection, $options);
...@@ -598,38 +765,88 @@ class DeleteQuery extends Query implements QueryConditionInterface { ...@@ -598,38 +765,88 @@ class DeleteQuery extends Query implements QueryConditionInterface {
$this->condition = new DatabaseCondition('AND'); $this->condition = new DatabaseCondition('AND');
} }
/**
* Implements QueryConditionInterface::condition().
*/
public function condition($field, $value = NULL, $operator = NULL) { public function condition($field, $value = NULL, $operator = NULL) {
$this->condition->condition($field, $value, $operator); $this->condition->condition($field, $value, $operator);
return $this; return $this;
} }
/**
* Implements QueryConditionInterface::isNull().
*/
public function isNull($field) { public function isNull($field) {
$this->condition->isNull($field); $this->condition->isNull($field);
return $this; return $this;
} }
/**
* Implements QueryConditionInterface::isNotNull().
*/
public function isNotNull($field) { public function isNotNull($field) {
$this->condition->isNotNull($field); $this->condition->isNotNull($field);
return $this; return $this;
} }
/**
* Implements QueryConditionInterface::exists().
*/
public function exists(SelectQueryInterface $select) {
$this->condition->exists($select);
return $this;
}
/**
* Implements QueryConditionInterface::notExists().
*/
public function notExists(SelectQueryInterface $select) {
$this->condition->notExists($select);
return $this;
}
/**
* Implements QueryConditionInterface::conditions().
*/
public function &conditions() { public function &conditions() {
return $this->condition->conditions(); return $this->condition->conditions();
} }
/**
* Implements QueryConditionInterface::arguments().
*/
public function arguments() { public function arguments() {
return $this->condition->arguments(); return $this->condition->arguments();
} }
/**
* Implements QueryConditionInterface::where().
*/
public function where($snippet, $args = array()) { public function where($snippet, $args = array()) {
$this->condition->where($snippet, $args); $this->condition->where($snippet, $args);
return $this; return $this;
} }
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder = NULL) { /**
return $this->condition->compile($connection, isset($queryPlaceholder) ? $queryPlaceholder : $this); * Implements QueryConditionInterface::compile().
*/
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
return $this->condition->compile($connection, $queryPlaceholder);
}
/**
* Implements QueryConditionInterface::compiled().
*/
public function compiled() {
return $this->condition->compiled();
} }
/**
* Executes the DELETE query.
*
* @return
* The return value is dependent on the database connection.
*/
public function execute() { public function execute() {
$values = array(); $values = array();
if (count($this->condition)) { if (count($this->condition)) {
...@@ -640,10 +857,15 @@ class DeleteQuery extends Query implements QueryConditionInterface { ...@@ -640,10 +857,15 @@ class DeleteQuery extends Query implements QueryConditionInterface {
return $this->connection->query((string) $this, $values, $this->queryOptions); return $this->connection->query((string) $this, $values, $this->queryOptions);
} }
/**
* Implements PHP magic __toString method to convert the query to a string.
*
* @return string
* The prepared statement.
*/
public function __toString() { public function __toString() {
// Create a sanitized comment string to prepend to the query.
// Create a comments string to prepend to the query. $comments = $this->connection->makeComment($this->comments);
$comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : '';
$query = $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} '; $query = $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} ';
...@@ -664,29 +886,61 @@ class DeleteQuery extends Query implements QueryConditionInterface { ...@@ -664,29 +886,61 @@ class DeleteQuery extends Query implements QueryConditionInterface {
class TruncateQuery extends Query { class TruncateQuery extends Query {
/** /**
* The table from which to delete. * The table to truncate.
* *
* @var string * @var string
*/ */
protected $table; protected $table;
/**
* Constructs a TruncateQuery object.
*
* @param DatabaseConnection $connection
* A DatabaseConnection object.
* @param string $table
* Name of the table to associate with this query.
* @param array $options
* Array of database options.
*/
public function __construct(DatabaseConnection $connection, $table, array $options = array()) { public function __construct(DatabaseConnection $connection, $table, array $options = array()) {
$options['return'] = Database::RETURN_AFFECTED; $options['return'] = Database::RETURN_AFFECTED;
parent::__construct($connection, $options); parent::__construct($connection, $options);
$this->table = $table; $this->table = $table;
} }
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder = NULL) { /**
return $this->condition->compile($connection, isset($queryPlaceholder) ? $queryPlaceholder : $this); * Implements QueryConditionInterface::compile().
*/
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
return $this->condition->compile($connection, $queryPlaceholder);
}
/**
* Implements QueryConditionInterface::compiled().
*/
public function compiled() {
return $this->condition->compiled();
} }
/**
* Executes the TRUNCATE query.
*
* @return
* Return value is dependent on the database type.
*/
public function execute() { public function execute() {
return $this->connection->query((string) $this, array(), $this->queryOptions); return $this->connection->query((string) $this, array(), $this->queryOptions);
} }
/**
* Implements PHP magic __toString method to convert the query to a string.
*
* @return string
* The prepared statement.
*/
public function __toString() { public function __toString() {
// Create a comments string to prepend to the query. // Create a sanitized comment string to prepend to the query.
$comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; $comments = $this->connection->makeComment($this->comments);
return $comments . 'TRUNCATE {' . $this->connection->escapeTable($this->table) . '} '; return $comments . 'TRUNCATE {' . $this->connection->escapeTable($this->table) . '} ';
} }
...@@ -719,26 +973,39 @@ class UpdateQuery extends Query implements QueryConditionInterface { ...@@ -719,26 +973,39 @@ class UpdateQuery extends Query implements QueryConditionInterface {
protected $arguments = array(); protected $arguments = array();
/** /**
* The condition object for this query. Condition handling is handled via * The condition object for this query.
* composition. *
* Condition handling is handled via composition.
* *
* @var DatabaseCondition * @var DatabaseCondition
*/ */
protected $condition; protected $condition;
/** /**
* An array of fields to update to an expression in case of a duplicate record. * Array of fields to update to an expression in case of a duplicate record.
* *
* This variable is a nested array in the following format: * This variable is a nested array in the following format:
* @code
* <some field> => array( * <some field> => array(
* 'condition' => <condition to execute, as a string> * 'condition' => <condition to execute, as a string>,
* 'arguments' => <array of arguments for condition, or NULL for none> * 'arguments' => <array of arguments for condition, or NULL for none>,
* ); * );
* @endcode
* *
* @var array * @var array
*/ */
protected $expressionFields = array(); protected $expressionFields = array();
/**
* Constructs an UpdateQuery object.
*
* @param DatabaseConnection $connection
* A DatabaseConnection object.
* @param string $table
* Name of the table to associate with this query.
* @param array $options
* Array of database options.
*/
public function __construct(DatabaseConnection $connection, $table, array $options = array()) { public function __construct(DatabaseConnection $connection, $table, array $options = array()) {
$options['return'] = Database::RETURN_AFFECTED; $options['return'] = Database::RETURN_AFFECTED;
parent::__construct($connection, $options); parent::__construct($connection, $options);
...@@ -747,44 +1014,89 @@ class UpdateQuery extends Query implements QueryConditionInterface { ...@@ -747,44 +1014,89 @@ class UpdateQuery extends Query implements QueryConditionInterface {
$this->condition = new DatabaseCondition('AND'); $this->condition = new DatabaseCondition('AND');
} }
/**
* Implements QueryConditionInterface::condition().
*/
public function condition($field, $value = NULL, $operator = NULL) { public function condition($field, $value = NULL, $operator = NULL) {
$this->condition->condition($field, $value, $operator); $this->condition->condition($field, $value, $operator);
return $this; return $this;
} }
/**
* Implements QueryConditionInterface::isNull().
*/
public function isNull($field) { public function isNull($field) {
$this->condition->isNull($field); $this->condition->isNull($field);
return $this; return $this;
} }
/**
* Implements QueryConditionInterface::isNotNull().
*/
public function isNotNull($field) { public function isNotNull($field) {
$this->condition->isNotNull($field); $this->condition->isNotNull($field);
return $this; return $this;
} }
/**
* Implements QueryConditionInterface::exists().
*/
public function exists(SelectQueryInterface $select) {
$this->condition->exists($select);
return $this;
}
/**
* Implements QueryConditionInterface::notExists().
*/
public function notExists(SelectQueryInterface $select) {
$this->condition->notExists($select);
return $this;
}
/**
* Implements QueryConditionInterface::conditions().
*/
public function &conditions() { public function &conditions() {
return $this->condition->conditions(); return $this->condition->conditions();
} }
/**
* Implements QueryConditionInterface::arguments().
*/
public function arguments() { public function arguments() {
return $this->condition->arguments(); return $this->condition->arguments();
} }
/**
* Implements QueryConditionInterface::where().
*/
public function where($snippet, $args = array()) { public function where($snippet, $args = array()) {
$this->condition->where($snippet, $args); $this->condition->where($snippet, $args);
return $this; return $this;
} }
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder = NULL) { /**
return $this->condition->compile($connection, isset($queryPlaceholder) ? $queryPlaceholder : $this); * Implements QueryConditionInterface::compile().
*/
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
return $this->condition->compile($connection, $queryPlaceholder);
}
/**
* Implements QueryConditionInterface::compiled().
*/
public function compiled() {
return $this->condition->compiled();
} }
/** /**
* Add a set of field->value pairs to be updated. * Adds a set of field->value pairs to be updated.
* *
* @param $fields * @param $fields
* An associative array of fields to write into the database. The array keys * An associative array of fields to write into the database. The array keys
* are the field names while the values are the values to which to set them. * are the field names and the values are the values to which to set them.
*
* @return UpdateQuery * @return UpdateQuery
* The called object. * The called object.
*/ */
...@@ -794,7 +1106,7 @@ class UpdateQuery extends Query implements QueryConditionInterface { ...@@ -794,7 +1106,7 @@ class UpdateQuery extends Query implements QueryConditionInterface {
} }
/** /**
* Specify fields to be updated as an expression. * Specifies fields to be updated as an expression.
* *
* Expression fields are cases such as counter=counter+1. This method takes * Expression fields are cases such as counter=counter+1. This method takes
* precedence over fields(). * precedence over fields().
...@@ -807,6 +1119,7 @@ class UpdateQuery extends Query implements QueryConditionInterface { ...@@ -807,6 +1119,7 @@ class UpdateQuery extends Query implements QueryConditionInterface {
* @param $arguments * @param $arguments
* If specified, this is an array of key/value pairs for named placeholders * If specified, this is an array of key/value pairs for named placeholders
* corresponding to the expression. * corresponding to the expression.
*
* @return UpdateQuery * @return UpdateQuery
* The called object. * The called object.
*/ */
...@@ -819,6 +1132,12 @@ class UpdateQuery extends Query implements QueryConditionInterface { ...@@ -819,6 +1132,12 @@ class UpdateQuery extends Query implements QueryConditionInterface {
return $this; return $this;
} }
/**
* Executes the UPDATE query.
*
* @return
* The number of rows affected by the update.
*/
public function execute() { public function execute() {
// Expressions take priority over literal fields, so we process those first // Expressions take priority over literal fields, so we process those first
...@@ -847,10 +1166,15 @@ class UpdateQuery extends Query implements QueryConditionInterface { ...@@ -847,10 +1166,15 @@ class UpdateQuery extends Query implements QueryConditionInterface {
return $this->connection->query((string) $this, $update_values, $this->queryOptions); return $this->connection->query((string) $this, $update_values, $this->queryOptions);
} }
/**
* Implements PHP magic __toString method to convert the query to a string.
*
* @return string
* The prepared statement.
*/
public function __toString() { public function __toString() {
// Create a sanitized comment string to prepend to the query.
// Create a comments string to prepend to the query. $comments = $this->connection->makeComment($this->comments);
$comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : '';
// Expressions take priority over literal fields, so we process those first // Expressions take priority over literal fields, so we process those first
// and remove any literal fields that conflict. // and remove any literal fields that conflict.
...@@ -880,7 +1204,7 @@ class UpdateQuery extends Query implements QueryConditionInterface { ...@@ -880,7 +1204,7 @@ class UpdateQuery extends Query implements QueryConditionInterface {
} }
/** /**
* General class for an abstracted MERGE operation. * General class for an abstracted MERGE query operation.
* *
* An ANSI SQL:2003 compatible database would run the following query: * An ANSI SQL:2003 compatible database would run the following query:
* *
...@@ -970,25 +1294,37 @@ class MergeQuery extends Query implements QueryConditionInterface { ...@@ -970,25 +1294,37 @@ class MergeQuery extends Query implements QueryConditionInterface {
protected $updateFields = array(); protected $updateFields = array();
/** /**
* An array of fields to update to an expression in case of a duplicate record. * Array of fields to update to an expression in case of a duplicate record.
* *
* This variable is a nested array in the following format: * This variable is a nested array in the following format:
* @code
* <some field> => array( * <some field> => array(
* 'condition' => <condition to execute, as a string> * 'condition' => <condition to execute, as a string>,
* 'arguments' => <array of arguments for condition, or NULL for none> * 'arguments' => <array of arguments for condition, or NULL for none>,
* ); * );
* @endcode
* *
* @var array * @var array
*/ */
protected $expressionFields = array(); protected $expressionFields = array();
/** /**
* Flag indicated whether an UPDATE is necessary. * Flag indicating whether an UPDATE is necessary.
* *
* @var boolean * @var boolean
*/ */
protected $needsUpdate = FALSE; protected $needsUpdate = FALSE;
/**
* Constructs a MergeQuery object.
*
* @param DatabaseConnection $connection
* A DatabaseConnection object.
* @param string $table
* Name of the table to associate with this query.
* @param array $options
* Array of database options.
*/
public function __construct(DatabaseConnection $connection, $table, array $options = array()) { public function __construct(DatabaseConnection $connection, $table, array $options = array()) {
$options['return'] = Database::RETURN_AFFECTED; $options['return'] = Database::RETURN_AFFECTED;
parent::__construct($connection, $options); parent::__construct($connection, $options);
...@@ -998,7 +1334,7 @@ class MergeQuery extends Query implements QueryConditionInterface { ...@@ -998,7 +1334,7 @@ class MergeQuery extends Query implements QueryConditionInterface {
} }
/** /**
* Set the table or subquery to be used for the condition. * Sets the table or subquery to be used for the condition.
* *
* @param $table * @param $table
* The table name or the subquery to be used. Use a SelectQuery object to * The table name or the subquery to be used. Use a SelectQuery object to
...@@ -1017,7 +1353,7 @@ class MergeQuery extends Query implements QueryConditionInterface { ...@@ -1017,7 +1353,7 @@ class MergeQuery extends Query implements QueryConditionInterface {
* *
* @param $fields * @param $fields
* An associative array of fields to write into the database. The array keys * An associative array of fields to write into the database. The array keys
* are the field names while the values are the values to which to set them. * are the field names and the values are the values to which to set them.
* *
* @return MergeQuery * @return MergeQuery
* The called object. * The called object.
...@@ -1029,7 +1365,7 @@ class MergeQuery extends Query implements QueryConditionInterface { ...@@ -1029,7 +1365,7 @@ class MergeQuery extends Query implements QueryConditionInterface {
} }
/** /**
* Specify fields to be updated as an expression. * Specifies fields to be updated as an expression.
* *
* Expression fields are cases such as counter = counter + 1. This method * Expression fields are cases such as counter = counter + 1. This method
* takes precedence over MergeQuery::updateFields() and it's wrappers, * takes precedence over MergeQuery::updateFields() and it's wrappers,
...@@ -1043,6 +1379,7 @@ class MergeQuery extends Query implements QueryConditionInterface { ...@@ -1043,6 +1379,7 @@ class MergeQuery extends Query implements QueryConditionInterface {
* @param $arguments * @param $arguments
* If specified, this is an array of key/value pairs for named placeholders * If specified, this is an array of key/value pairs for named placeholders
* corresponding to the expression. * corresponding to the expression.
*
* @return MergeQuery * @return MergeQuery
* The called object. * The called object.
*/ */
...@@ -1104,7 +1441,7 @@ class MergeQuery extends Query implements QueryConditionInterface { ...@@ -1104,7 +1441,7 @@ class MergeQuery extends Query implements QueryConditionInterface {
} }
/** /**
* Set common field-value pairs in the INSERT and UPDATE query parts. * Sets common field-value pairs in the INSERT and UPDATE query parts.
* *
* This method should only be called once. It may be called either * This method should only be called once. It may be called either
* with a single associative array or two indexed arrays. If called * with a single associative array or two indexed arrays. If called
...@@ -1114,11 +1451,11 @@ class MergeQuery extends Query implements QueryConditionInterface { ...@@ -1114,11 +1451,11 @@ class MergeQuery extends Query implements QueryConditionInterface {
* and the second array is taken as the corresponding values. * and the second array is taken as the corresponding values.
* *
* @param $fields * @param $fields
* An associative array of fields on which to insert. The keys of the * An array of fields to insert, or an associative array of fields and
* array are taken to be the fields and the values are taken to be * values. The keys of the array are taken to be the fields and the values
* corresponding values to insert. * are taken to be corresponding values to insert.
* @param $values * @param $values
* An array of fields to set into the database. The values must be * An array of values to set into the database. The values must be
* specified in the same order as the $fields array. * specified in the same order as the $fields array.
* *
* @return MergeQuery * @return MergeQuery
...@@ -1137,7 +1474,7 @@ class MergeQuery extends Query implements QueryConditionInterface { ...@@ -1137,7 +1474,7 @@ class MergeQuery extends Query implements QueryConditionInterface {
} }
/** /**
* Set the key field(s) to be used as conditions for this query. * Sets the key field(s) to be used as conditions for this query.
* *
* This method should only be called once. It may be called either * This method should only be called once. It may be called either
* with a single associative array or two indexed arrays. If called * with a single associative array or two indexed arrays. If called
...@@ -1150,10 +1487,11 @@ class MergeQuery extends Query implements QueryConditionInterface { ...@@ -1150,10 +1487,11 @@ class MergeQuery extends Query implements QueryConditionInterface {
* If no other method is called, the UPDATE will become a no-op. * If no other method is called, the UPDATE will become a no-op.
* *
* @param $fields * @param $fields
* An array of fields to set. * An array of fields to set, or an associative array of fields and values.
* @param $values * @param $values
* An array of fields to set into the database. The values must be * An array of values to set into the database. The values must be
* specified in the same order as the $fields array. * specified in the same order as the $fields array.
*
* @return MergeQuery * @return MergeQuery
* The called object. * The called object.
*/ */
...@@ -1168,38 +1506,91 @@ class MergeQuery extends Query implements QueryConditionInterface { ...@@ -1168,38 +1506,91 @@ class MergeQuery extends Query implements QueryConditionInterface {
return $this; return $this;
} }
/**
* Implements QueryConditionInterface::condition().
*/
public function condition($field, $value = NULL, $operator = NULL) { public function condition($field, $value = NULL, $operator = NULL) {
$this->condition->condition($field, $value, $operator); $this->condition->condition($field, $value, $operator);
return $this; return $this;
} }
/**
* Implements QueryConditionInterface::isNull().
*/
public function isNull($field) { public function isNull($field) {
$this->condition->isNull($field); $this->condition->isNull($field);
return $this; return $this;
} }
/**
* Implements QueryConditionInterface::isNotNull().
*/
public function isNotNull($field) { public function isNotNull($field) {
$this->condition->isNotNull($field); $this->condition->isNotNull($field);
return $this; return $this;
} }
/**
* Implements QueryConditionInterface::exists().
*/
public function exists(SelectQueryInterface $select) {
$this->condition->exists($select);
return $this;
}
/**
* Implements QueryConditionInterface::notExists().
*/
public function notExists(SelectQueryInterface $select) {
$this->condition->notExists($select);
return $this;
}
/**
* Implements QueryConditionInterface::conditions().
*/
public function &conditions() { public function &conditions() {
return $this->condition->conditions(); return $this->condition->conditions();
} }
/**
* Implements QueryConditionInterface::arguments().
*/
public function arguments() { public function arguments() {
return $this->condition->arguments(); return $this->condition->arguments();
} }
/**
* Implements QueryConditionInterface::where().
*/
public function where($snippet, $args = array()) { public function where($snippet, $args = array()) {
$this->condition->where($snippet, $args); $this->condition->where($snippet, $args);
return $this; return $this;
} }
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder = NULL) { /**
return $this->condition->compile($connection, isset($queryPlaceholder) ? $queryPlaceholder : $this); * Implements QueryConditionInterface::compile().
*/
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
return $this->condition->compile($connection, $queryPlaceholder);
} }
/**
* Implements QueryConditionInterface::compiled().
*/
public function compiled() {
return $this->condition->compiled();
}
/**
* Implements PHP magic __toString method to convert the query to a string.
*
* In the degenerate case, there is no string-able query as this operation
* is potentially two queries.
*
* @return string
* The prepared query statement.
*/
public function __toString() { public function __toString() {
} }
...@@ -1261,25 +1652,59 @@ class MergeQuery extends Query implements QueryConditionInterface { ...@@ -1261,25 +1652,59 @@ class MergeQuery extends Query implements QueryConditionInterface {
*/ */
class DatabaseCondition implements QueryConditionInterface, Countable { class DatabaseCondition implements QueryConditionInterface, Countable {
/**
* Array of conditions.
*
* @var array
*/
protected $conditions = array(); protected $conditions = array();
/**
* Array of arguments.
*
* @var array
*/
protected $arguments = array(); protected $arguments = array();
/**
* Whether the conditions have been changed.
*
* TRUE if the condition has been changed since the last compile.
* FALSE if the condition has been compiled and not changed.
*
* @var bool
*/
protected $changed = TRUE; protected $changed = TRUE;
/**
* The identifier of the query placeholder this condition has been compiled against.
*/
protected $queryPlaceholderIdentifier;
/**
* Constructs a DataBaseCondition object.
*
* @param string $conjunction
* The operator to use to combine conditions: 'AND' or 'OR'.
*/
public function __construct($conjunction) { public function __construct($conjunction) {
$this->conditions['#conjunction'] = $conjunction; $this->conditions['#conjunction'] = $conjunction;
} }
/** /**
* Return the size of this conditional. This is part of the Countable interface. * Implements Countable::count().
* *
* The size of the conditional is the size of its conditional array minus * Returns the size of this conditional. The size of the conditional is the
* one, because one element is the the conjunction. * size of its conditional array minus one, because one element is the the
* conjunction.
*/ */
public function count() { public function count() {
return count($this->conditions) - 1; return count($this->conditions) - 1;
} }
/**
* Implements QueryConditionInterface::condition().
*/
public function condition($field, $value = NULL, $operator = NULL) { public function condition($field, $value = NULL, $operator = NULL) {
if (!isset($operator)) { if (!isset($operator)) {
if (is_array($value)) { if (is_array($value)) {
...@@ -1303,6 +1728,9 @@ class DatabaseCondition implements QueryConditionInterface, Countable { ...@@ -1303,6 +1728,9 @@ class DatabaseCondition implements QueryConditionInterface, Countable {
return $this; return $this;
} }
/**
* Implements QueryConditionInterface::where().
*/
public function where($snippet, $args = array()) { public function where($snippet, $args = array()) {
$this->conditions[] = array( $this->conditions[] = array(
'field' => $snippet, 'field' => $snippet,
...@@ -1314,18 +1742,44 @@ class DatabaseCondition implements QueryConditionInterface, Countable { ...@@ -1314,18 +1742,44 @@ class DatabaseCondition implements QueryConditionInterface, Countable {
return $this; return $this;
} }
/**
* Implements QueryConditionInterface::isNull().
*/
public function isNull($field) { public function isNull($field) {
return $this->condition($field); return $this->condition($field);
} }
/**
* Implements QueryConditionInterface::isNotNull().
*/
public function isNotNull($field) { public function isNotNull($field) {
return $this->condition($field, NULL, 'IS NOT NULL'); return $this->condition($field, NULL, 'IS NOT NULL');
} }
/**
* Implements QueryConditionInterface::exists().
*/
public function exists(SelectQueryInterface $select) {
return $this->condition('', $select, 'EXISTS');
}
/**
* Implements QueryConditionInterface::notExists().
*/
public function notExists(SelectQueryInterface $select) {
return $this->condition('', $select, 'NOT EXISTS');
}
/**
* Implements QueryConditionInterface::conditions().
*/
public function &conditions() { public function &conditions() {
return $this->conditions; return $this->conditions;
} }
/**
* Implements QueryConditionInterface::arguments().
*/
public function arguments() { public function arguments() {
// If the caller forgot to call compile() first, refuse to run. // If the caller forgot to call compile() first, refuse to run.
if ($this->changed) { if ($this->changed) {
...@@ -1334,8 +1788,15 @@ class DatabaseCondition implements QueryConditionInterface, Countable { ...@@ -1334,8 +1788,15 @@ class DatabaseCondition implements QueryConditionInterface, Countable {
return $this->arguments; return $this->arguments;
} }
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder = NULL) { /**
if ($this->changed) { * Implements QueryConditionInterface::compile().
*/
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
// Re-compile if this condition changed or if we are compiled against a
// different query placeholder object.
if ($this->changed || isset($this->queryPlaceholderIdentifier) && ($this->queryPlaceholderIdentifier != $queryPlaceholder->uniqueIdentifier())) {
$this->queryPlaceholderIdentifier = $queryPlaceholder->uniqueIdentifier();
$condition_fragments = array(); $condition_fragments = array();
$arguments = array(); $arguments = array();
...@@ -1407,6 +1868,19 @@ class DatabaseCondition implements QueryConditionInterface, Countable { ...@@ -1407,6 +1868,19 @@ class DatabaseCondition implements QueryConditionInterface, Countable {
} }
} }
/**
* Implements QueryConditionInterface::compiled().
*/
public function compiled() {
return !$this->changed;
}
/**
* Implements PHP magic __toString method to convert the conditions to string.
*
* @return string
* A string version of the conditions.
*/
public function __toString() { public function __toString() {
// If the caller forgot to call compile() first, refuse to run. // If the caller forgot to call compile() first, refuse to run.
if ($this->changed) { if ($this->changed) {
...@@ -1415,12 +1889,23 @@ class DatabaseCondition implements QueryConditionInterface, Countable { ...@@ -1415,12 +1889,23 @@ class DatabaseCondition implements QueryConditionInterface, Countable {
return $this->stringVersion; return $this->stringVersion;
} }
/**
* PHP magic __clone() method.
*
* Only copies fields that implement QueryConditionInterface. Also sets
* $this->changed to TRUE.
*/
function __clone() { function __clone() {
$this->changed = TRUE; $this->changed = TRUE;
foreach ($this->conditions as $key => $condition) { foreach ($this->conditions as $key => $condition) {
if ($key !== '#conjunction') {
if ($condition['field'] instanceOf QueryConditionInterface) { if ($condition['field'] instanceOf QueryConditionInterface) {
$this->conditions[$key]['field'] = clone($condition['field']); $this->conditions[$key]['field'] = clone($condition['field']);
} }
if ($condition['value'] instanceOf SelectQueryInterface) {
$this->conditions[$key]['value'] = clone($condition['value']);
}
}
} }
} }
...@@ -1433,6 +1918,7 @@ class DatabaseCondition implements QueryConditionInterface, Countable { ...@@ -1433,6 +1918,7 @@ class DatabaseCondition implements QueryConditionInterface, Countable {
* *
* @param $operator * @param $operator
* The condition operator, such as "IN", "BETWEEN", etc. Case-sensitive. * The condition operator, such as "IN", "BETWEEN", etc. Case-sensitive.
*
* @return * @return
* The extra handling directives for the specified operator, or NULL. * The extra handling directives for the specified operator, or NULL.
*/ */
...@@ -1442,6 +1928,8 @@ class DatabaseCondition implements QueryConditionInterface, Countable { ...@@ -1442,6 +1928,8 @@ class DatabaseCondition implements QueryConditionInterface, Countable {
'BETWEEN' => array('delimiter' => ' AND '), 'BETWEEN' => array('delimiter' => ' AND '),
'IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'), 'IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'),
'NOT IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'), 'NOT IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'),
'EXISTS' => array('prefix' => ' (', 'postfix' => ')'),
'NOT EXISTS' => array('prefix' => ' (', 'postfix' => ')'),
'IS NULL' => array('use_value' => FALSE), 'IS NULL' => array('use_value' => FALSE),
'IS NOT NULL' => array('use_value' => FALSE), 'IS NOT NULL' => array('use_value' => FALSE),
// Use backslash for escaping wildcard characters. // Use backslash for escaping wildcard characters.
...@@ -1472,5 +1960,5 @@ class DatabaseCondition implements QueryConditionInterface, Countable { ...@@ -1472,5 +1960,5 @@ class DatabaseCondition implements QueryConditionInterface, Countable {
} }
/** /**
* @} End of "ingroup database". * @} End of "addtogroup database".
*/ */