Index: CHANGELOG.txt =================================================================== RCS file: /cvs/drupal/drupal/CHANGELOG.txt,v retrieving revision 1.336 diff -u -p -r1.336 CHANGELOG.txt --- CHANGELOG.txt 25 Aug 2009 10:58:31 -0000 1.336 +++ CHANGELOG.txt 27 Aug 2009 04:12:12 -0000 @@ -43,6 +43,7 @@ Drupal 7.0, xxxx-xx-xx (development vers * Redesigned the add content type screen. * Highlight duplicate URL aliases. * Renamed "input formats" to "text formats". + * Moved text format permissions to the main permissions page. * Added configurable ability for users to cancel their own accounts. * Added "vertical tabs", a reusable interface component that features automatic summaries and increases usability. @@ -80,6 +81,7 @@ Drupal 7.0, xxxx-xx-xx (development vers fallback to the system time zone and will have to be reconfigured by each user. - Filter system: * Refactored the HTML corrector to take advantage of PHP 5 features. + * Added support for default text formats to be assigned on a per-role basis. - Removed ping module: * Contributed modules with similar functionality are available. - Removed per-user themes: Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.366 diff -u -p -r1.366 form.inc --- includes/form.inc 25 Aug 2009 21:16:31 -0000 1.366 +++ includes/form.inc 27 Aug 2009 04:12:12 -0000 @@ -1863,7 +1863,7 @@ function form_process_radios($element) { * $form['body'] = array( * '#type' => 'textarea', * '#title' => t('Body'), - * '#text_format' => isset($node->format) ? $node->format : FILTER_FORMAT_DEFAULT, + * '#text_format' => isset($node->format) ? $node->format : filter_default_format(), * ); * @endcode * Index: modules/block/block.module =================================================================== RCS file: /cvs/drupal/drupal/modules/block/block.module,v retrieving revision 1.368 diff -u -p -r1.368 block.module --- modules/block/block.module 26 Aug 2009 10:53:45 -0000 1.368 +++ modules/block/block.module 27 Aug 2009 04:12:12 -0000 @@ -205,7 +205,7 @@ function block_block_list() { * Implement hook_block_configure(). */ function block_block_configure($delta = 0) { - $box = array('format' => FILTER_FORMAT_DEFAULT); + $box = array('format' => filter_default_format()); if ($delta) { $box = block_box_get($delta); } @@ -398,7 +398,7 @@ function block_box_form($edit = array()) '#type' => 'textarea', '#title' => t('Block body'), '#default_value' => $edit['body'], - '#text_format' => isset($edit['format']) ? $edit['format'] : FILTER_FORMAT_DEFAULT, + '#text_format' => isset($edit['format']) ? $edit['format'] : filter_default_format(), '#rows' => 15, '#description' => t('The content of the block as shown to the user.'), '#required' => TRUE, @@ -856,9 +856,9 @@ function template_preprocess_block(&$var /** * Implement hook_filter_format_delete(). */ -function block_filter_format_delete($format, $default) { +function block_filter_format_delete($format, $fallback) { db_update('box') - ->fields(array('format' => $default->format)) + ->fields(array('format' => $fallback->format)) ->condition('format', $format->format) ->execute(); } Index: modules/comment/comment.module =================================================================== RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v retrieving revision 1.762 diff -u -p -r1.762 comment.module --- modules/comment/comment.module 26 Aug 2009 10:28:45 -0000 1.762 +++ modules/comment/comment.module 27 Aug 2009 04:12:12 -0000 @@ -1809,7 +1809,7 @@ function comment_form(&$form_state, $com '#title' => t('Comment'), '#rows' => 15, '#default_value' => $default, - '#text_format' => isset($comment->format) ? $comment->format : FILTER_FORMAT_DEFAULT, + '#text_format' => isset($comment->format) ? $comment->format : filter_default_format(), '#required' => TRUE, ); @@ -2446,9 +2446,9 @@ function comment_menu_alter(&$items) { /** * Implement hook_filter_format_delete(). */ -function comment_filter_format_delete($format, $default) { +function comment_filter_format_delete($format, $fallback) { db_update('comment') - ->fields(array('format' => $default->format)) + ->fields(array('format' => $fallback->format)) ->condition('format', $format->format) ->execute(); } Index: modules/field/modules/text/text.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/text/text.module,v retrieving revision 1.25 diff -u -p -r1.25 text.module --- modules/field/modules/text/text.module 27 Aug 2009 00:33:52 -0000 1.25 +++ modules/field/modules/text/text.module 27 Aug 2009 04:12:12 -0000 @@ -558,14 +558,14 @@ function text_elements() { '#columns' => array('value', 'format'), '#delta' => 0, '#process' => array('text_textarea_elements_process'), '#theme_wrappers' => array('text_textarea'), - '#filter_value' => FILTER_FORMAT_DEFAULT, + '#filter_value' => filter_default_format(), ), 'text_textarea_with_summary' => array( '#input' => TRUE, '#columns' => array('value', 'format', 'summary'), '#delta' => 0, '#process' => array('text_textarea_with_summary_process'), '#theme_wrappers' => array('text_textarea'), - '#filter_value' => FILTER_FORMAT_DEFAULT, + '#filter_value' => filter_default_format(), ), ); } @@ -667,7 +667,7 @@ function text_textfield_elements_process if (!empty($instance['settings']['text_processing'])) { $filter_key = (count($element['#columns']) == 2) ? $element['#columns'][1] : 'format'; - $format = isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : FILTER_FORMAT_DEFAULT; + $format = isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : filter_default_format(); $element[$field_key]['#text_format'] = $format; } @@ -700,7 +700,7 @@ function text_textarea_elements_process( if (!empty($instance['settings']['text_processing'])) { $filter_key = (count($element['#columns']) == 2) ? $element['#columns'][1] : 'format'; - $format = isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : FILTER_FORMAT_DEFAULT; + $format = isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : filter_default_format(); $element[$field_key]['#text_format'] = $format; } @@ -748,7 +748,7 @@ function text_textarea_with_summary_proc if (!empty($instance['settings']['text_processing'])) { $filter_key = (count($element['#columns']) == 2) ? $element['#columns'][1] : 'format'; - $format = isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : FILTER_FORMAT_DEFAULT; + $format = isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : filter_default_format(); $element[$field_key]['#text_format'] = $format; } @@ -769,7 +769,7 @@ function text_field_widget_formatted_tex // The format selector uses #access = FALSE if only one format is // available. In this case, we don't receive its value, and need to // manually set it. - $edit['format'] = !empty($edit[$default_key]) ? $edit[$default_key] : filter_resolve_format(FILTER_FORMAT_DEFAULT); + $edit['format'] = !empty($edit[$default_key]) ? $edit[$default_key] : filter_default_format(); unset($edit[$default_key]); return $edit; } Index: modules/filter/filter.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/filter/filter.admin.inc,v retrieving revision 1.40 diff -u -p -r1.40 filter.admin.inc --- modules/filter/filter.admin.inc 26 Aug 2009 10:28:45 -0000 1.40 +++ modules/filter/filter.admin.inc 27 Aug 2009 04:12:12 -0000 @@ -7,46 +7,40 @@ */ /** - * Menu callback; Displays a list of all text formats and which - * one is the default. + * Menu callback; displays a list of all text formats and allows them to be + * rearranged. * * @ingroup forms * @see filter_admin_overview_submit() */ function filter_admin_overview() { - // Overview of all formats. $formats = filter_formats(); - $error = FALSE; + $fallback_format = filter_fallback_format(); $form = array('#tree' => TRUE); foreach ($formats as $id => $format) { - $roles = array(); - foreach (user_roles() as $rid => $name) { - // Prepare a roles array with roles that may access the filter. - if (strpos($format->roles, ",$rid,") !== FALSE) { - $roles[] = $name; - } - } - $default = ($id == variable_get('filter_default_format', 1)); - $options[$id] = ''; - $form[$id]['name'] = array('#markup' => $format->name); - $form[$id]['roles'] = array('#markup' => $default ? t('All roles may use the default format') : ($roles ? implode(', ', $roles) : t('No roles may use this format'))); + // Check whether this is the fallback text format. This format is available + // to all roles and cannot be deleted via the admin interface. + $is_fallback_format = ($id == $fallback_format); + $form[$id]['name'] = array('#markup' => $is_fallback_format ? theme('placeholder', $format->name) : check_plain($format->name)); + if ($is_fallback_format) { + $roles_markup = theme('placeholder', t('All roles may use this format')); + } + else { + $roles = filter_format_roles($format); + $roles_markup = $roles ? implode(', ', $roles) : t('No roles may use this format'); + } + $form[$id]['roles'] = array('#markup' => $roles_markup); $form[$id]['configure'] = array('#markup' => l(t('configure'), 'admin/settings/formats/' . $id)); - $form[$id]['delete'] = array('#markup' => $default ? '' : l(t('delete'), 'admin/settings/formats/delete/' . $id)); + $form[$id]['delete'] = array('#markup' => $is_fallback_format ? '' : l(t('delete'), 'admin/settings/formats/delete/' . $id)); $form[$id]['weight'] = array('#type' => 'weight', '#default_value' => $format->weight); } - $form['default'] = array('#type' => 'radios', '#options' => $options, '#default_value' => variable_get('filter_default_format', 1)); $form['submit'] = array('#type' => 'submit', '#value' => t('Save changes')); return $form; } function filter_admin_overview_submit($form, &$form_state) { - // Process form submission to set the default format. - if (is_numeric($form_state['values']['default'])) { - drupal_set_message(t('Default format updated.')); - variable_set('filter_default_format', $form_state['values']['default']); - } foreach ($form_state['values'] as $id => $data) { if (is_array($data) && isset($data['weight'])) { // Only update if this is a form element with weight. @@ -56,6 +50,7 @@ function filter_admin_overview_submit($f ->execute(); } } + filter_format_reset_cache(); drupal_set_message(t('The text format ordering has been saved.')); } @@ -72,9 +67,8 @@ function theme_filter_admin_overview($fo $element['weight']['#attributes']['class'] = array('text-format-order-weight'); $rows[] = array( 'data' => array( - check_plain($element['name']['#markup']), + drupal_render($element['name']), drupal_render($element['roles']), - drupal_render($form['default'][$id]), drupal_render($element['weight']), drupal_render($element['configure']), drupal_render($element['delete']), @@ -84,7 +78,7 @@ function theme_filter_admin_overview($fo unset($form[$id]); } } - $header = array(t('Name'), t('Roles'), t('Default'), t('Weight'), array('data' => t('Operations'), 'colspan' => 2)); + $header = array(t('Name'), t('Roles'), t('Weight'), array('data' => t('Operations'), 'colspan' => 2)); $output = theme('table', $header, $rows, array('id' => 'text-format-order')); $output .= drupal_render_children($form); @@ -99,8 +93,9 @@ function theme_filter_admin_overview($fo function filter_admin_format_page($format = NULL) { if (!isset($format->name)) { drupal_set_title(t('Add text format'), PASS_THROUGH); - $format = (object)array('name' => '', 'roles' => '', 'format' => ''); + $format = (object)array('name' => '', 'format' => ''); } + filter_admin_display_warning($format); return drupal_get_form('filter_admin_format_form', $format); } @@ -112,12 +107,6 @@ function filter_admin_format_page($forma * @see filter_admin_format_form_submit() */ function filter_admin_format_form(&$form_state, $format) { - $default = ($format->format == variable_get('filter_default_format', 1)); - if ($default) { - $help = t('All roles for the default format must be enabled and cannot be changed.'); - $form['default_format'] = array('#type' => 'hidden', '#value' => 1); - } - $form['name'] = array('#type' => 'textfield', '#title' => t('Name'), '#default_value' => $format->name, @@ -125,23 +114,6 @@ function filter_admin_format_form(&$form '#required' => TRUE, ); - // Add a row of checkboxes for form group. - $form['roles'] = array('#type' => 'fieldset', - '#title' => t('Roles'), - '#description' => $default ? $help : t('Choose which roles may use this text format. Note that roles with the "administer filters" permission can always use all text formats.'), - '#tree' => TRUE, - ); - - foreach (user_roles() as $rid => $name) { - $checked = strpos($format->roles, ",$rid,") !== FALSE; - $form['roles'][$rid] = array('#type' => 'checkbox', - '#title' => $name, - '#default_value' => ($default || $checked), - ); - if ($default) { - $form['roles'][$rid]['#disabled'] = TRUE; - } - } // Table with filters $filter_info = filter_get_filters(); $enabled = filter_list_format($format->format); @@ -220,6 +192,19 @@ function filter_admin_format_form_submit } /** + * Displays a warning about which roles can use a text format. + * + * @param $format + * An object representing the text format. + */ +function filter_admin_display_warning($format) { + $roles = filter_format_roles($format); + if (!empty($roles)) { + drupal_set_message(t('This text format is available to the following roles: %roles. Since text formats can have security implications depending on how they are configured, you should be careful making changes here if any of these roles are untrusted.', array('%roles' => implode(', ', $roles))), 'warning'); + } +} + +/** * Menu callback; confirm deletion of a format. * * @ingroup forms @@ -227,19 +212,19 @@ function filter_admin_format_form_submit */ function filter_admin_delete(&$form_state, $format) { if ($format) { - if ($format->format != variable_get('filter_default_format', 1)) { + if ($format->format != filter_fallback_format()) { $form['#format'] = $format; return confirm_form($form, t('Are you sure you want to delete the text format %format?', array('%format' => $format->name)), 'admin/settings/formats', - t('If you have any content left in this text format, it will be switched to the default text format. This action cannot be undone.'), + t('If you have any content left in this text format, it will be switched to the %fallback text format. This action cannot be undone.', array('%fallback' => filter_fallback_format_title())), t('Delete'), t('Cancel') ); } else { - drupal_set_message(t('The default format cannot be deleted.')); + drupal_set_message(t('The %fallback format cannot be deleted.', array('%fallback' => filter_fallback_format_title()))); drupal_goto('admin/settings/formats'); } } @@ -259,12 +244,12 @@ function filter_admin_delete_submit($for $form_state['redirect'] = 'admin/settings/formats'; } - /** * Menu callback; display settings defined by a format's filters. */ function filter_admin_configure_page($format) { drupal_set_title(t("Configure %format", array('%format' => $format->name)), PASS_THROUGH); + filter_admin_display_warning($format); return drupal_get_form('filter_admin_configure', $format); } @@ -309,6 +294,7 @@ function filter_admin_configure_submit($ */ function filter_admin_order_page($format) { drupal_set_title(t("Rearrange %format", array('%format' => $format->name)), PASS_THROUGH); + filter_admin_display_warning($format); return drupal_get_form('filter_admin_order', $format); } Index: modules/filter/filter.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/filter/filter.api.php,v retrieving revision 1.12 diff -u -p -r1.12 filter.api.php --- modules/filter/filter.api.php 26 Aug 2009 10:28:45 -0000 1.12 +++ modules/filter/filter.api.php 27 Aug 2009 04:12:12 -0000 @@ -152,20 +152,21 @@ function hook_filter_format_update($form * * It is recommended for modules to implement this hook, when they store * references to text formats to replace existing references to the deleted - * text format with the default format. + * text format with the fallback format. * * @param $format * The format object of the format being deleted. - * @param $default - * The format object of the site's default format. + * @param $fallback + * The format object of the site's fallback format, which is always available + * to all users. * * @see hook_filter_format_update(). * @see hook_filter_format_delete(). */ -function hook_filter_format_delete($format, $default) { - // Replace the deleted format with the default format. +function hook_filter_format_delete($format, $fallback) { + // Replace the deleted format with the fallback format. db_update('my_module_table') - ->fields(array('format' => $default->format)) + ->fields(array('format' => $fallback->format)) ->condition('format', $format->format) ->execute(); } Index: modules/filter/filter.install =================================================================== RCS file: /cvs/drupal/drupal/modules/filter/filter.install,v retrieving revision 1.18 diff -u -p -r1.18 filter.install --- modules/filter/filter.install 26 Aug 2009 10:17:54 -0000 1.18 +++ modules/filter/filter.install 27 Aug 2009 04:12:12 -0000 @@ -69,13 +69,6 @@ function filter_schema() { 'default' => '', 'description' => 'Name of the text format (Filtered HTML).', ), - 'roles' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => 'A comma-separated string of roles; references {role}.rid.', // This is bad since you can't use joins, nor index. - ), 'cache' => array( 'type' => 'int', 'not null' => TRUE, @@ -104,6 +97,11 @@ function filter_schema() { } /** + * @defgroup updates-6.x-to-7.x Filter updates from 6.x to 7.x + * @{ + */ + +/** * Add a weight column to the filter formats table. */ function filter_update_7000() { @@ -187,3 +185,97 @@ function filter_update_7003() { return $ret; } +/** + * Integrate text formats with the user permissions system. + * + * This function converts text format role assignments to use the new text + * format permissions introduced in Drupal 7, creates a fallback (plain text) + * format that is available to all users, and explicitly sets the text format + * in cases that used to rely on a single site-wide default. + */ +function filter_update_7004() { + $ret = array(); + + // Move role data from the filter system to the user permission system. + $all_roles = array_keys(user_roles()); + $default_format = variable_get('filter_default_format', 1); + $result = db_query("SELECT * FROM {filter_format}"); + foreach ($result as $format) { + // We need to assign the default format to all roles (regardless of what + // was stored in the database) to preserve the behavior of the site at the + // moment of the upgrade. + $format_roles = ($format->format == $default_format ? $all_roles : explode(',', $format->roles)); + foreach ($format_roles as $format_role) { + if (in_array($format_role, $all_roles)) { + db_insert('role_permission') + ->fields(array( + 'rid' => $format_role, + 'permission' => filter_permission_name($format), + )) + ->execute(); + } + } + } + + // Drop the roles field from the {filter_format} table. + db_drop_field($ret, 'filter_format', 'roles'); + + // Add a fallback text format which appears last on the list for all users. + $fallback_format = db_insert('filter_format') + ->fields(array( + 'name' => 'Plain text', + 'cache' => 1, + 'weight' => 1, + )) + ->execute(); + variable_set('filter_fallback_format', $fallback_format); + drupal_set_message('A new Plain text format has been created which will be available to all users. You can configure this text format on the text format configuration page.'); + + // The fallback text format should output plain text, so we escape all HTML + // and apply the line break filter only. + db_insert('filter') + ->fields(array('format', 'module', 'name', 'weight')) + ->values(array( + 'format' => $fallback_format, + 'module' => 'filter', + 'name' => 'filter_html_escape', + 'weight' => 0, + )) + ->values(array( + 'format' => $fallback_format, + 'module' => 'filter', + 'name' => 'filter_autop', + 'weight' => 1, + )) + ->execute(); + + // Move the former site-wide default text format to the top of the list, so + // that it continues to be the default text format for all users. + db_update('filter_format') + ->fields(array('weight' => -1)) + ->condition('format', $default_format) + ->execute(); + + // It was previously possible for a value of "0" to be stored in database + // tables to indicate that a particular piece of text should be filtered + // using the default text format. Therefore, we have to convert all such + // instances (in Drupal core) to explicitly use the appropriate format. + // Note that the update of the node body field is handled separately, in + // node_update_7006(). + foreach (array('box', 'comment') as $table) { + db_update($table) + ->fields(array('format' => $default_format)) + ->condition('format', 0) + ->execute(); + } + + // Note: We do not delete the 'filter_default_format' variable, since other + // modules may need it in their update functions. + // @TODO: This variable can probably be deleted in Drupal 8. + return $ret; +} + +/** + * @} End of "defgroup updates-6.x-to-7.x" + * The next series of updates should start at 8000. + */ Index: modules/filter/filter.module =================================================================== RCS file: /cvs/drupal/drupal/modules/filter/filter.module,v retrieving revision 1.282 diff -u -p -r1.282 filter.module --- modules/filter/filter.module 26 Aug 2009 10:28:45 -0000 1.282 +++ modules/filter/filter.module 27 Aug 2009 04:12:12 -0000 @@ -7,14 +7,6 @@ */ /** - * Special format ID which means "use the default format". - * - * This value can be passed to the filter APIs as a format ID: this is - * equivalent to not passing an explicit format at all. - */ -define('FILTER_FORMAT_DEFAULT', 0); - -/** * Implement hook_help(). */ function filter_help($path, $arg) { @@ -22,12 +14,12 @@ function filter_help($path, $arg) { case 'admin/help#filter': $output = '

' . t("The filter module allows administrators to configure text formats for use on your site. A text format defines the HTML tags, codes, and other input allowed in both content and comments, and is a key feature in guarding against potentially damaging input from malicious users. Two formats included by default are Filtered HTML (which allows only an administrator-approved subset of HTML tags) and Full HTML (which allows the full set of HTML tags). Additional formats may be created by an administrator.") . '

'; $output .= '

' . t('Each text format uses filters to manipulate text, and most formats apply several different filters to text in a specific order. Each filter is designed for a specific purpose, and generally either adds, removes or transforms elements within user-entered text before it is displayed. A filter does not change the actual content of a post, but instead, modifies it temporarily before it is displayed. A filter may remove unapproved HTML tags, for instance, while another automatically adds HTML to make links referenced in text clickable.') . '

'; - $output .= '

' . t('Users with access to more than one text format can use the Text format fieldset to choose between available text formats when creating or editing multi-line content. Administrators determine the text formats available to each user role, select a default text format, and control the order of formats listed in the Text format fieldset.') . '

'; + $output .= '

' . t('Users with access to more than one text format can use the Text format fieldset to choose between available text formats when creating or editing multi-line content. Administrators determine the text formats available to each user role and control the order of formats listed in the Text format fieldset.') . '

'; $output .= '

' . t('For more information, see the online handbook entry for Filter module.', array('@filter' => 'http://drupal.org/handbook/modules/filter/')) . '

'; return $output; case 'admin/settings/formats': - $output = '

' . t('Use the list below to review the text formats available to each user role, to select a default text format, and to control the order of formats listed in the Text format fieldset. (The Text format fieldset is displayed below textareas when users with access to more than one text format create multi-line content.) The text format selected as Default is available to all users and, unless another format is selected, is applied to all content. All text formats are available to users in roles with the "administer filters" permission.') . '

'; - $output .= '

' . t('Since text formats, if available, are presented in the same order as the list below, it may be helpful to arrange the formats in descending order of your preference for their use. Remember that your changes will not be saved until you click the Save changes button at the bottom of the page.') . '

'; + $output = '

' . t('Use the list below to review the text formats available to each user role and to control the order of formats listed in the Text format fieldset. (The Text format fieldset is displayed below textareas when users with access to more than one text format create multi-line content.) All text formats are available to users in roles with the "administer filters" permission, and the special %fallback format is available to all users. You can configure access to other text formats on the permissions page.', array('%fallback' => filter_fallback_format_title(), '@url' => url('admin/config/people/permissions', array('fragment' => 'module-filter')))) . '

'; + $output .= '

' . t('Since text formats, if available, are presented in the same order as the list below, and the default format for each user is the first one on the list for which that user has access, it may be helpful to arrange the formats in descending order of your preference for their use. Remember that your changes will not be saved until you click the Save changes button at the bottom of the page.') . '

'; return $output; case 'admin/settings/formats/%': return '

' . t('Every filter performs one particular change on the user input, for example stripping out malicious HTML or making URLs clickable. Choose which filters you want to apply to text in this format. If you notice some filters are causing conflicts in the output, you can rearrange them.', array('@rearrange' => url('admin/settings/formats/' . $arg[3] . '/order'))) . '

'; @@ -140,8 +132,12 @@ function filter_menu() { return $items; } -function filter_format_load($arg) { - return filter_formats($arg); +/** + * Return the text format object corresponding to the provided format ID. + */ +function filter_format_load($id) { + $formats = filter_formats(); + return isset($formats[$id]) ? $formats[$id] : FALSE; } /** @@ -151,17 +147,6 @@ function filter_format_load($arg) { * A format object. */ function filter_format_save($format) { - // We store the roles as a string for ease of use. - // We should always set all roles to TRUE when saving the default format. - // We use leading and trailing comma's to allow easy substring matching. - $roles = array_filter($format->roles); - if ($format->format == variable_get('filter_default_format', 1)) { - $roles = ',' . implode(',', array_keys(user_roles())) . ','; - } - else { - $roles = ',' . implode(',',array_keys($roles)) . ','; - } - $format->roles = $roles; $format->name = trim($format->name); // Add a new text format. @@ -200,6 +185,7 @@ function filter_format_save($format) { module_invoke_all('filter_format_update', $format); } + filter_format_reset_cache(); cache_clear_all($format->format . ':', 'cache_filter', TRUE); return $status; @@ -220,9 +206,10 @@ function filter_format_delete($format) { ->execute(); // Allow modules to react on text format deletion. - $default = filter_format_load(variable_get('filter_default_format', 1)); - module_invoke_all('filter_format_delete', $format, $default); + $fallback = filter_format_load(filter_fallback_format()); + module_invoke_all('filter_format_delete', $format, $fallback); + filter_format_reset_cache(); cache_clear_all($format->format . ':', 'cache_filter', TRUE); } @@ -237,12 +224,44 @@ function filter_admin_format_title($form * Implement hook_permission(). */ function filter_permission() { - return array( - 'administer filters' => array( - 'title' => t('Administer filters'), - 'description' => t('Manage text formats and filters, and select which roles may use them. %warning', array('%warning' => t('Warning: Give to trusted roles only; this permission has security implications.'))), - ), - ); + $perms = array(); + $perms['administer filters'] = array( + 'title' => t('Administer filters'), + 'description' => t('Manage text formats and filters, and use any of them, without restriction, when entering or editing content. %warning', array('%warning' => t('Warning: Give to trusted roles only; this permission has security implications.'))), + ); + + // Generate permissions for each text format. Warn the administrator that any + // of them are potentially unsafe. + foreach (filter_formats() as $format) { + $permission = filter_permission_name($format); + if (!empty($permission)) { + // Only link to the text format configuration page if the user who is + // viewing this will have access to that page. + $format_name_replacement = user_access('administer filters') ? l($format->name, 'admin/settings/formats/' . $format->format) : theme('placeholder', $format->name); + $perms[$permission] = array( + 'title' => t("Use the '@text_format' text format", array('@text_format' => $format->name)), + 'description' => t('Use !text_format in forms when entering or editing content. %warning', array('!text_format' => $format_name_replacement, '%warning' => t('Warning: This permission may have security implications depending on how the text format is configured.'))), + ); + } + } + return $perms; +} + +/** + * Returns the machine-readable permission name for the provided text format. + * + * @param $format + * An object representing the text format. + * @return + * The machine-readable permission name, or FALSE if the provided text format + * is either (a) malformed, or (b) the fallback format available to all users + * (and therefore not controlled by the permission system). + */ +function filter_permission_name($format) { + if (isset($format->format) && $format->format != filter_fallback_format()) { + return 'use text format ' . $format->format; + } + return FALSE; } /** @@ -375,45 +394,152 @@ function _filter_html_escape_tips() { /** * @} End of "Tips callback for filters". */ + /** - * Retrieve a list of text formats. + * Retrieve a list of text formats, ordered by weight. + * + * @param $account + * Optional. If provided, only those formats that are allowed for this user + * account will be returned. All formats will be returned otherwise. + * @param $reset + * This parameter is for internal use only; when set to TRUE, the function + * resets the internal cache of all text formats and returns immediately + * without retrieving any new formats. + * @return + * An array of text format objects, keyed by the format ID and ordered by + * weight. + * + * @see filter_format_reset_cache() */ -function filter_formats($index = NULL) { - global $user; - static $formats; - - // Administrators can always use all text formats. - $all = user_access('administer filters'); - - if (!isset($formats)) { +function filter_formats($account = NULL, $reset = FALSE) { + $formats = &drupal_static(__FUNCTION__, array()); + if ($reset) { $formats = array(); + return; + } + + // Check if we've already loaded format data before doing a query. + if (!isset($formats['all'])) { + $formats['all'] = db_query('SELECT * FROM {filter_format} ORDER BY weight')->fetchAllAssoc('format'); + } - $query = db_select('filter_format', 'f'); - $query->addField('f', 'format', 'format'); - $query->addField('f', 'name', 'name'); - $query->addField('f', 'roles', 'roles'); - $query->addField('f', 'cache', 'cache'); - $query->addField('f', 'weight', 'weight'); - $query->orderBy('weight'); - - // Build query for selecting the format(s) based on the user's roles. - if (!$all) { - $or = db_or()->condition('format', variable_get('filter_default_format', 1)); - foreach ($user->roles as $rid => $role) { - $or->condition('roles', '%' . (int)$rid . '%', 'LIKE'); + // Build a list of user-specific formats if not already set. + if (isset($account) && !isset($formats['user'][$account->uid])) { + $formats['user'][$account->uid] = array(); + foreach ($formats['all'] as $format) { + if (filter_access($format, $account)) { + $formats['user'][$account->uid][$format->format] = $format; } - $query->condition($or); } + } - $formats = $query->execute()->fetchAllAssoc('format'); + return isset($account) ? $formats['user'][$account->uid] : $formats['all']; +} + +/** + * Resets the static cache of all text formats. + */ +function filter_format_reset_cache() { + filter_formats(NULL, TRUE); +} + +/** + * Retrieves a list of roles that are allowed to use a particular text format. + * + * @param $format + * Either the format ID or format object that will be checked for access. + * @return + * An array of role names, keyed by role ID. + */ +function filter_format_roles($format) { + // Handle the fallback format up front (all roles have access to this + // format). + $format_id = isset($format->format) ? $format->format : $format; + if ($format_id == filter_fallback_format()) { + return user_roles(); } - if (isset($index)) { - return isset($formats[$index]) ? $formats[$index] : FALSE; + // Otherwise, retrieve the full format object if one was not provided. + if (!isset($format->format)) { + $format = filter_format_load($format); + } + // Don't list any roles if the permission doesn't exist. + $permission = filter_permission_name($format); + return !empty($permission) ? user_roles(FALSE, $permission) : array(); +} + +/** + * Retrieves a list of text formats that are allowed for a particular role. + * + * @param $rid + * The ID of the role to check. + * @return + * An array of text format objects that are allowed for the role, keyed by + * the format ID and ordered by weight. + */ +function filter_role_formats($rid) { + $formats = array(); + foreach (filter_formats() as $format) { + $roles = filter_format_roles($format); + if (isset($roles[$rid])) { + $formats[$format->format] = $format; + } } return $formats; } /** + * Returns the ID of the default text format for a particular user. + * + * The default text format is the first available format that the user is + * allowed to access, when the formats are ordered by weight. It should + * generally be used as a default choice when presenting the user with a list + * of possible text formats (for example, in a node creation form). + * + * Conversely, when existing content that does not have an assigned text format + * needs to be filtered for display, the default text format is usually the + * wrong choice, because it is not guaranteed to be consistent from user to + * user, and some trusted users may have an unsafe text format set as their + * default which should not be used on text of unknown origin. Instead, the + * fallback format returned by filter_fallback_format() should be used, since + * that is intended to be a safe, consistent format that is always available + * to all users. + * + * @param $account + * Optional. The user account to check. Defaults to the current logged-in + * user. + * @return + * The ID of the user's default text format. + * + * @see filter_fallback_format() + */ +function filter_default_format($account = NULL) { + global $user; + if (!isset($account)) { + $account = $user; + } + // Get a list of formats for this user, ordered by weight. The first one + // available is the user's default format. + $formats = filter_formats($account); + $first_format = array_shift($formats); + return $first_format->format; +} + +/** + * Returns the ID of the fallback text format that all users have access to. + */ +function filter_fallback_format() { + return variable_get('filter_fallback_format'); +} + +/** + * Returns the title of the fallback text format. + */ +function filter_fallback_format_title() { + $fallback_format = filter_format_load(filter_fallback_format()); + return filter_admin_format_title($fallback_format); +} + +/** * Return a list of all filters provided by modules. */ function filter_get_filters() { @@ -443,17 +569,10 @@ function _filter_list_cmp($a, $b) { } /** - * Resolve a format id, including the default format. - */ -function filter_resolve_format($format) { - return $format == FILTER_FORMAT_DEFAULT ? variable_get('filter_default_format', 1) : $format; -} -/** * Check if text in a certain text format is allowed to be cached. */ function filter_format_allowcache($format) { static $cache = array(); - $format = filter_resolve_format($format); if (!isset($cache[$format])) { $cache[$format] = db_query('SELECT cache FROM {filter_format} WHERE format = :format', array(':format' => $format))->fetchField(); } @@ -502,8 +621,8 @@ function filter_list_format($format) { * @param $text * The text to be filtered. * @param $format - * The format of the text to be filtered. Specify FILTER_FORMAT_DEFAULT for - * the default format. + * The format of the text to be filtered. If no format is assigned, the + * fallback format will be used. * @param $langcode * Optional: the language code of the text to be filtered, e.g. 'en' for * English. This allows filters to be language aware so language specific @@ -513,8 +632,10 @@ function filter_list_format($format) { * The caller may set this to FALSE when the output is already cached * elsewhere to avoid duplicate cache lookups and storage. */ -function check_markup($text, $format = FILTER_FORMAT_DEFAULT, $langcode = '', $cache = TRUE) { - $format = filter_resolve_format($format); +function check_markup($text, $format = NULL, $langcode = '', $cache = TRUE) { + if (empty($format)) { + $format = filter_fallback_format(); + } // Check for a cached version of this piece of text. $cache_id = $format . ':' . $langcode . ':' . md5($text); @@ -565,9 +686,16 @@ function check_markup($text, $format = F * @return * HTML for the form element. */ -function filter_form($value = FILTER_FORMAT_DEFAULT, $weight = NULL, $parents = array('format')) { - $value = filter_resolve_format($value); - $formats = filter_formats(); +function filter_form($value = NULL, $weight = NULL, $parents = array('format')) { + global $user; + + // Use the default format for this user if none was selected. + if (empty($value)) { + $value = filter_default_format($user); + } + + // Get a list of formats that the current user has access to. + $formats = filter_formats($user); drupal_add_js('misc/form.js'); drupal_add_css(drupal_get_path('module', 'filter') . '/filter.css'); @@ -610,17 +738,35 @@ function filter_form($value = FILTER_FOR } /** - * Returns TRUE if the user is allowed to access this format. + * Checks if a user has access to a particular text format. + * + * @param $format + * Either the format ID or format object that will be checked for access. + * @param $account + * The user object that access will be checked against. Defaults to the + * current logged-in user. + * @return + * Boolean TRUE if the user has access to the requested format. */ -function filter_access($format) { - $format = filter_resolve_format($format); - if (user_access('administer filters') || ($format == variable_get('filter_default_format', 1))) { - return TRUE; +function filter_access($format, $account = NULL) { + global $user; + if (!isset($account)) { + $account = $user; } - else { - $formats = filter_formats(); - return isset($formats[$format]); + // Handle special cases up front. All users have access to the fallback + // format, and administrators have access to all formats. + $format_id = isset($format->format) ? $format->format : $format; + if (user_access('administer filters', $account) || $format_id == filter_fallback_format()) { + return TRUE; } + // Otherwise, retrieve the full format object if a format ID was provided. + if (!isset($format->format)) { + $format = filter_format_load($format); + } + // Check the permission if one exists; otherwise, we have a nonexistent + // format so we return FALSE. + $permission = filter_permission_name($format); + return !empty($permission) && user_access($permission, $account); } /** @@ -632,7 +778,9 @@ function filter_access($format) { * Helper function for fetching filter tips. */ function _filter_tips($format, $long = FALSE) { - $formats = filter_formats(); + global $user; + + $formats = filter_formats($user); $filter_info = filter_get_filters(); $tips = array(); Index: modules/node/node.install =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.install,v retrieving revision 1.28 diff -u -p -r1.28 node.install --- modules/node/node.install 22 Aug 2009 00:58:54 -0000 1.28 +++ modules/node/node.install 27 Aug 2009 04:12:12 -0000 @@ -519,7 +519,11 @@ function node_update_7006(&$context) { $revision->body = substr($revision->body, strlen($break)); } $node->body[FIELD_LANGUAGE_NONE][0]['value'] = $revision->body; - $node->body[FIELD_LANGUAGE_NONE][0]['format'] = $revision->format; + // Explicitly store the current default text format if the revision + // did not have its own text format. Similar conversions for other + // core modules are performed in filter_update_7004(), but we do this + // one here since we are already migrating the data. + $node->body[FIELD_LANGUAGE_NONE][0]['format'] = !empty($revision->format) ? $revision->format : variable_get('filter_default_format', 1); // This is a core update and no contrib modules are enabled yet, so // we can assume default field storage for a faster update. field_sql_storage_field_storage_write('node', $node, FIELD_STORAGE_INSERT, array()); Index: modules/php/php.install =================================================================== RCS file: /cvs/drupal/drupal/modules/php/php.install,v retrieving revision 1.10 diff -u -p -r1.10 php.install --- modules/php/php.install 21 Aug 2009 17:28:26 -0000 1.10 +++ modules/php/php.install 27 Aug 2009 04:12:12 -0000 @@ -19,7 +19,6 @@ function php_install() { $format = db_insert('filter_format') ->fields(array( 'name' => 'PHP code', - 'roles' => '', 'cache' => 0, )) ->execute(); Index: modules/profile/profile.module =================================================================== RCS file: /cvs/drupal/drupal/modules/profile/profile.module,v retrieving revision 1.271 diff -u -p -r1.271 profile.module --- modules/profile/profile.module 24 Aug 2009 00:14:21 -0000 1.271 +++ modules/profile/profile.module 27 Aug 2009 04:12:12 -0000 @@ -292,7 +292,7 @@ function profile_view_field($account, $f if (isset($account->{$field->name}) && $value = $account->{$field->name}) { switch ($field->type) { case 'textarea': - return check_markup($value); + return check_markup($value, filter_default_format($user)); case 'textfield': case 'selection': return $browse ? l($value, 'profile/' . $field->name . '/' . $value) : check_plain($value); Index: modules/simpletest/drupal_web_test_case.php =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v retrieving revision 1.144 diff -u -p -r1.144 drupal_web_test_case.php --- modules/simpletest/drupal_web_test_case.php 24 Aug 2009 00:14:21 -0000 1.144 +++ modules/simpletest/drupal_web_test_case.php 27 Aug 2009 04:12:12 -0000 @@ -728,7 +728,7 @@ class DrupalWebTestCase extends DrupalTe // Merge body field value and format separately. $body = array( 'value' => $this->randomName(32), - 'format' => FILTER_FORMAT_DEFAULT + 'format' => filter_default_format(), ); $settings['body'][FIELD_LANGUAGE_NONE][0] += $body; Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.379 diff -u -p -r1.379 system.install --- modules/system/system.install 24 Aug 2009 00:14:22 -0000 1.379 +++ modules/system/system.install 27 Aug 2009 04:12:12 -0000 @@ -387,13 +387,27 @@ function system_install() { $query = db_insert('role_permission')->fields(array('rid', 'permission')); // Anonymous role permissions. - $query->values(array( - 'rid' => DRUPAL_ANONYMOUS_RID, - 'permission' => 'access content', - )); + $anonymous_permissions = array( + 'access content', + 'use text format 1', // Filtered HTML + ); + foreach ($anonymous_permissions as $permission) { + $query->values(array( + 'rid' => DRUPAL_ANONYMOUS_RID, + 'permission' => $permission, + )); + } // Authenticated role permissions. - foreach (array('access comments', 'access content', 'post comments', 'post comments without approval', 'view own unpublished content') as $permission) { + $authenticated_permissions = array( + 'access comments', + 'access content', + 'post comments', + 'post comments without approval', + 'view own unpublished content', + 'use text format 1', // Filtered HTML + ); + foreach ($authenticated_permissions as $permission) { $query->values(array( 'rid' => DRUPAL_AUTHENTICATED_RID, 'permission' => $permission, @@ -424,15 +438,22 @@ function system_install() { $filtered_html_format = db_insert('filter_format') ->fields(array( 'name' => 'Filtered HTML', - 'roles' => ',' . DRUPAL_ANONYMOUS_RID . ',' . DRUPAL_AUTHENTICATED_RID . ',', 'cache' => 1, + 'weight' => 0, )) ->execute(); $full_html_format = db_insert('filter_format') ->fields(array( 'name' => 'Full HTML', - 'roles' => '', 'cache' => 1, + 'weight' => 0, + )) + ->execute(); + $plain_text_format = db_insert('filter_format') + ->fields(array( + 'name' => 'Plain text', + 'cache' => 1, + 'weight' => 1, )) ->execute(); @@ -491,10 +512,25 @@ function system_install() { 'name' => 'filter_htmlcorrector', 'weight' => 10, )) + // Plain text: + // Escape all HTML. + ->values(array( + 'format' => $plain_text_format, + 'module' => 'filter', + 'name' => 'filter_html_escape', + 'weight' => 0, + )) + // Line break filter. + ->values(array( + 'format' => $plain_text_format, + 'module' => 'filter', + 'name' => 'filter_autop', + 'weight' => 1, + )) ->execute(); - // Set the default input format to Filtered HTML. - variable_set('filter_default_format', $filtered_html_format); + // Set the fallback format to plain text. + variable_set('filter_fallback_format', $plain_text_format); $cron_key = md5(mt_rand()); @@ -2047,8 +2083,9 @@ function system_update_7021() { $ret[] = update_sql("UPDATE {block} SET region = 'sidebar_second' WHERE region = 'right'"); // Migrate contact form information. + $default_format = variable_get('filter_default_format', 1); if ($contact_help = variable_get('contact_form_information', '')) { - $bid = db_insert('box')->fields(array('body' => $contact_help, 'info' => 'Contact page help', 'format' => FILTER_FORMAT_DEFAULT))->execute(); + $bid = db_insert('box')->fields(array('body' => $contact_help, 'info' => 'Contact page help', 'format' => $default_format))->execute(); foreach ($themes_with_blocks as $theme) { // Add contact help block for themes, which had blocks. $ret[] = update_sql("INSERT INTO {block} (module, delta, theme, status, weight, region, visibility, pages, cache) VALUES ('block', '" . $bid . "', '" . $theme . "', 1, 5, 'help', 1, 'contact', -1)"); @@ -2058,7 +2095,7 @@ function system_update_7021() { // Migrate user help setting. if ($user_help = variable_get('user_registration_help', '')) { - $bid = db_insert('box')->fields(array('body' => $user_help, 'info' => 'User registration guidelines', 'format' => FILTER_FORMAT_DEFAULT))->execute(); + $bid = db_insert('box')->fields(array('body' => $user_help, 'info' => 'User registration guidelines', 'format' => $default_format))->execute(); foreach ($themes_with_blocks as $theme) { // Add user registration help block for themes, which had blocks. $ret[] = update_sql("INSERT INTO {block} (module, delta, theme, status, weight, region, visibility, pages, cache) VALUES ('block', '" . $bid . "', '" . $theme . "', 1, 5, 'help', 1, 'user/register', -1)"); @@ -2068,7 +2105,7 @@ function system_update_7021() { // Migrate site mission setting. if ($mission = variable_get('site_mission')) { - $bid = db_insert('box')->fields(array('body' => $mission, 'info' => 'Site mission', 'format' => FILTER_FORMAT_DEFAULT))->execute(); + $bid = db_insert('box')->fields(array('body' => $mission, 'info' => 'Site mission', 'format' => $default_format))->execute(); foreach ($themes_with_blocks as $theme) { // Add mission block for themes, which had blocks. $ret[] = update_sql("INSERT INTO {block} (module, delta, theme, status, weight, region, visibility, pages, cache) VALUES ('block', '" . $bid . "', '" . $theme . "', 1, 0, 'highlight', 1, '', -1)"); @@ -2080,7 +2117,7 @@ function system_update_7021() { // Migrate site footer message to a custom block. if ($footer_message = variable_get('site_footer', '')) { - $bid = db_insert('box')->fields(array('body' => $footer_message, 'info' => 'Footer message', 'format' => FILTER_FORMAT_DEFAULT))->execute(); + $bid = db_insert('box')->fields(array('body' => $footer_message, 'info' => 'Footer message', 'format' => $default_format))->execute(); foreach ($themes_with_blocks as $theme) { // Add site footer block for themes, which had blocks. // Set low weight, so the block comes early (it used to be