'Download',
'type' => MENU_CALLBACK,
'page callback' => 'webform_protected_downloads_download_page',
'page arguments' => array(1),
'access callback' => 'webform_protected_downloads_access',
'access arguments' => array('view', 1),
'file' => 'webform_protected_downloads.page.inc',
);
$items['node/%webform_menu/download/%'] = array(
'title' => 'Download',
'type' => MENU_CALLBACK,
'page callback' => 'webform_protected_downloads_download_page',
'page arguments' => array(1, 3),
'access callback' => 'webform_protected_downloads_access',
'access arguments' => array('view', 1),
'file' => 'webform_protected_downloads.page.inc',
);
$items['node/%webform_menu/protected-downloads'] = array(
'title' => 'Protected Downloads',
'type' => MENU_LOCAL_TASK,
'page callback' => 'drupal_get_form',
'page arguments' => array('webform_protected_downloads_configuration_form', 1),
'access callback' => 'webform_protected_downloads_access',
'access arguments' => array('update', 1),
'weight' => 3,
'file' => 'webform_protected_downloads.form.inc',
);
return $items;
}
/**
* Implementation of hook_admin_paths().
*/
function webform_protected_downloads_admin_paths() {
return array(
'node/*/protected-downloads' => TRUE,
);
}
/**
* Implements hook_contextual_links_view_alter().
*/
function webform_protected_downloads_contextual_links_view_alter(&$element, &$items) {
// Add a link to the protected downloads configuration page on
// webform-enabled nodes.
if (isset($element['#element']['#node']) && webform_protected_downloads_node_is_webform($element['#element']['#node'])) {
$element['#links']['protected-downloads'] = array(
'title' => t('Protected downloads'),
'href' => 'node/' . $element['#element']['#node']->nid . '/protected-downloads',
);
}
}
/**
* Custom access callback
*
* @param object $node
* @return boolean
*/
function webform_protected_downloads_access($op, $node) {
switch ($op) {
case 'view':
// first check if there are any files attached to this node
if (!isset($node->wpd['private_files']) || !count($node->wpd['private_files'])) {
return FALSE;
}
return node_access($op, $node);
break;
case 'update':
return node_access($op, $node) && user_access('administer webform protected downloads');
break;
}
return FALSE;
}
/**
* Implementation of hook_permission().
*/
function webform_protected_downloads_permission() {
return array(
'administer webform protected downloads' => array(
'title' => t('Administer webform protected downloads'),
)
);
}
/**
* Implementation of hook_theme().
*/
function webform_protected_downloads_theme($existing, $type, $theme, $path) {
return array(
'webform_protected_downloads_download_page' => array(
'variables' => array('text' => NULL, 'files_table' => NULL),
'template' => 'webform-protected-downloads-download-page',
),
'webform_protected_downloads_configuration_form_file_list' => array(
'render element' => 'element',
'file' => 'webform_protected_downloads.form.inc',
),
'webform_protected_downloads_mail_token_file_list' => array(
'variables' => array('files' => NULL, 'checksum' => FALSE),
'file' => 'webform_protected_downloads.form.inc',
)
);
}
/**
* Implementation of hook_file_download().
*
* @param string $filepath
*/
function webform_protected_downloads_file_download($uri) {
global $conf;
$admin_access = user_access('administer webform protected downloads');
// Get the file record based on the URI. If not in the database just return.
$files = file_load_multiple(array(), array('uri' => $uri));
if (count($files)) {
foreach ($files as $item) {
// Since some database servers sometimes use a case-insensitive comparison
// by default, double check that the filename is an exact match.
if ($item->uri === $uri) {
$file = $item;
break;
}
}
}
// no file found, it is not up to us to handle this
if (!isset($file)) {
return NULL;
}
// check if the file is registered as protected
$count = db_select('wpd_protected_files', 'p')->fields('p')->condition('fid', $file->fid)->countQuery()->execute()->fetchColumn();
if (!$count) {
return NULL;
}
// now we know, that this is a protected file
// get all nodes that this file has been attached to
$result = db_select('file_usage', 'f')->fields('f')->condition('fid', $file->fid)->condition('module', 'file')->condition('type', 'node')->execute();
while ($record = $result->fetchObject()) {
$nid = $record->id;
// check if the file is protected for this node
if (webform_protected_downloads_file_is_protected($nid, $file->fid)) {
// don't cache access allowed or denied
$conf['cache'] = 0;
// check if the current user should be granted access to this file
if (webform_protected_downloads_file_user_has_access($nid, $file->fid) || $admin_access) {
// access granted
return file_get_content_headers($file);
}
else {
// access denied
return -1;
}
}
}
// file is not protected so we have nothing to say
return NULL;
}
/**
* Implementation of hook_file_delete().
*/
function webform_protected_downloads_file_delete($file) {
// delete all wpd information associated with the file
db_delete('wpd_protected_files')->condition('fid', $file->fid)->execute();
}
/**
* Implementation of hook_menu_alter().
*
* @param array $items
* @return void
*/
function webform_protected_downloads_menu_alter(&$items) {
$item = &$items['node/%webform_menu/webform/components/%webform_menu_component/delete'];
$item['access callback'] = 'webform_protected_downloads_component_delete_access';
$item['access arguments'] = array('update', 1, 4);
}
/**
* Custom access callback that checks wheather a webform component can be
* deleted
*
* @param string $op
* @param object $node
* @param array $component
* @return boolean
*/
function webform_protected_downloads_component_delete_access($op, $node, $component) {
// check whether the current component exists already, otherwhise the coming
// checks would be unnecessary
if (isset($component['cid'])) {
$component_in_use = webform_protected_downloads_get_configuration($node->nid, 'mail_field_cid') == $component['cid'];
$node_protected = webform_protected_downloads_node_has_protected_files($node->nid);
if ($node_protected && $component_in_use) {
if (arg(5) == 'delete') {
// we are really on the delete confirmation page
drupal_set_message(t('Access to this action has been disabled by the Webform Protected Downloads module. The component that you want to delete is in use. Please got to the Protected Downloads configuration of this webform and change the Mail confirmation field or unprotect all files.
Go back to the form', array(
'@protected_downloads_page' => url('node/' . $node->nid . '/protected-downloads'),
'@last_page' => url($_GET['destination']),
)));
}
else {
// we are most probably on the components edit form
drupal_set_message(t('Deletion of this component has been disabled by the Webform Protected Downloads module, because the component is currently in use. This can be changed on the Protected Downloads configuration page.', array(
'@protected_downloads_page' => url('node/' . $node->nid . '/protected-downloads'),
)));
}
return FALSE;
}
}
return node_access($op, $node);
}
/**
* Implementation of hook_cron().
*/
function webform_protected_downloads_cron() {
// delete expired hash codes, note the missing alias for the first table,
// this is necessary to support sqlite
db_query("DELETE
FROM {wpd_access_hashes}
WHERE (expires < :expires AND expires != 0)
OR EXISTS (SELECT 1 FROM {webform_submissions} s
LEFT JOIN {wpd_node_configuration} n USING (nid)
WHERE (s.sid = {wpd_access_hashes}.sid AND n.access_type = :access_type AND {wpd_access_hashes}.used != 0))",
array(
':expires' => time(),
':access_type' => WEBFORM_PROTECTED_DOWNLOADS_ACCESS_TYPE_SINGLE
)
);
}
/**
* Implementation of hook_node_load().
*/
function webform_protected_downloads_node_load($nodes, $types) {
foreach ($nodes as $nid => $node) {
if (webform_protected_downloads_node_is_webform($node) && !isset($node->wpd['valid'])) {
// check if the node has protected files
if (webform_protected_downloads_node_has_protected_files($node->nid)) {
// check if the registered component still exists, needed, because we
// can't prevent webform from deleting a component that we might use
// as a mail field for the protected downloads
$result = db_query("SELECT COUNT(*) FROM {wpd_node_configuration} n LEFT JOIN {webform_component} c ON c.cid = n.mail_field_cid AND c.nid = n.nid WHERE n.nid = :nid AND c.cid IS NOT NULL", array(':nid' => $node->nid))->fetchAssoc();
$nodes[$nid]->wpd['valid'] = $result > 0;
}
else {
$nodes[$nid]->wpd['valid'] = TRUE;
}
// attach any private files to this node
$node->wpd['private_files'] = webform_protected_downloads_node_get_private_files($node);
// attach any protected files to this node
$node->wpd['protected_files'] = array();
foreach ($node->wpd['private_files'] as $file) {
if ($file->protected) {
$node->wpd['protected_files'][$file->fid] = $file;
}
}
// attach wpd configuration for this node
$node->wpd['config'] = webform_protected_downloads_get_configuration($node->nid);
$node->wpd['nid'] = $node->nid;
}
}
}
/**
* Implementation of hook_insert().
*/
function webform_protected_downloads_node_insert($node) {
// check for webform enabled node types and programatically created nodes
// with a wpd configuration
if (webform_protected_downloads_node_is_webform($node) && isset($node->wpd)
&& $node->nid != $node->wpd['nid']) {
// a new node has probably been created programatically or by a helper
// module like node_clone, here we must therefore save all the necessary
// data
webform_protected_downloads_set_configuration($node->nid, (array) $node->wpd['config']);
foreach ($node->wpd['protected_files'] as $file) {
webform_protected_downloads_file_set_protected($node->nid, $file->fid, TRUE);
}
}
}
/**
* Implementation of hook_node_view().
*/
function webform_protected_downloads_node_view($node, $view_mode, $langcode) {
if (!webform_protected_downloads_node_is_webform($node)) {
return;
}
// check if the node has protected files
if (webform_protected_downloads_node_has_protected_files($node->nid)) {
// make sure that protected files are not displayed
$disabled = array();
foreach ($node->wpd['protected_files'] as $file) {
if (!isset($node->content[$file->field])) {
// the attribute is only there for files that have been set to display
// on the node edit form and that have been protected afterwards via
// this module, once the node form is hit and saved the property is no
// more there, that's why we need to check that here
continue;
}
foreach ($node->content[$file->field]['#items'] as $key => $item) {
if ($item['fid'] == $file->fid) {
// disable access
$node->content[$file->field][$key]['#access'] = FALSE;
// count the number of protected files for this field
$disabled[$file->field] = isset($disabled[$file->field]) ? $disabled[$file->field] + 1 : 1;
}
}
}
// disable the whole fieldset if all files for a field are protected
foreach ($disabled as $field => $count) {
if (!isset($node->content[$field]['#items']) || count($node->content[$field]['#items']) <= $count) {
$node->content[$field]['#access'] = FALSE;
}
}
}
}
/**
* Implementation of hook_node_delete().
*
* @param object $node
* @return void
*/
function webform_protected_downloads_node_delete($node) {
// delete our own references and configuration for this node
db_delete("wpd_node_configuration")->condition('nid', $node->nid)->execute();
db_delete("wpd_protected_files")->condition('nid', $node->nid)->execute();
// delete all entries in table wpd_access_hashes that are no longer used,
// note the missing alias for the first table, this is necessary to support
// sqlite
$sql = "DELETE
FROM {wpd_access_hashes}
WHERE NOT EXISTS (SELECT 1 FROM {webform_submissions} s WHERE s.sid = {wpd_access_hashes}.sid)";
db_query($sql);
}
/**
* Check whether the given node is used as a webform
*
* @param object $node
* @return boolean
*/
function webform_protected_downloads_node_is_webform($node) {
// API change introduced by https://drupal.org/node/2062235
if (function_exists('webform_node_types')) {
$webform_types = webform_node_types();
}
else {
$webform_types = webform_variable_get('webform_node_types');
}
return in_array($node->type, $webform_types);
}
/**
* Retrieve all private file fields.
*
* This actually retrieves fields of types 'file' and 'image'.
*
* @param string $node
* @return array
*/
function webform_protected_downloads_node_get_private_file_fields($node) {
$private_file_fields = &drupal_static(__FUNCTION__);
if (!isset($private_file_fields[$node->nid])) {
$private_file_fields[$node->nid] = array();
$field_instances = field_info_instances('node', $node->type);
foreach ($field_instances as $field_name => $field) {
$field_info = field_info_field($field_name);
if (in_array($field_info['type'], array('file', 'image')) && $field_info['settings']['uri_scheme'] == 'private') {
$private_file_fields[$node->nid][] = $field_name;
}
}
}
return $private_file_fields[$node->nid];
}
/**
* Retrieve private files for the given node
*
* @param object $node
* @return array
*/
function webform_protected_downloads_node_get_private_files($node) {
$private_file_fields = webform_protected_downloads_node_get_private_file_fields($node);
if (!count($private_file_fields)) {
return array();
}
/*
When trying to retrieve attached private fields, we need to pay attention
to language handling, possibilities:
1. Node has no language (locale disabled) and file has no language either
2. Node has a language but file has no language
3. Node has a language and file has a language
For now all attached files are treated the same.
See http://drupal.org/node/1239916 for examples of arising problems.
*/
$private_files = array();
foreach ($private_file_fields as $field) {
// check if the node has acceptable file fields
if (!is_array($node->$field) || !count($node->$field)) {
continue;
}
// iterate over possible fields, in the node object they are organised by a
// language code, so in the first iteration we go into the langcode
foreach ($node->$field as $langcode => $files) {
// now iterate over the specific files
foreach ($files as $key => $file) {
$file = (object) $file;
$file->field = $field;
$file->nid = $node->nid;
$file->weight = $key;
$file->protected = webform_protected_downloads_file_is_protected($node->nid, $file->fid);
$private_files[$file->fid] = $file;
}
}
}
return $private_files;
}
/**
* Implementation of hook_help().
*/
function webform_protected_downloads_help($path, $arg) {
$output = '';
switch ($path) {
case 'admin/help#webform_protected_downloads':
$output = t("
'. t("This page displays files that are currently attached to this webform. You can select one or more of these files to be protected downloads. This means, that they won't be listed on the normal webform view page. Instead when the user submits the form, he receives an email to a given mail (you can choose any webform component of the type mail that you have already added to this webform) containing a link to download the protected file.") .'
'; break; } return $output; } /** * Set the protected status for the given node / file combination * * @param int $nid * @param int $fid * @param boolean $protected * @return void */ function webform_protected_downloads_file_set_protected($nid, $fid, $protected) { if (webform_protected_downloads_file_is_protected($nid, $fid) && !$protected) { db_delete('wpd_protected_files')->condition('nid', $nid)->condition('fid', $fid)->execute(); } elseif (!webform_protected_downloads_file_is_protected($nid, $fid) && $protected) { $record = array('nid' => $nid, 'fid' => $fid, 'created' => time()); drupal_write_record('wpd_protected_files', $record); } } /** * Checks wheather the given file is protected for the given node * * @param int $nid * @param int $fid * @return boolean */ function webform_protected_downloads_file_is_protected($nid, $fid) { $wpd_protected = &drupal_static(__FUNCTION__); if (!isset($wpd_protected[$nid])) { $wpd_protected[$nid] = array(); $result = db_query("SELECT fid FROM {wpd_protected_files} WHERE nid = :nid", array(':nid' => $nid)); while ($row = $result->fetchObject()) { if (!in_array($row->fid, $wpd_protected[$nid])) { $wpd_protected[$nid][] = $row->fid; } } } return isset($wpd_protected[$nid]) ? in_array($fid, $wpd_protected[$nid]) : FALSE; } /** * Checks wheather the current user has access to the given file for the given * node * * @param int $nid * @param int $fid * @return boolean */ function webform_protected_downloads_file_user_has_access($nid, $fid) { // if it is protected, allow access only if the hash has been added to the // session if (!isset($_SESSION[WEBFORM_PROTECTED_DOWNLOADS_SESSION_KEY])) { return FALSE; } $sql = "SELECT expires, hash FROM {wpd_protected_files} p LEFT JOIN {webform_submissions} s ON (s.nid = p.nid) LEFT JOIN {wpd_node_configuration} n ON (p.nid = n.nid) LEFT JOIN {wpd_access_hashes} h USING(sid) WHERE p.nid = :nid AND p.fid = :fid AND expires IS NOT NULL AND hash IS NOT NULL AND (n.retroactive = 1 OR (n.retroactive = 0 AND s.submitted > p.created))"; $args = array(':nid' => $nid, ':fid' => $fid); $result = db_query($sql, $args); while ($row = $result->fetchObject()) { $ok = FALSE; // necessary condition if (isset($_SESSION[WEBFORM_PROTECTED_DOWNLOADS_SESSION_KEY][$row->hash])) { $session_expires = $_SESSION[WEBFORM_PROTECTED_DOWNLOADS_SESSION_KEY][$row->hash]['expires']; if ($session_expires == 0 || $session_expires > time()) { $ok = TRUE; } else { unset($_SESSION[WEBFORM_PROTECTED_DOWNLOADS_SESSION_KEY][$row->hash]); $ok = FALSE; } } else { $ok = FALSE; } $ok = $ok && ($row->expires == 0 || $row->expires > time()); if ($ok) { return TRUE; } } return FALSE; } /** * Checks if the given node has protected files * * @param int $nid * @return void */ function webform_protected_downloads_node_has_protected_files($nid) { $wpd_nodes = &drupal_static(__FUNCTION__); if (!isset($wpd_nodes[$nid])) { $count = db_query("SELECT COUNT(*) FROM {wpd_protected_files} WHERE nid = :nid", array(':nid' => $nid))->fetchColumn(); $wpd_nodes[$nid] = $count > 0; } return $wpd_nodes[$nid]; } /** * Set the configuration for the given node * * @param int $nid * @param int $cid * @param string $subject * @param string $body * @param string $text_access * @param string $text_noaccess * @return void */ function webform_protected_downloads_set_configuration($nid, $args) { $configuration = array( 'nid' => $nid, 'mail_field_cid' => $args['mail_field_cid'], 'mail_from' => $args['mail_from'], 'mail_subject' => $args['mail_subject'], 'mail_body' => $args['mail_body'], 'access_type' => $args['access_type'], 'expiration_download' => $args['expiration_download'], 'expiration_session' => $args['expiration_session'], 'retroactive' => $args['retroactive'], 'redirect' => $args['redirect'], 'text_download_access' => $args['text_download_access'], 'text_download_access_format' => $args['text_download_access_format'], 'text_download_noaccess' => $args['text_download_noaccess'], 'text_download_noaccess_format' => $args['text_download_noaccess_format'], ); drupal_write_record('wpd_node_configuration', $configuration, webform_protected_downloads_get_configuration($nid) ? array('nid') : array()); } /** * Get the configuration for the given node * * @param int $nid * @param string $field optional * @return void */ function webform_protected_downloads_get_configuration($nid, $field = NULL, $default = NULL) { $wpd_node_conf = &drupal_static(__FUNCTION__); if (!isset($wpd_node_conf[$nid])) { $wpd_node_conf[$nid] = db_query("SELECT * FROM {wpd_node_configuration} WHERE nid = :nid", array(':nid' => $nid))->fetchObject(); } if ($wpd_node_conf[$nid]) { return $field !== NULL ? (isset($wpd_node_conf[$nid]->$field) && !empty($wpd_node_conf[$nid]->$field) ? $wpd_node_conf[$nid]->$field : $default) : $wpd_node_conf[$nid]; } else { return FALSE; } } /** * Process unprocessed webform submissions * * @return void */ function webform_protected_downloads_process_submissions($form, &$form_state) { $sid = $form_state['values']['details']['sid']; $nid = $form_state['values']['details']['nid']; // check whether the node has protected files, otherwise we can skip the // following steps if (!webform_protected_downloads_node_has_protected_files($nid)) { return; } // load the configuration for this node $conf = webform_protected_downloads_get_configuration($nid); // this should not happen, if it does something is seriously not working and // we can't figurue out where to send the mail, so skip it if (!isset($form_state['values']['submitted'][$conf->mail_field_cid])) { _webform_protected_downloads_log('Problem with node !nid: The mail field could not be found in the form submission. No e-mail has been send.', array('!nid' => $nid), WATCHDOG_ERROR); return; } // now get the mail adress that should be used $mail = $form_state['values']['submitted'][$conf->mail_field_cid]; // create hash, calculate expiration timestamp $hash = webform_protected_downloads_create_hash(); $processed = time(); $expires = $conf->expiration_download == 0 ? 0 : $processed + $conf->expiration_download; // we need to save before sending the mail $record = array( 'sid' => $sid, 'hash' => $hash, 'processed' => $processed, 'expires' => $expires, 'used' => 0, ); drupal_write_record('wpd_access_hashes', $record); // now send the mail webform_protected_downloads_send_mail($sid, $nid, $mail, $hash); if ($conf->redirect) { $form_state['redirect'] = 'node/' . $nid . '/download/' . $hash; } } /** * Create a hash that users can use to access the download page * * @param string $row * @return void */ function webform_protected_downloads_create_hash() { $seed = 'JvKnrQWPsThuJteNQAuH'; $hash = sha1(uniqid($seed . mt_rand(), true)); $hash = substr($hash, 0, 32); return $hash; } /** * Retrieve details for this hash * * @param string $hash * @return void */ function webform_protected_downloads_get_hash_details($hash) { $wpd_hash = &drupal_static(__FUNCTION__); if (!isset($wpd_hash[$hash])) { $result = db_query("SELECT * FROM {wpd_access_hashes} WHERE hash = :hash", array(':hash' => $hash)); $wpd_hash[$hash] = $result->fetchObject(); } return $wpd_hash[$hash]; } /** * Retrieve the webform node based on the given hash * * @param string $hash * @return void */ function webform_protected_downloads_get_node_from_hash($hash) { $result = db_query("SELECT nid FROM {wpd_access_hashes} LEFT JOIN {webform_submissions} USING(sid) WHERE hash = :hash", array(':hash' => $hash)); $row = $result->fetchObject(); return isset($row->nid) ? $row->nid : FALSE; } /** * Send mail to the user with a valid hash so that he can access the download page * * @param string $mail * @param string $hash * @return void */ function webform_protected_downloads_send_mail($sid, $nid, $mail, $hash) { global $user; // choose the language $language = $user->uid ? user_preferred_language($user) : language_default(); // load the webform node, needed for token replacement $node = node_load($nid); // get the submission, including all it's data $submission = webform_get_submission($nid, $sid); // the sender address $from = webform_protected_downloads_get_configuration($nid, 'mail_from', variable_get('site_mail', NULL)); // build the subject $subject = webform_protected_downloads_get_configuration($nid, 'mail_subject'); $subject = _webform_protected_downloads_token_replace($subject, $node, $hash); $subject = _webform_filter_values($subject, $node, $submission, $email = NULL, $strict = FALSE, $allow_anonymous = TRUE); // build the body $body = webform_protected_downloads_get_configuration($nid, 'mail_body'); $body = _webform_protected_downloads_token_replace($body, $node, $hash); $body = _webform_filter_values($body, $node, $submission, $email = NULL, $strict = FALSE, $allow_anonymous = TRUE); $params = array( 'subject' => $subject, 'body' => $body, 'From' => $from, ); // send the mail drupal_mail('webform_protected_downloads', 'confirmation', $mail, $language, $params, $from); } /** * Implementation of hook_mail(). */ function webform_protected_downloads_mail($key, &$message, $params) { switch($key) { case 'confirmation': $message['subject'] = $params['subject']; $message['body'][] = $params['body']; break; } } /** * Helper function for token replacement * * @param string $string * @param object $node * @param string $hash * @return string */ function _webform_protected_downloads_token_replace($string, $node, $hash) { return token_replace($string, array('node' => $node, 'hash' => $hash)); } /** * Implementation of hook_form_alter(). * * @param array $form * @param array $form_state * @param string $form_id * @return void */ function webform_protected_downloads_form_alter(&$form, &$form_state, $form_id) { if (strpos($form_id, 'webform_client_form_') !== FALSE) { // trigger processing after the form has been submitted and handled by webform $form['#submit'][] = 'webform_protected_downloads_process_submissions'; } // update the file upload form, so that protected files can't be deleted // and listed if (strpos($form_id, '_node_form') !== FALSE && isset($form['#node']) && webform_protected_downloads_node_is_webform($form['#node'])) { if (isset($form['#node']->nid) && webform_protected_downloads_node_has_protected_files($form['#node']->nid)) { foreach ($form['#node']->wpd['protected_files'] as $file) { $file_item = &$form[$file->field][$form[$file->field]['#language']]; $file_item[$file->weight]['#default_value']['display'] = 0; if (!in_array('webform_protected_downloads_file_widget_after_build', $file_item['#after_build'])) { $file_item['#after_build'][] = 'webform_protected_downloads_file_widget_after_build'; } } } } switch ($form_id) { // disable redirect options on the form configuration form if the redirect // to the downloads page has been activated for protected downloads case 'webform_configure_form': if (webform_protected_downloads_get_configuration($form['nid']['#value'], 'redirect')) { $form['submission']['redirection']['redirect']['#attributes']['disabled'] = 'disabled'; $form['submission']['redirection']['redirect_url']['#attributes']['disabled'] = 'disabled'; $form['submission']['redirection']['#description'] .= '