Index: CHANGELOG.txt =================================================================== RCS file: /cvs/drupal/drupal/CHANGELOG.txt,v retrieving revision 1.288 diff -u -p -r1.288 CHANGELOG.txt --- CHANGELOG.txt 23 Nov 2008 06:06:15 -0000 1.288 +++ CHANGELOG.txt 24 Nov 2008 03:56:01 -0000 @@ -21,6 +21,7 @@ Drupal 7.0, xxxx-xx-xx (development vers * Implemented drag-and-drop positioning for language listing. * Implemented drag-and-drop positioning for poll options. * Provided descriptions and human-readable names for user permissions. + * Moved filter permissions to the main permissions page. * Removed comment controls for users. * Removed display order settings for comment module. Comment display order can now be customised using the Views module. @@ -31,6 +32,8 @@ Drupal 7.0, xxxx-xx-xx (development vers * Added an edit tab to taxonomy term pages. * Redesigned password strength validator. * Redesigned the add content type screen. +- Filter system: + * Added support for default input formats to be assigned on a per-role basis. - News aggregator: * Added OPML import functionality for RSS feeds. * Optionally, RSS feeds may be configured to not automatically generate feed blocks. Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.307 diff -u -p -r1.307 form.inc --- includes/form.inc 24 Nov 2008 00:40:44 -0000 1.307 +++ includes/form.inc 24 Nov 2008 03:56:01 -0000 @@ -1800,7 +1800,7 @@ function form_process_radios($element) { * $form['body'] = array( * '#type' => 'textarea', * '#title' => t('Body'), - * '#input_format' => isset($node->format) ? $node->format : FILTER_FORMAT_DEFAULT, + * '#input_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.315 diff -u -p -r1.315 block.module --- modules/block/block.module 15 Nov 2008 08:23:07 -0000 1.315 +++ modules/block/block.module 24 Nov 2008 03:56:01 -0000 @@ -198,7 +198,7 @@ function block_block($op = 'list', $delt return $blocks; case 'configure': - $box = array('format' => FILTER_FORMAT_DEFAULT); + $box = array('format' => filter_default_format()); if ($delta) { $box = block_box_get($delta); } @@ -321,7 +321,7 @@ function block_box_form($edit = array()) '#type' => 'textarea', '#title' => t('Block body'), '#default_value' => $edit['body'], - '#input_format' => isset($edit['format']) ? $edit['format'] : FILTER_FORMAT_DEFAULT, + '#input_format' => isset($edit['format']) ? $edit['format'] : filter_default_format(), '#rows' => 15, '#description' => t('The content of the block as shown to the user.'), '#weight' => -17, @@ -332,7 +332,7 @@ function block_box_form($edit = array()) function block_box_save($edit, $delta) { if (!filter_access($edit['body_format'])) { - $edit['body_format'] = FILTER_FORMAT_DEFAULT; + $edit['body_format'] = filter_default_format(); } db_query("UPDATE {box} SET body = '%s', info = '%s', format = %d WHERE bid = %d", $edit['body'], $edit['info'], $edit['body_format'], $delta); Index: modules/blogapi/blogapi.module =================================================================== RCS file: /cvs/drupal/drupal/modules/blogapi/blogapi.module,v retrieving revision 1.133 diff -u -p -r1.133 blogapi.module --- modules/blogapi/blogapi.module 11 Nov 2008 16:49:37 -0000 1.133 +++ modules/blogapi/blogapi.module 24 Nov 2008 03:56:01 -0000 @@ -202,7 +202,7 @@ function blogapi_blogger_new_post($appke $edit['promote'] = in_array('promote', $node_type_default); $edit['comment'] = variable_get('comment_' . $edit['type'], 2); $edit['revision'] = in_array('revision', $node_type_default); - $edit['format'] = FILTER_FORMAT_DEFAULT; + $edit['format'] = filter_default_format($user); $edit['status'] = $publish; // Check for bloggerAPI vs. metaWeblogAPI. @@ -622,7 +622,7 @@ function blogapi_mt_validate_terms($node function blogapi_mt_supported_text_filters() { // NOTE: we're only using anonymous' formats because the MT spec // does not allow for per-user formats. - $formats = filter_formats(); + $formats = filter_formats(drupal_anonymous_user()); $filters = array(); foreach ($formats as $format) { Index: modules/comment/comment.module =================================================================== RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v retrieving revision 1.666 diff -u -p -r1.666 comment.module --- modules/comment/comment.module 23 Nov 2008 16:04:41 -0000 1.666 +++ modules/comment/comment.module 24 Nov 2008 03:56:01 -0000 @@ -1450,7 +1450,7 @@ function comment_form(&$form_state, $edi '#title' => t('Comment'), '#rows' => 15, '#default_value' => $default, - '#input_format' => isset($edit['format']) ? $edit['format'] : FILTER_FORMAT_DEFAULT, + '#input_format' => isset($edit['format']) ? $edit['format'] : filter_default_format(), '#required' => TRUE, ); Index: modules/filter/filter.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/filter/filter.admin.inc,v retrieving revision 1.17 diff -u -p -r1.17 filter.admin.inc --- modules/filter/filter.admin.inc 15 Nov 2008 08:23:07 -0000 1.17 +++ modules/filter/filter.admin.inc 24 Nov 2008 03:56:01 -0000 @@ -7,50 +7,44 @@ */ /** - * Menu callback; Displays a list of all input formats and which - * one is the default. + * Menu callback; displays a list of all input 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; - $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 (strstr($format->roles, ",$rid,")) { - $roles[] = $name; - } + // Check whether this is the fallback "plain text" input format. This + // format is available to all roles and cannot be configured via the + // admin interface. + $is_fallback_format = ($id == filter_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'); } - $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 default format') : ($roles ? implode(', ', $roles) : t('No roles may use this format'))); - $form[$id]['configure'] = array('#markup' => l(t('configure'), 'admin/settings/filters/' . $id)); - $form[$id]['delete'] = array('#markup' => $default ? '' : l(t('delete'), 'admin/settings/filters/delete/' . $id)); + $form[$id]['roles'] = array('#markup' => $roles_markup); + $form[$id]['configure'] = array('#markup' => $is_fallback_format ? '' : l(t('configure'), 'admin/settings/filters/' . $id)); + $form[$id]['delete'] = array('#markup' => $is_fallback_format ? '' : l(t('delete'), 'admin/settings/filters/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. db_query("UPDATE {filter_formats} SET weight = %d WHERE format = %d", $data['weight'], $id); + filter_formats_reset_cache(); } } drupal_set_message(t('The input format ordering has been saved.')); @@ -68,9 +62,8 @@ function theme_filter_admin_overview($fo $element['weight']['#attributes']['class'] = 'input-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']), @@ -80,7 +73,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' => 'input-format-order')); $output .= drupal_render($form); @@ -95,8 +88,9 @@ function theme_filter_admin_overview($fo function filter_admin_format_page($format = NULL) { if (!isset($format->name)) { drupal_set_title(t('Add input 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); } @@ -108,43 +102,20 @@ 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, - '#description' => t('Specify a unique name for this filter format.'), + '#description' => t('Specify a unique name for this input format.'), '#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 filter format. Note that roles with the "administer filters" permission can always use all the filter formats.'), - '#tree' => TRUE, - ); - - foreach (user_roles() as $rid => $name) { - $checked = strstr($format->roles, ",$rid,"); - $form['roles'][$rid] = array('#type' => 'checkbox', - '#title' => $name, - '#default_value' => ($default || $checked), - ); - if ($default) { - $form['roles'][$rid]['#disabled'] = TRUE; - } - } // Table with filters $all = filter_list_all(); $enabled = filter_list_format($format->format); $form['filters'] = array('#type' => 'fieldset', '#title' => t('Filters'), - '#description' => t('Choose the filters that will be used in this filter format.'), + '#description' => t('Choose the filters that will be used in this input format.'), '#tree' => TRUE, ); foreach ($all as $id => $filter) { @@ -181,7 +152,7 @@ function filter_admin_format_form_valida $name = trim($form_state['values']['name']); $result = db_fetch_object(db_query("SELECT format FROM {filter_formats} WHERE name='%s'", $name)); if ($result) { - form_set_error('name', t('Filter format names need to be unique. A format named %name already exists.', array('%name' => $name))); + form_set_error('name', t('Input format names need to be unique. A format named %name already exists.', array('%name' => $name))); } } } @@ -195,7 +166,7 @@ function filter_admin_format_form_submit $name = trim($form_state['values']['name']); $cache = TRUE; - // Add a new filter format. + // Add a new input format. if (!$format) { $new = TRUE; db_query("INSERT INTO {filter_formats} (name) VALUES ('%s')", $name); @@ -219,26 +190,15 @@ function filter_admin_format_form_submit } } - // We store the roles as a string for ease of use. - // We should always set all roles to TRUE when saving a default role. - // We use leading and trailing comma's to allow easy substring matching. - $roles = array(); - if (isset($form_state['values']['roles'])) { - foreach ($form_state['values']['roles'] as $id => $checked) { - if ($checked) { - $roles[] = $id; - } - } - } - if (!empty($form_state['values']['default_format'])) { - $roles = ',' . implode(',', array_keys(user_roles())) . ','; - } - else { - $roles = ',' . implode(',', $roles) . ','; - } - - db_query("UPDATE {filter_formats} SET cache = %d, name='%s', roles = '%s' WHERE format = %d", $cache, $name, $roles, $format); + db_update('filter_formats') + ->fields(array( + 'cache' => $cache, + 'name' => $name, + )) + ->condition('format', $format) + ->execute(); + filter_formats_reset_cache(); cache_clear_all($format . ':', 'cache_filter', TRUE); // If a new filter was added, return to the main list of filters. Otherwise, stay on edit filter page to show new changes. @@ -251,29 +211,34 @@ function filter_admin_format_form_submit } /** + * Display a warning when the administrator is editing a format that + * potentially-untrusted users have permission to use. + * + * @param $format + * An object representing the input format. + */ +function filter_admin_display_warning($format) { + $roles = filter_format_roles($format); + if (!empty($roles)) { + drupal_set_message(t('This input format is available to the following roles: %roles. Since input 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 * @see filter_admin_delete_submit() */ -function filter_admin_delete() { - $format = arg(4); - $format = db_fetch_object(db_query('SELECT * FROM {filter_formats} WHERE format = %d', $format)); - - if ($format) { - if ($format->format != variable_get('filter_default_format', 1)) { - $form['format'] = array('#type' => 'hidden', '#value' => $format->format); - $form['name'] = array('#type' => 'hidden', '#value' => $format->name); - - return confirm_form($form, t('Are you sure you want to delete the input format %format?', array('%format' => $format->name)), 'admin/settings/filters', t('If you have any content left in this input format, it will be switched to the default input format. This action cannot be undone.'), t('Delete'), t('Cancel')); - } - else { - drupal_set_message(t('The default format cannot be deleted.')); - drupal_goto('admin/settings/filters'); - } +function filter_admin_delete(&$form_state, $format) { + if ($format->format != filter_fallback_format()) { + $form['format'] = array('#type' => 'value', '#value' => $format->format); + $form['name'] = array('#type' => 'value', '#value' => $format->name); + return confirm_form($form, t('Are you sure you want to delete the input format %format?', array('%format' => $format->name)), 'admin/settings/filters', t('If you have any content left in this input format, it will be displayed as plain text until you manually assign it a new format. This action cannot be undone.'), t('Delete'), t('Cancel')); } else { - drupal_not_found(); + drupal_set_message(t('The %format format cannot be deleted since it is required to be available for all users.', array('%format' => $format->name))); + drupal_goto('admin/settings/filters'); } } @@ -284,12 +249,18 @@ function filter_admin_delete_submit($for db_query("DELETE FROM {filter_formats} WHERE format = %d", $form_state['values']['format']); db_query("DELETE FROM {filters} WHERE format = %d", $form_state['values']['format']); - $default = variable_get('filter_default_format', 1); - // Replace existing instances of the deleted format with the default format. - db_query("UPDATE {node_revisions} SET format = %d WHERE format = %d", $default, $form_state['values']['format']); - db_query("UPDATE {comments} SET format = %d WHERE format = %d", $default, $form_state['values']['format']); - db_query("UPDATE {box} SET format = %d WHERE format = %d", $default, $form_state['values']['format']); + // Replace existing instances of the deleted format with the fallback + // (plain text) format. + $fallback_format = filter_fallback_format(); + $database_tables = array('node_revisions', 'comments', 'box'); + foreach ($database_tables as $table) { + db_update($table) + ->fields(array('format' => $fallback_format)) + ->condition('format', $form_state['values']['format']) + ->execute(); + } + filter_formats_reset_cache(); cache_clear_all($form_state['values']['format'] . ':', 'cache_filter', TRUE); drupal_set_message(t('Deleted input format %format.', array('%format' => $form_state['values']['name']))); @@ -297,12 +268,12 @@ function filter_admin_delete_submit($for return; } - /** * 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); } @@ -344,6 +315,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.install =================================================================== RCS file: /cvs/drupal/drupal/modules/filter/filter.install,v retrieving revision 1.10 diff -u -p -r1.10 filter.install --- modules/filter/filter.install 15 Nov 2008 13:01:06 -0000 1.10 +++ modules/filter/filter.install 24 Nov 2008 03:56:01 -0000 @@ -64,13 +64,6 @@ function filter_schema() { 'default' => '', 'description' => 'Name of the input 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, @@ -99,6 +92,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() { @@ -122,3 +120,65 @@ function filter_update_7001() { } return $ret; } + +/** + * Move filter format access to the user permissions handler, create a + * fallback plain text input format, and explicitly set the input format + * in cases that used to rely on a single site-wide default. + */ +function filter_update_7002() { + $ret = array(); + + // Move role data from filter_formats to user permissions. + $all_roles = array_keys(user_roles()); + $result = db_query("SELECT * FROM {filter_formats}"); + while ($format = db_fetch_object($result)) { + $format_roles = explode(',', $format->roles); + foreach ($format_roles as $format_role) { + if (in_array($format_role, $all_roles)) { + $ret[] = update_sql("INSERT INTO {role_permission} (rid, permission) VALUES (" . $format_role . ", " . filter_permission_name($format) . ")"); + } + } + } + // Drop the roles field from filter_formats. + db_drop_field($ret, 'filter_formats', 'roles'); + + // Add a fallback plain text input format (and make sure its weight is high + // enough so that it appears last on the list for all users). + $weight = db_result(db_query("SELECT MAX(weight) FROM {filter_formats}")) + 1; + $ret[] = update_sql("INSERT INTO {filter_formats} (name, cache, weight) VALUES ('Plain text', 1, " . $weight . ")"); + $fallback_format = db_last_insert_id('filter_formats', 'format'); + if ($fallback_format != filter_fallback_format()) { + variable_set('filter_fallback_format', $fallback_format); + } + // Use the line break filter only (check_plain is hardcoded separately + // for this input format). + $ret[] = update_sql("INSERT INTO {filters} (format, module, delta, weight) VALUES (" . $fallback_format . ", 'filter', 1, 0)"); + + // Move the former site-wide default input format to the top of the list, + // so that it continues to be the default input format for all users. + $default_format = variable_get('filter_default_format', 1); + $default_format_weight = db_result(db_query("SELECT weight FROM {filter_formats} WHERE format = %d", $default_format)); + $minimum_weight = db_result(db_query("SELECT MIN(weight) FROM {filter_formats}")); + if ($default_format_weight >= $minimum_weight) { + $ret[] = update_sql("UPDATE {filter_formats} SET weight = " . ($minimum_weight - 1) . " WHERE format = " . $default_format); + } + // 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 input format. Therefore, we have to convert all such + // instances (in Drupal core) to explicitly use the appropriate format. + $database_tables = array('node_revisions', 'comments', 'box'); + foreach ($database_tables as $table) { + $ret[] = update_sql("UPDATE {" . $table . "} SET format = " . $default_format . " WHERE format = 0"); + } + // 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.234 diff -u -p -r1.234 filter.module --- modules/filter/filter.module 15 Nov 2008 11:45:03 -0000 1.234 +++ modules/filter/filter.module 24 Nov 2008 03:56:01 -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); - -/** * Implementation of hook_help(). */ function filter_help($path, $arg) { @@ -22,12 +14,13 @@ function filter_help($path, $arg) { case 'admin/help#filter': $output = '

' . t("The filter module allows administrators to configure text input formats for use on your site. An input 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 input 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 input formats may be created by an administrator.") . '

'; $output .= '

' . t('Each input format uses filters to manipulate text, and most input 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 input format can use the Input format fieldset to choose between available input formats when creating or editing multi-line content. Administrators determine the input formats available to each user role, select a default input format, and control the order of formats listed in the Input format fieldset.') . '

'; + $output .= '

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

'; + $output .= '

' . t('The special %plain_text input format is available to all users. Content with this format will be displayed with line and paragraph breaks preserved but otherwise without any visible formatting.', array('%plain_text' => filter_fallback_format_title())) . '

'; $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/filters': - $output = '

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

'; - $output .= '

' . t('Since input 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. To change the order of an input format, grab a drag-and-drop handle under the Name column and drag to a new location in the list. (Grab a handle by clicking and holding the mouse while hovering over a handle icon.) 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 input formats available to each user role and to control the order of formats listed in the Input format fieldset. (The Input format fieldset is displayed below textareas when users with access to more than one input format create multi-line content.) All input formats are available to users in roles with the "Administer filters" permission, and the special %plain_text format is available to all users. You can configure access to other input formats on the permissions page.', array('%plain_text' => filter_fallback_format_title(), '@url' => url('admin/user/permissions', array('fragment' => 'module-filter')))) . '

'; + $output .= '

' . t('It may be helpful to arrange the formats in order of your preference for their use, since the default input format for each user is the first one on the list for which that user has access. To change the order of an input format, grab a drag-and-drop handle under the Name column and drag to a new location in the list. (Grab a handle by clicking and holding the mouse while hovering over a handle icon.) 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/filters/%': 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 input format. If you notice some filters are causing conflicts in the output, you can rearrange them.', array('@rearrange' => url('admin/settings/filters/' . $arg[3] . '/order'))) . '

'; @@ -85,10 +78,10 @@ function filter_menu() { 'type' => MENU_LOCAL_TASK, 'weight' => 1, ); - $items['admin/settings/filters/delete'] = array( + $items['admin/settings/filters/delete/%filter_format'] = array( 'title' => 'Delete input format', 'page callback' => 'drupal_get_form', - 'page arguments' => array('filter_admin_delete'), + 'page arguments' => array('filter_admin_delete', 4), 'access arguments' => array('administer filters'), 'type' => MENU_CALLBACK, ); @@ -104,7 +97,8 @@ function filter_menu() { 'title arguments' => array(3), 'page callback' => 'filter_admin_format_page', 'page arguments' => array(3), - 'access arguments' => array('administer filters'), + 'access callback' => 'filter_edit_format_access', + 'access arguments' => array(3), ); $items['admin/settings/filters/%filter_format/edit'] = array( 'title' => 'Edit', @@ -115,7 +109,8 @@ function filter_menu() { 'title' => 'Configure', 'page callback' => 'filter_admin_configure_page', 'page arguments' => array(3), - 'access arguments' => array('administer filters'), + 'access callback' => 'filter_edit_format_access', + 'access arguments' => array(3), 'type' => MENU_LOCAL_TASK, 'weight' => 1, ); @@ -123,15 +118,31 @@ function filter_menu() { 'title' => 'Rearrange', 'page callback' => 'filter_admin_order_page', 'page arguments' => array(3), - 'access arguments' => array('administer filters'), + 'access callback' => 'filter_edit_format_access', + 'access arguments' => array(3), 'type' => MENU_LOCAL_TASK, 'weight' => 2, ); return $items; } -function filter_format_load($arg) { - return filter_formats($arg); +/** + * Return the input format object corresponding to the provided format ID. + */ +function filter_format_load($id) { + $formats = filter_formats(); + if (isset($formats[$id])) { + return $formats[$id]; + } + return FALSE; +} + +/** + * Determine access for editing input formats. + */ +function filter_edit_format_access($format) { + // The fallback format can never be edited. + return user_access('administer filters') && ($format->format != filter_fallback_format()); } /** @@ -145,12 +156,45 @@ function filter_admin_format_title($form * Implementation of hook_perm(). */ function filter_perm() { - return array( - 'administer filters' => array( - 'title' => t('Administer filters'), - 'description' => t('Manage input 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 input 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 input format. Warn the administrator + // that any of them are potentially unsafe. + foreach (filter_formats() as $format) { + $permission = filter_permission_name($format); + if ($permission) { + // Only link to the input 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/filters/' . $format->format) : theme('placeholder', $format->name); + $perms[$permission] = array( + 'title' => t("Use the '@input_format' input format", array('@input_format' => $format->name)), + 'description' => t('Use !input_format in forms when entering or editing content. %warning', array('!input_format' => $format_name_replacement, '%warning' => t('Warning: This permission may have security implications depending on how the input format is configured.'))), + ); + } + } + return $perms; +} + +/** + * Returns the machine-readable permission name for the provided input + * format. + * + * @param $format + * An object representing the input format. + * @return + * The machine-readable permission name, or FALSE if the provided input + * 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 input format ' . $format->format; + } + return FALSE; } /** @@ -283,41 +327,84 @@ function filter_filter_tips($delta, $for } /** - * Retrieve a list of input formats. - */ -function filter_formats($index = NULL) { - global $user; - static $formats; - - // Administrators can always use all input formats. - $all = user_access('administer filters'); - - if (!isset($formats)) { + * Retrieve a list of input 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 + * Whether to reset the internal cache of all input formats. Defaults to + * FALSE. + */ +function filter_formats($account = NULL, $reset = FALSE) { + static $formats = array(); + if ($reset) { $formats = array(); + } + + // Check if we've already loaded format data before doing a query. + if (!isset($formats['all'])) { + $formats['all'] = db_query('SELECT * FROM {filter_formats} ORDER BY weight')->fetchAllAssoc('format'); + } - $query = db_select('filter_formats', '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']; +} + +/** + * Helper function to reset the static cache of all input formats. + */ +function filter_formats_reset_cache() { + filter_formats(NULL, TRUE); +} + +/** + * Return the ID of the default input format for a particular user. + * + * @param $account + * Optional. The user account to check. Defaults to the current logged-in + * user. + */ +function filter_default_format($account = NULL) { + global $user; + if (!isset($account)) { + $account = $user; } - if (isset($index)) { - return isset($formats[$index]) ? $formats[$index] : FALSE; + // Get a list of formats for this user, ordered by weight. The first one + // available is that user's default format. + $formats = filter_formats($account); + $first_format = array_shift($formats); + if (isset($first_format) && isset($first_format->format)) { + return $first_format->format; } - return $formats; + // All users have access to the fallback format, so we use that here if + // no formats were available above (however, under normal circumstances + // this code should never be reached). + return filter_fallback_format(); +} + +/** + * Return the ID of the fallback ("plain text") input format that all users + * have access to. + */ +function filter_fallback_format() { + return variable_get('filter_fallback_format', 3); +} + +/** + * Display the title of the fallback ("plain text") input format. + */ +function filter_fallback_format_title() { + return filter_admin_format_title(filter_format_load(filter_fallback_format())); } /** @@ -349,17 +436,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 input 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_result(db_query('SELECT cache FROM {filter_formats} WHERE format = %d', $format)); } @@ -408,8 +488,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 provided, the + * fallback "plain text" 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 @@ -421,10 +501,14 @@ function filter_list_format($format) { * showing content that is not (yet) stored in the database (eg. upon preview), * set to TRUE so the user's permissions are checked. */ -function check_markup($text, $format = FILTER_FORMAT_DEFAULT, $langcode = '', $check = TRUE) { +function check_markup($text, $format = NULL, $langcode = '', $check = TRUE) { + // Use the fallback format if none was provided. + if (!isset($format)) { + $format = filter_fallback_format(); + } + // When $check = TRUE, do an access check on $format. - if (isset($text) && (!$check || filter_access($format))) { - $format = filter_resolve_format($format); + if (!$check || filter_access($format)) { // Check for a cached version of this piece of text. $cache_id = $format . ':' . $langcode . ':' . md5($text); @@ -432,9 +516,6 @@ function check_markup($text, $format = F return $cached->data; } - // See if caching is allowed for this format. - $cache = filter_format_allowcache($format); - // Convert all Windows and Mac newlines to a single newline, // so filters only need to deal with one possibility. $text = str_replace(array("\r\n", "\r"), "\n", $text); @@ -442,6 +523,33 @@ function check_markup($text, $format = F // Get a complete list of filters, ordered properly. $filters = filter_list_format($format); + // If no filters were returned, check to make sure that this corresponds + // to an actual input format before proceeding. If not, we will process + // this input format in the same way that we process the fallback format, + // thereby ensuring that text with an unrecognized input format will not + // be allowed to pass through unsanitized. We do this check here (rather + // than at the top of the function) to avoid extra database calls except + // when strictly necessary. + $use_fallback_format = ($format == filter_fallback_format()); + if (empty($filters) && !$use_fallback_format && !filter_format_load($format)) { + $use_fallback_format = TRUE; + $filters = filter_list_format(filter_fallback_format()); + } + + // The fallback "plain text" format is available to any user who has + // permission to create content and is therefore an inherent security + // risk if configured incorrectly. To mitigate this risk, we make sure + // to escape all HTML tags here, before allowing any filters to run, + // and thereby attempt to enforce the "plain" nature of this text via a + // hardcoded method. This means that even if additional, unsafe filters + // were added to this input format, it would still be difficult to make + // the input format unsafe; e.g., if the "PHP evaluator" filter were + // added, that filter (by itself) would not be able to evaluate PHP + // commands, due to the code here. + if ($use_fallback_format) { + $text = trim(check_plain($text)); + } + // Give filters the chance to escape HTML-like data such as code or formulas. foreach ($filters as $filter) { $text = module_invoke($filter->module, 'filter', 'prepare', $filter->delta, $format, $text, $langcode, $cache_id); @@ -452,8 +560,22 @@ function check_markup($text, $format = F $text = module_invoke($filter->module, 'filter', 'process', $filter->delta, $format, $text, $langcode, $cache_id); } + // In the case of the fallback "plain text" format, perform additional + // filtering at the end to only allow a very limited set of tags to pass + // through. This ensures that any filters that have run preserve the + // "plain" nature of this text to some degree. + if ($use_fallback_format) { + $allowed_tags = array( + // These are needed so that the line break filter will work. + 'p', 'br', + // These are just the default tags allowed by filter_xss(). + 'a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', + ); + $text = filter_xss($text, $allowed_tags); + } + // Store in cache with a minimum expiration time of 1 day. - if ($cache) { + if (filter_format_allowcache($format)) { cache_set($cache_id, $text, 'cache_filter', REQUEST_TIME + (60 * 60 * 24)); } } @@ -478,9 +600,25 @@ 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 (!isset($value)) { + $value = filter_default_format($user); + } + + // Get a list of formats that the current user has access to. + $formats = filter_formats($user); + + // If the user doesn't have access to the currently-selected format, it + // could be because the format doesn't exist any more. In that case, + // check_markup() treats this text as if it were in the fallback format, + // and therefore the best thing to do here is to pre-select that format + // on the form. + if (!isset($formats[$value])) { + $value = filter_fallback_format(); + } $extra = theme('filter_tips_more_info'); @@ -513,7 +651,7 @@ function filter_form($value = FILTER_FOR // Only one format available: use a hidden form item and only show tips. $format = array_shift($formats); $form[$format->format] = array('#type' => 'value', '#value' => $format->format, '#parents' => $parents); - $tips = _filter_tips(variable_get('filter_default_format', 1), FALSE); + $tips = _filter_tips($format->format, FALSE); $form['format']['guidelines'] = array( '#title' => t('Formatting guidelines'), '#markup' => theme('filter_tips', $tips, FALSE, $extra), @@ -534,17 +672,71 @@ function filter_form_validate($form) { } /** - * Returns TRUE if the user is allowed to access this format. + * Check if a user has access to a particular input 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 filter. */ -function filter_access($format) { - $format = filter_resolve_format($format); - if (user_access('administer filters') || ($format == variable_get('filter_default_format', 1))) { +function filter_access($format, $account = NULL) { + global $user; + if (!isset($account)) { + $account = $user; + } + // 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 one was not 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 $permission && user_access($permission, $account); +} + +/** + * Retrieve a list of roles that are allowed to use a particular format. + * + * @param $format + * The format object that will be checked for access. + * + * @return + * An array of roles structured $rid => $role_name. + */ +function filter_format_roles($format) { + $permission = filter_permission_name($format); + if ($permission) { + return user_roles(FALSE, $permission); + } + // Don't list any roles if the permission doesn't exist, unless it's for + // the fallback format (which all roles have access to). else { - $formats = filter_formats(); - return isset($formats[$format]); + return $format->format == filter_fallback_format() ? user_roles() : array(); + } +} + +/** + * Retreive a list of formats that are allowed for a particular role. + */ +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->name; + } } + return $formats; } /** @@ -556,11 +748,13 @@ function filter_access($format) { * Helper function for fetching filter tips. */ function _filter_tips($format, $long = FALSE) { + global $user; + if ($format == -1) { - $formats = filter_formats(); + $formats = filter_formats($user); } else { - $formats = array(db_fetch_object(db_query("SELECT * FROM {filter_formats} WHERE format = %d", $format))); + $formats = array(filter_format_load($format)); } $tips = array(); Index: modules/node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.998 diff -u -p -r1.998 node.module --- modules/node/node.module 22 Nov 2008 14:09:41 -0000 1.998 +++ modules/node/node.module 24 Nov 2008 03:56:01 -0000 @@ -870,7 +870,7 @@ function node_submit($node) { // module-provided 'teaser' form item). if (!isset($node->teaser)) { if (isset($node->body)) { - $node->format = (!empty($node->body_format) ? $node->body_format : FILTER_FORMAT_DEFAULT); + $node->format = (!empty($node->body_format) ? $node->body_format : filter_default_format()); $node->teaser = node_teaser($node->body, isset($node->format) ? $node->format : NULL); // Chop off the teaser from the body if needed. The teaser_include // property might not be set (eg. in Blog API postings), so only act on Index: modules/node/node.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.pages.inc,v retrieving revision 1.45 diff -u -p -r1.45 node.pages.inc --- modules/node/node.pages.inc 13 Oct 2008 00:33:03 -0000 1.45 +++ modules/node/node.pages.inc 24 Nov 2008 03:56:01 -0000 @@ -297,7 +297,7 @@ function node_body_field(&$node, $label, '#default_value' => $include ? $node->body : ($node->teaser . $node->body), '#rows' => 20, '#required' => ($word_count > 0), - '#input_format' => isset($node->format) ? $node->format : FILTER_FORMAT_DEFAULT, + '#input_format' => isset($node->format) ? $node->format : filter_default_format(), ); return $form; Index: modules/php/php.install =================================================================== RCS file: /cvs/drupal/drupal/modules/php/php.install,v retrieving revision 1.2 diff -u -p -r1.2 php.install --- modules/php/php.install 14 Apr 2008 17:48:41 -0000 1.2 +++ modules/php/php.install 24 Nov 2008 03:56:01 -0000 @@ -11,7 +11,7 @@ function php_install() { // reliable method to identify the format in an uninstall hook or in // subsequent clean installs. if (!$format_exists) { - db_query("INSERT INTO {filter_formats} (name, roles, cache) VALUES ('PHP code', '', 0)"); + db_query("INSERT INTO {filter_formats} (name, cache) VALUES ('PHP code', 0)"); $format = db_result(db_query("SELECT MAX(format) FROM {filter_formats}")); // Enable the PHP evaluator filter. Index: modules/profile/profile.module =================================================================== RCS file: /cvs/drupal/drupal/modules/profile/profile.module,v retrieving revision 1.246 diff -u -p -r1.246 profile.module --- modules/profile/profile.module 12 Oct 2008 04:30:08 -0000 1.246 +++ modules/profile/profile.module 24 Nov 2008 03:56:01 -0000 @@ -289,7 +289,7 @@ function profile_view_field($user, $fiel if (isset($user->{$field->name}) && $value = $user->{$field->name}) { switch ($field->type) { case 'textarea': - return check_markup($value); + return check_markup($value, filter_default_format($user), FALSE); 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.58 diff -u -p -r1.58 drupal_web_test_case.php --- modules/simpletest/drupal_web_test_case.php 23 Nov 2008 21:17:48 -0000 1.58 +++ modules/simpletest/drupal_web_test_case.php 24 Nov 2008 03:56:02 -0000 @@ -371,7 +371,7 @@ class DrupalWebTestCase { 'title' => $this->randomName(8), 'comment' => 2, 'changed' => REQUEST_TIME, - 'format' => FILTER_FORMAT_DEFAULT, + 'format' => filter_default_format(), 'moderate' => 0, 'promote' => 0, 'revision' => 1, Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.285 diff -u -p -r1.285 system.install --- modules/system/system.install 23 Nov 2008 21:17:48 -0000 1.285 +++ modules/system/system.install 24 Nov 2008 03:56:02 -0000 @@ -369,12 +369,14 @@ function system_install() { // Anonymous role permissions. db_query("INSERT INTO {role_permission} (rid, permission) VALUES (%d, '%s')", 1, 'access content'); + db_query("INSERT INTO {role_permission} (rid, permission) VALUES (%d, '%s')", 1, 'use input format 1'); // Filtered HTML // Authenticated role permissions. db_query("INSERT INTO {role_permission} (rid, permission) VALUES (%d, '%s')", 2, 'access comments'); db_query("INSERT INTO {role_permission} (rid, permission) VALUES (%d, '%s')", 2, 'access content'); db_query("INSERT INTO {role_permission} (rid, permission) VALUES (%d, '%s')", 2, 'post comments'); db_query("INSERT INTO {role_permission} (rid, permission) VALUES (%d, '%s')", 2, 'post comments without approval'); + db_query("INSERT INTO {role_permission} (rid, permission) VALUES (%d, '%s')", 2, 'use input format 1'); // Filtered HTML db_query("INSERT INTO {variable} (name, value) VALUES ('%s', '%s')", 'theme_default', 's:7:"garland";'); db_query("UPDATE {system} SET status = %d WHERE type = '%s' AND name = '%s'", 1, 'theme', 'garland'); @@ -385,8 +387,13 @@ function system_install() { db_query("INSERT INTO {node_access} (nid, gid, realm, grant_view, grant_update, grant_delete) VALUES (%d, %d, '%s', %d, %d, %d)", 0, 0, 'all', 1, 0, 0); // Add input formats. - db_query("INSERT INTO {filter_formats} (name, roles, cache) VALUES ('%s', '%s', %d)", 'Filtered HTML', ',1,2,', 1); - db_query("INSERT INTO {filter_formats} (name, roles, cache) VALUES ('%s', '%s', %d)", 'Full HTML', '', 1); + db_query("INSERT INTO {filter_formats} (name, cache, weight) VALUES ('%s', %d, %d)", 'Filtered HTML', 1, 0); + db_query("INSERT INTO {filter_formats} (name, cache, weight) VALUES ('%s', %d, %d)", 'Full HTML', 1, 0); + db_query("INSERT INTO {filter_formats} (name, cache, weight) VALUES ('%s', %d, %d)", 'Plain text', 1, 1); + $fallback_format = db_last_insert_id('filter_formats', 'format'); + if ($fallback_format != 3) { + db_query("INSERT INTO {variable} (name, value) VALUES ('%s','%s')", 'filter_fallback_format', serialize($fallback_format)); + } // Enable filters for each input format. @@ -408,6 +415,11 @@ function system_install() { // HTML corrector filter. db_query("INSERT INTO {filters} (format, module, delta, weight) VALUES (%d, '%s', %d, %d)", 2, 'filter', 3, 10); + // Plain text: + // Use the line break filter only (check_plain is hardcoded separately + // for this input format). + db_query("INSERT INTO {filters} (format, module, delta, weight) VALUES (%d, '%s', %d, %d)", 3, 'filter', 1, 0); + db_query("INSERT INTO {variable} (name, value) VALUES ('%s','%s')", 'filter_html_1', 'i:1;'); db_query("INSERT INTO {variable} (name, value) VALUES ('%s', '%s')", 'node_options_forum', 'a:1:{i:0;s:6:"status";}');