Index: CHANGELOG.txt =================================================================== RCS file: /cvs/drupal/drupal/CHANGELOG.txt,v retrieving revision 1.300 diff -u -p -r1.300 CHANGELOG.txt --- CHANGELOG.txt 15 Mar 2009 01:53:16 -0000 1.300 +++ CHANGELOG.txt 7 Apr 2009 04:33:11 -0000 @@ -33,6 +33,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 optional filter that can use [internal:node/123] to link to internal pages. @@ -41,6 +42,8 @@ Drupal 7.0, xxxx-xx-xx (development vers objects in a single database query. - Documentation: * Hook API documentation now included in Drupal core. +- Filter system: + * Added support for default text 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.326 diff -u -p -r1.326 form.inc --- includes/form.inc 30 Mar 2009 03:15:40 -0000 1.326 +++ includes/form.inc 7 Apr 2009 04:33:12 -0000 @@ -1883,7 +1883,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.325 diff -u -p -r1.325 block.module --- modules/block/block.module 5 Apr 2009 12:31:56 -0000 1.325 +++ modules/block/block.module 7 Apr 2009 04:33:12 -0000 @@ -198,7 +198,7 @@ function block_block_list() { * Implementation of hook_block_configure(). */ function block_block_configure($delta = 0, $edit = array()) { - $box = array('format' => FILTER_FORMAT_DEFAULT); + $box = array('format' => filter_default_format()); if ($delta) { $box = block_box_get($delta); } @@ -383,7 +383,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, @@ -395,7 +395,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.147 diff -u -p -r1.147 blogapi.module --- modules/blogapi/blogapi.module 1 Apr 2009 01:28:24 -0000 1.147 +++ modules/blogapi/blogapi.module 7 Apr 2009 04:33:12 -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. @@ -231,6 +231,12 @@ function blogapi_blogger_new_post($appke return $valid; } + // Attach the node format to the body field, in the manner expected by the + // validation and submission handlers. + // @see form_process_text_format() + $edit['body_format'] = $edit['format']; + unset($edit['format']); + node_validate($edit); if ($errors = form_get_errors()) { return blogapi_error(implode("\n", $errors)); @@ -289,6 +295,12 @@ function blogapi_blogger_edit_post($appk return $valid; } + // Attach the node format to the body field, in the manner expected by the + // validation and submission handlers. + // @see form_process_text_format() + $node->body_format = $node->format; + unset($node->format); + node_validate($node); if ($errors = form_get_errors()) { return blogapi_error(implode("\n", $errors)); @@ -622,7 +634,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.701 diff -u -p -r1.701 comment.module --- modules/comment/comment.module 4 Apr 2009 12:35:16 -0000 1.701 +++ modules/comment/comment.module 7 Apr 2009 04:33:12 -0000 @@ -1565,7 +1565,7 @@ function comment_form(&$form_state, $edi '#title' => t('Comment'), '#rows' => 15, '#default_value' => $default, - '#text_format' => isset($edit['format']) ? $edit['format'] : FILTER_FORMAT_DEFAULT, + '#text_format' => isset($edit['format']) ? $edit['format'] : filter_default_format(), '#required' => TRUE, ); Index: modules/field/modules/text/text.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/text/text.module,v retrieving revision 1.5 diff -u -p -r1.5 text.module --- modules/field/modules/text/text.module 26 Mar 2009 13:31:25 -0000 1.5 +++ modules/field/modules/text/text.module 7 Apr 2009 04:33:12 -0000 @@ -240,7 +240,7 @@ function text_elements() { '#input' => TRUE, '#columns' => array('value', 'format'), '#delta' => 0, '#process' => array('text_textarea_process'), - '#filter_value' => FILTER_FORMAT_DEFAULT, + '#filter_value' => filter_default_format(), ), ); } @@ -332,7 +332,7 @@ function text_textfield_process($element if (!empty($instance['settings']['text_processing'])) { $filter_key = $element['#columns'][1]; - $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(); $parents = array_merge($element['#parents'] , array($filter_key)); $element[$filter_key] = filter_form($format, 1, $parents); } @@ -371,7 +371,7 @@ function text_textarea_process($element, 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(); $parents = array_merge($element['#parents'] , array($filter_key)); $element[$filter_key] = filter_form($format, 1, $parents); } Index: modules/filter/filter.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/filter/filter.admin.inc,v retrieving revision 1.25 diff -u -p -r1.25 filter.admin.inc --- modules/filter/filter.admin.inc 8 Mar 2009 21:25:18 -0000 1.25 +++ modules/filter/filter.admin.inc 7 Apr 2009 04:33:12 -0000 @@ -7,52 +7,47 @@ */ /** - * 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; $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; - } - } - $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'))); - $form[$id]['configure'] = array('#markup' => l(t('configure'), 'admin/settings/filter/' . $id)); - $form[$id]['delete'] = array('#markup' => $default ? '' : l(t('delete'), 'admin/settings/filter/delete/' . $id)); + // Check whether this is the fallback text 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'); + } + $form[$id]['roles'] = array('#markup' => $roles_markup); + $form[$id]['configure'] = array('#markup' => $is_fallback_format ? '' : l(t('configure'), 'admin/settings/filter/' . $id)); + $form[$id]['delete'] = array('#markup' => $is_fallback_format ? '' : l(t('delete'), 'admin/settings/filter/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_format} SET weight = %d WHERE format = %d", $data['weight'], $id); } } + filter_format_reset_cache(); drupal_set_message(t('The text format ordering has been saved.')); } @@ -68,9 +63,8 @@ function theme_filter_admin_overview($fo $element['weight']['#attributes']['class'] = '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']), @@ -80,7 +74,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); @@ -95,8 +89,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); } @@ -108,12 +103,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, @@ -121,23 +110,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 = 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); @@ -221,26 +193,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_format} SET cache = %d, name='%s', roles = '%s' WHERE format = %d", $cache, $name, $roles, $format); + db_update('filter_format') + ->fields(array( + 'cache' => $cache, + 'name' => $name, + )) + ->condition('format', $format) + ->execute(); + filter_format_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. @@ -253,30 +214,29 @@ 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 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 * @see filter_admin_delete_submit() */ -function filter_admin_delete() { - $format = arg(4); - $format = db_fetch_object(db_query('SELECT * FROM {filter_format} 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 text format %format?', array('%format' => $format->name)), 'admin/settings/filter', 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('Delete'), t('Cancel')); - } - else { - drupal_set_message(t('The default format cannot be deleted.')); - drupal_goto('admin/settings/filter'); - } - } - else { - drupal_not_found(); - } +function filter_admin_delete(&$form_state, $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 text format %format?', array('%format' => $format->name)), 'admin/settings/filter', t('If you have any content left in this text format, it will be displayed as %plain_text until you manually assign it a new format. This action cannot be undone.', array('%plain_text' => filter_fallback_format_title())), t('Delete'), t('Cancel')); } /** @@ -286,12 +246,19 @@ function filter_admin_delete_submit($for db_query("DELETE FROM {filter_format} WHERE format = %d", $form_state['values']['format']); db_query("DELETE FROM {filter} 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_revision} SET format = %d WHERE format = %d", $default, $form_state['values']['format']); - db_query("UPDATE {comment} 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 format. + $fallback_format = filter_fallback_format(); + $database_tables = array('node_revision', 'comment', 'box'); + foreach ($database_tables as $table) { + if (db_table_exists($table)) { + db_update($table) + ->fields(array('format' => $fallback_format)) + ->condition('format', $form_state['values']['format']) + ->execute(); + } + } + filter_format_reset_cache(); cache_clear_all($form_state['values']['format'] . ':', 'cache_filter', TRUE); drupal_set_message(t('Deleted text format %format.', array('%format' => $form_state['values']['name']))); @@ -299,12 +266,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); } @@ -346,6 +313,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.12 diff -u -p -r1.12 filter.install --- modules/filter/filter.install 21 Jan 2009 16:58:42 -0000 1.12 +++ modules/filter/filter.install 7 Apr 2009 04:33:12 -0000 @@ -64,13 +64,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, @@ -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() { @@ -132,3 +130,73 @@ function filter_update_7002() { db_rename_table($ret, 'filter_formats', 'filter_format'); return $ret; } + +/** + * Move text format access to the user permissions handler, create a fallback + * (plain text) text format, and explicitly set the text format in cases that + * used to rely on a single site-wide default. + */ +function filter_update_7003() { + $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}"); + while ($format = db_fetch_object($result)) { + // 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)) { + $ret[] = update_sql("INSERT INTO {role_permission} (rid, permission) VALUES (" . $format_role . ", '" . filter_permission_name($format) . "')"); + } + } + } + + // 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 last for all users. + $ret[] = update_sql("INSERT INTO {filter_format} (name, cache, weight) VALUES ('Plain text', 1, 1)"); + $fallback_format = db_last_insert_id('filter_format', 'format'); + if ($fallback_format != filter_fallback_format()) { + variable_set('filter_fallback_format', $fallback_format); + } + // The fallback text format should output plain text, so we escape all HTML + // and apply the line break filter only. + $ret[] = update_sql("INSERT INTO {filter} (format, module, delta, weight) VALUES (" . $fallback_format . ", 'filter', 4, 0)"); + $ret[] = update_sql("INSERT INTO {filter} (format, module, delta, weight) VALUES (" . $fallback_format . ", 'filter', 1, 1)"); + + // 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. + $ret[] = update_sql("UPDATE {filter_format} SET 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 text format. Therefore, we have to convert all such + // instances (in Drupal core) to explicitly use the appropriate format. + $ret[] = update_sql("UPDATE {box} SET format = " . $default_format . " WHERE format = 0"); + $ret[] = update_sql("UPDATE {comment} SET format = " . $default_format . " WHERE format = 0"); + // The {node_revisions} table is a special case, since there, a value of + // "0" could also mean that the revision was saved without the node body + // enabled, so that it does not have any text format at all. We can't + // distinguish these cases exactly, so we assume that a revision with + // text in the body field is intended to be displayed in the default + // format, while a revision with no text in the body field was saved with + // an undefined format. In the latter case, we leave the format at "0", + // which is the value of the new defined constant FILTER_FORMAT_UNASSIGNED + // that is used for this purpose. + $ret[] = update_sql("UPDATE {node_revisions} SET format = " . $default_format . " WHERE format = 0 AND body != '' AND body IS NOT NULL"); + + // 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.246 diff -u -p -r1.246 filter.module --- modules/filter/filter.module 30 Mar 2009 03:15:40 -0000 1.246 +++ modules/filter/filter.module 7 Apr 2009 04:33:12 -0000 @@ -7,12 +7,15 @@ */ /** - * Special format ID which means "use the default format". + * Special format ID which is used to indicate that a piece of text does + * not have a text format assigned. * - * This value can be passed to the filter APIs as a format ID: this is - * equivalent to not passing an explicit format at all. + * This value can be passed to the filter APIs as a format ID; this is + * equivalent to not passing an explicit format at all. In particular, if + * text with this format is ever displayed, it will be filtered using the + * fallback format available to all users. */ -define('FILTER_FORMAT_DEFAULT', 0); +define('FILTER_FORMAT_UNASSIGNED', 0); /** * Implementation of hook_help(). @@ -22,12 +25,13 @@ 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('The special %plain_text text 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/filter': - $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. To change the order of an text 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 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 %plain_text format is available to all users. You can configure access to other text 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 text formats in order of your preference for their use, since the default text format for each user is the first one on the list for which that user has access. To change the order of a text 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/filter/%': 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/filter/' . $arg[3] . '/order'))) . '

'; @@ -88,11 +92,12 @@ function filter_menu() { 'type' => MENU_LOCAL_TASK, 'weight' => 1, ); - $items['admin/settings/filter/delete'] = array( + $items['admin/settings/filter/delete/%filter_format'] = array( 'title' => 'Delete text format', 'page callback' => 'drupal_get_form', - 'page arguments' => array('filter_admin_delete'), - 'access arguments' => array('administer filters'), + 'page arguments' => array('filter_admin_delete', 4), + 'access callback' => 'filter_edit_format_access', + 'access arguments' => array(4), 'type' => MENU_CALLBACK, ); $items['filter/tips'] = array( @@ -107,7 +112,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/filter/%filter_format/edit'] = array( 'title' => 'Edit', @@ -118,7 +124,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, ); @@ -126,15 +133,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 text 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 text formats. + */ +function filter_edit_format_access($format) { + // The fallback format can never be edited. + return user_access('administer filters') && ($format->format != filter_fallback_format()); } /** @@ -148,12 +171,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 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/filter/' . $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; } /** @@ -286,41 +342,83 @@ function filter_filter_tips($delta, $for } /** - * Retrieve a list of text formats. - */ -function filter_formats($index = NULL) { - global $user; - static $formats; - - // Administrators can always use all text formats. - $all = user_access('administer filters'); - - if (!isset($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 + * Whether to reset the internal cache of all text formats. Defaults to + * FALSE. + */ +function filter_formats($account = NULL, $reset = FALSE) { + $formats = &drupal_static(__FUNCTION__, array()); + if ($reset) { $formats = array(); + } - $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'); + // 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'); + } + + // 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 text formats. + */ +function filter_format_reset_cache() { + filter_formats(NULL, TRUE); +} + +/** + * Return the ID of the default text 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 text format that all users have access to. + */ +function filter_fallback_format() { + return variable_get('filter_fallback_format', 3); +} + +/** + * Display the title of the fallback text format. + */ +function filter_fallback_format_title() { + return filter_admin_format_title(filter_format_load(filter_fallback_format())); } /** @@ -352,11 +450,13 @@ function _filter_list_cmp($a, $b) { } /** - * Resolve a format id, including the default format. + * Resolve a format id, including an unassigned format (which defaults to + * using the fallback format available to all users). */ function filter_resolve_format($format) { - return $format == FILTER_FORMAT_DEFAULT ? variable_get('filter_default_format', 1) : $format; + return $format == FILTER_FORMAT_UNASSIGNED ? filter_fallback_format() : $format; } + /** * Check if text in a certain text format is allowed to be cached. */ @@ -411,8 +511,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 @@ -424,9 +524,10 @@ 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 = FILTER_FORMAT_UNASSIGNED, $langcode = '', $check = TRUE) { // When $check = TRUE, do an access check on $format. if (isset($text) && (!$check || filter_access($format))) { + // Make sure to use the fallback format if none was provided. $format = filter_resolve_format($format); // Check for a cached version of this piece of text. @@ -435,9 +536,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); @@ -456,7 +554,7 @@ function check_markup($text, $format = F } // 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)); } } @@ -481,9 +579,24 @@ 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 = FILTER_FORMAT_UNASSIGNED, $weight = NULL, $parents = array('format')) { + global $user; + + // Use the default format for this user if none was selected. + if ($value == FILTER_FORMAT_UNASSIGNED) { + $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. The fallback text + // format is designed for this situation, so we pre-select that format + // on the form. + if (!isset($formats[$value])) { + $value = filter_fallback_format(); + } drupal_add_js('misc/form.js'); drupal_add_css(drupal_get_path('module', 'filter') . '/filter.css'); @@ -526,17 +639,74 @@ function filter_form($value = FILTER_FOR } /** - * Returns TRUE if the user is allowed to access this format. + * Check 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))) { +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; + $format_id = filter_resolve_format($format_id); + if (user_access('administer filters', $account) || $format_id == filter_fallback_format()) { return TRUE; } - else { - $formats = filter_formats(); - return isset($formats[$format]); + // 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 !empty($permission) && user_access($permission, $account); +} + +/** + * Retrieve 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 roles structured $rid => $role_name. + */ +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(); + } + // 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(); +} + +/** + * Retrieve a list of text 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; } /** @@ -548,11 +718,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_format} WHERE format = %d", $format))); + $formats = array(filter_format_load($format)); } $tips = array(); Index: modules/filter/filter.test =================================================================== RCS file: /cvs/drupal/drupal/modules/filter/filter.test,v retrieving revision 1.18 diff -u -p -r1.18 filter.test --- modules/filter/filter.test 31 Mar 2009 01:49:52 -0000 1.18 +++ modules/filter/filter.test 7 Apr 2009 04:33:12 -0000 @@ -1,7 +1,79 @@ admin_user = $this->drupalCreateUser(array('administer filters', 'create page content', 'edit own page content')); + } + + /** + * Create a new text format. + * + * @param $filters + * An array containing the combined 'module/delta' value for each filter + * to be added to this text format. + * @param $name + * The name of the text format. (If not set, a random name is used.) + * @return + * An object representing the new text format. + */ + protected function drupalCreateTextFormat($filters = array(), $name = NULL) { + $edit = array(); + + // Add the parameters that were provided. + foreach ($filters as $filter) { + $edit['filters[' . $filter . ']'] = TRUE; + } + $edit['name'] = !isset($name) ? $this->randomName() : $name; + + // Submit the form. + $this->drupalPost('admin/settings/filter/add', $edit, t('Save configuration')); + + // Verify that the text format exists in the database. + $format = db_query("SELECT * FROM {filter_format} WHERE name = :name", array(':name' => $edit['name']))->fetchObject(); + $this->assertTrue($format, t('New text format successfully loaded.')); + + // Clear the static permission cache, since a new permission has been + // added as a result of the new text format that was just created. + filter_format_reset_cache(); + $this->checkPermissions(array(), TRUE); + + return $format; + } + + /** + * Delete a text format. + * + * @param $format + * The ID of the text format to delete. + */ + protected function drupalDeleteTextFormat($format) { + // Submit the form. + $this->drupalPost('admin/settings/filter/delete/' . $format, array(), t('Delete')); + + // Verify that the deleted text format does not exist in the database. + $format = db_query("SELECT * FROM {filter_format} WHERE format = :format", array(':format' => $format))->fetchObject(); + $this->assertFalse($format, t('Text format deleted successfully.')); + } + + /** + * Load a text format by name. + * + * @param $name + * The name of the text format to load. + * @return + * The fully-loaded text format object. + */ + protected function getTextFormatByName($name) { + return db_query("SELECT * FROM {filter_format} WHERE name = :name", array(':name' => $name))->fetchObject(); + } + +} + +class FilterAdminTestCase extends FilterHelperTestCase { public static function getInfo() { return array( 'name' => t('Filter administration functionality'), @@ -17,41 +89,28 @@ class FilterAdminTestCase extends Drupal $first_filter = 2; // URL filter. $second_filter = 1; // Line filter. - // Create users. - $admin_user = $this->drupalCreateUser(array('administer filters')); - $web_user = $this->drupalCreateUser(array('create page content')); - - $this->drupalLogin($admin_user); + $this->drupalLogin($this->admin_user); - list($filtered, $full) = $this->checkFilterFormats(); - - // Change default filter. - $edit = array(); - $edit['default'] = $full; - $this->drupalPost('admin/settings/filter', $edit, t('Save changes')); - $this->assertText(t('Default format updated.'), t('Default filter updated successfully.')); - - $this->assertNoRaw('admin/settings/filter/delete/' . $full, t('Delete link not found.')); - - // Add an additional tag. + // Add an additional tag to the Filtered HTML format. + $filtered_html = $this->getTextFormatByName('Filtered HTML'); + $filtered_html_id = $filtered_html->format; $edit = array(); $edit['allowed_html_1'] = '