diff --git a/aloha.module b/aloha.module index 3e3eef9..6c28e4b 100644 --- a/aloha.module +++ b/aloha.module @@ -35,6 +35,18 @@ function aloha_menu() { } /** + * Implements hook_editor_info(). + */ +function aloha_editor_info() { + $editors['aloha'] = array( + 'label' => t('Aloha'), + 'library' => array('aloha', 'aloha-for-textareas'), + 'js settings callback' => 'aloha_add_format_settings', + ); + return $editors; +} + +/** * Implements hook_library_info(). */ function aloha_library_info() { @@ -222,6 +234,7 @@ function aloha_library_info() { 'plugins' => array('load' => array('common/contenthandler')), 'contentHandler' => array( 'allows' => array('elements' => array(), 'attributes' => array()), + 'handler' => array('sanitize' => array()), ), ))), ), @@ -411,79 +424,6 @@ function aloha_library_info() { } /** - * Implements hook_element_info_alter(). - */ -function aloha_element_info_alter(&$types) { - $types['text_format']['#pre_render'][] = 'aloha_pre_render_text_format'; -} - -/** - * Render API callback: processes a text format widget to load and attach Aloha - * Editor. - * - * Uses the element's #id as reference to attach Aloha Editor. - */ -function aloha_pre_render_text_format($element) { - // filter_process_format() copies properties to the expanded 'value' child - // element. Skip this text format widget, if it contains no 'format' or when - // the current user does not have access to edit the value. - if (!isset($element['format']) || !empty($element['value']['#disabled'])) { - return $element; - } - // Allow modules to programmatically enforce no client-side editor by setting - // the #wysiwyg property to FALSE. - if (isset($element['#wysiwyg']) && !$element['#wysiwyg']) { - return $element; - } - - $format_field = &$element['format']; - $field = &$element['value']; - - // Use a hidden element for a single text format. - if (!$format_field['format']['#access']) { - $format_field['aloha'] = array( - '#type' => 'hidden', - '#name' => $format_field['format']['#name'], - '#value' => $field['#format'], - '#attributes' => array( - 'id' => $format_field['format']['#id'], - 'class' => array('aloha-formatselector-for-textarea'), - ), - '#attached' => array( - 'library' => array( - array('aloha', 'aloha-for-textareas'), - ), - 'js' => array( - array( - 'type' => 'setting', - 'data' => array('aloha' => array('textareas' => array( - $format_field['format']['#id'] => $field['#id'], - ))), - ), - ), - 'aloha_add_format_settings' => array( - array() - ), - ), - ); - } - // Otherwise, attach to text format selector. - else { - $format_field['format']['#attributes']['class'][] = 'aloha-formatselector-for-textarea'; - $format_field['format']['#attached']['library'][] = array('aloha', 'aloha-for-textareas'); - $format_field['format']['#attached']['js'][] = array( - 'type' => 'setting', - 'data' => array('aloha' => array('textareas' => array( - $format_field['format']['#id'] => $field['#id'], - ))), - ); - $format_field['format']['#attached']['aloha_add_format_settings'][] = array(); - } - - return $element; -} - -/** * Implements hook_filter_format_update(). */ function aloha_filter_format_update($format) { @@ -491,32 +431,6 @@ function aloha_filter_format_update($format) { } /** - * Get a list of HTML tags and attributes that are allowed by a given text - * format, by performing black-box testing. - * - * @param string $format_id - * A text format ID. - * @return array - * An array of which the keys list all allowed tags and the corresponding - * values list the allowed attributes. An empty array as value means no - * attributes are allowed, array('*') means all attributes are allowed. In - * other cases, it's an enumeration of the allowed attributes, plus "data-" if - * any data- attribute is allowed. - */ -function aloha_get_allowed_html_by_format($format_id) { - $cache_id = 'aloha-allowed-html:' . $format_id; - if ($cached = cache()->get($cache_id)) { - return $cached->data; - } - - module_load_include('inc', 'aloha', 'includes/format'); - $allowed_html = _aloha_calculate_allowed_html($format_id); - cache()->set($cache_id, $allowed_html); - - return $allowed_html; -} - -/** * Check the compatibility of Aloha Editor with a given text format. * * Aloha Editor can only work if the br and p tags are allowed by the text @@ -560,132 +474,124 @@ function aloha_check_format_compatibility($format_id) { } /** - * Adds Drupal.settings.aloha.formats and Aloha.settings.plugins.drupal. Ensures - * Aloha Editor has the metadata to deal with formats. + * Filter JS settings callback; Add Aloha settings to the page for a format. * - * @todo refactor; how to make the mapping of Aloha cleaner? + * @param $formats + * The filter formats for which Aloha is adding its settings. * @todo revisit mapping when this Aloha issue is solved: * https://github.com/alohaeditor/Aloha-Editor/issues/794 */ -function aloha_add_format_settings() { - $format_settings_added = &drupal_static(__FUNCTION__, FALSE); - - if (!$format_settings_added) { - $plugins = array('format', 'list', 'link', 'cite', 'align', 'image'); - // Mapping from HTML element to Aloha Editor plug-in. - $tag_mapping = array( - 'a' => 'link', - // Drupal does not approve . - 'b' => FALSE, - 'blockquote' => 'cite', - 'br' => 'format', - 'cite' => 'cite', - 'code' => 'format', - 'em' => 'format', - 'h1' => 'format', - 'h2' => 'format', - 'h3' => 'format', - 'h4' => 'format', - 'h5' => 'format', - 'h6' => 'format', - // Drupal does not approve . - 'i' => FALSE, - 'img' => 'image', - 'li' => 'list', - 'ol' => 'list', - 'p' => 'format', - 'q' => 'cite', - 'pre' => 'format', - 's' => 'format', - 'strong' => 'format', - 'sub' => 'format', - 'sup' => 'format', - 'u' => 'format', - 'ul' => 'list', - 'u' => 'format', - ); - // Special cases. - $tag_mapping['removeFormat'] = 'format'; - $attr_mapping = array( - 'p' => array('style' => array(array('align', array('left', 'right', 'center', 'justify')))), - ); - - global $user; - $formats = filter_formats($user); - $settings['formats'] = array(); - - // Gather all necessary metadata for each format available to the current - // user. - foreach (array_keys($formats) as $format_id) { - $class = 'text-format-' . drupal_html_class($format_id); - $allowed_html = aloha_get_allowed_html_by_format($format_id); +function aloha_add_format_settings($format, $filters, $existing_settings) { + $plugins = array('format', 'list', 'link', 'cite', 'align', 'image'); + // Mapping from HTML element to Aloha Editor plug-in. + $tag_mapping = array( + 'a' => 'link', + // Drupal does not approve . + 'b' => FALSE, + 'blockquote' => 'cite', + 'br' => 'format', + 'cite' => 'cite', + 'code' => 'format', + 'em' => 'format', + 'h1' => 'format', + 'h2' => 'format', + 'h3' => 'format', + 'h4' => 'format', + 'h5' => 'format', + 'h6' => 'format', + // Drupal does not approve . + 'i' => FALSE, + 'img' => 'image', + 'li' => 'list', + 'ol' => 'list', + 'p' => 'format', + 'q' => 'cite', + 'pre' => 'format', + 's' => 'format', + 'strong' => 'format', + 'sub' => 'format', + 'sup' => 'format', + 'u' => 'format', + 'ul' => 'list', + 'u' => 'format', + ); + // Special cases. + $tag_mapping['removeFormat'] = 'format'; + $attr_mapping = array( + 'p' => array('style' => array(array('align', array('left', 'right', 'center', 'justify')))), + ); - // Let drupal.aloha.js know which text formats are compatible and which - // class name should be set on the editable. - $settings['formats'][$format_id] = array( - 'id' => $format_id, - // Only enable Aloha Editor if it is compatible with the text format. - 'status' => aloha_check_format_compatibility($format_id), - // The class that will be set on the editable whenever this text format - // is active. - 'className' => $class, - ); + $format_name = $format->format; + $allowed_tags = aloha_get_allowed_html_by_format($format_name); - // Let the relevant Aloha Editor plug-ins know which HTML tags are - // allowed by this text format. Aloha Editor will only show the buttons - // for the allowed HTML tags. - // However, by default, explicitly disallow plug-ins from using defaults - // by setting an empty array. - foreach ($plugins as $plugin) { - $settings['settings']['plugins'][$plugin]['editables']['.' . $class] = array(); + // Let the relevant Aloha Editor plug-ins know which HTML tags are + // allowed by this text format. Aloha Editor will only show the buttons + // for the allowed HTML tags. + // However, by default, explicitly disallow plug-ins from using defaults + // by setting an empty array. + foreach ($plugins as $plugin) { + $settings['plugins'][$plugin] = array(); + } + $tags = array_keys($allowed_tags); + foreach ($tags as $tag) { + if (!isset($tag_mapping[$tag])) { + continue; + } + $plugin = $tag_mapping[$tag]; + if ($plugin === FALSE) { + continue; + } + $settings['plugins'][$plugin][] = $tag; + // For some, there also exists an attribute mapping. + if (isset($attr_mapping[$tag])) { + $attrs = array_keys($attr_mapping[$tag]); + $allowed_attrs = $allowed_tags[$tag]; + if ($format_name == 'filtered_html') { + $allowed_attrs = array(); } - $tags = array_keys($allowed_html); - foreach ($tags as $tag) { - if (!isset($tag_mapping[$tag])) { - continue; - } - $plugin = $tag_mapping[$tag]; - if ($plugin === FALSE) { - continue; - } - $settings['settings']['plugins'][$plugin]['editables']['.' . $class][] = $tag; - // For some, there also exists an attribute mapping. - if (isset($attr_mapping[$tag])) { - $attrs = array_keys($attr_mapping[$tag]); - $allowed_attrs = $allowed_html[$tag]; - if ($format_id == 'filtered_html') { - $allowed_attrs = array(); - } - if ($allowed_attrs === array('*')) { - $allowed_attrs = $attrs; - } - foreach ($attrs as $attr) { - if (in_array($attr, $allowed_attrs)) { - foreach ($attr_mapping[$tag][$attr] as $config_for_plugin) { - list($plugin, $config) = $config_for_plugin; - $settings['settings']['plugins'][$plugin]['editables']['.' . $class] = $config; - } - } - } - } + if ($allowed_attrs === array('*')) { + $allowed_attrs = $attrs; } - - // Let Aloha Editor's "sanitize" content handler know which HTML tags and - // attributes are allowed by this text format. Aloha Editor will then - // ensure that when pasting content, nothing in the pasted content that is - // disallowed will continue to exist. - foreach ($allowed_html as $tag => $attributes) { - $settings['settings']['contentHandler']['handler']['sanitize']['.' . $class]['elements'][] = $tag; - if ($attributes !== array('*')) { - $settings['settings']['contentHandler']['handler']['sanitize']['.' . $class]['attributes'][$tag] = $attributes; + foreach ($attrs as $attr) { + if (in_array($attr, $allowed_attrs)) { + foreach ($attr_mapping[$tag][$attr] as $config_for_plugin) { + list($plugin, $config) = $config_for_plugin; + $settings['plugins'][$plugin] = $config; + } } } } + } + $settings['allowedTags'] = $allowed_tags; + + return $settings; +} - drupal_add_js(array('aloha' => $settings), array('type' => 'setting')); - $format_settings_added = TRUE; +/** + * Get a list of HTML tags and attributes that are allowed by a given text + * format, by performing black-box testing. + * + * @param string $format_name + * A text format name. + * @return array + * An array of which the keys list all allowed tags and the corresponding + * values list the allowed attributes. An empty array as value means no + * attributes are allowed, array('*') means all attributes are allowed. In + * other cases, it's an enumeration of the allowed attributes, plus "data-" if + * any data- attribute is allowed. + */ +function aloha_get_allowed_html_by_format($format_name) { + $cache_id = 'aloha-allowed-tags:' . $format_name; + if ($cached = cache()->get($cache_id)) { + return $cached->data; } + + module_load_include('inc', 'aloha', 'includes/format'); + $allowed_tags = _aloha_calculate_allowed_tags($format_name); + cache()->set($cache_id, $allowed_tags); + + return $allowed_tags; } /** diff --git a/includes/format.inc b/includes/format.inc index ddf1146..2ed9764 100644 --- a/includes/format.inc +++ b/includes/format.inc @@ -1,153 +1,216 @@ 'p', - // For a single

that would be stripped, filter_autop would generate - // it again, making it impossible to detect that filter_html actually - // stripped it away. Hence we need to use two adjacent

tags. - 'testcase' => '

Hello, world!

This is Drupal!

', - 'attributes' => array( - 'prefix' => '

'>Hello, world!

', - 'names' => array('dir'), - ), - ), - array( - 'tag' => 'blockquote', - 'testcase' => '
Veni, vidi, vici.
', - 'attributes' => array( - 'prefix' => '
'>Veni, vidi, vici!
', - 'names' => array('cite'), - ), - ), - array('tag' => 'h1', 'testcase' => '

heading 1

'), - array('tag' => 'h2', 'testcase' => '

heading 2

'), - array('tag' => 'h3', 'testcase' => '

heading 3

'), - array('tag' => 'h4', 'testcase' => '

heading 4

'), - array('tag' => 'h5', 'testcase' => '
heading 5
'), - array('tag' => 'h6', 'testcase' => '
heading 6
'), - array('tag' => 'ul', 'testcase' => '
  • A
  • B
'), - array('tag' => 'ol', 'testcase' => '
  1. A
  2. B
'), - array('tag' => 'li', 'testcase' => '
  • A
  • '), - array('tag' => 'pre', 'testcase' => '
    preformatted text
    '), - // Text-level elements. (Should all be wrapped in

    tags.) - array( - 'tag' => 'a', - 'testcase' => '

    hyperlink

    ', - 'attributes' => array( - 'prefix' => '

    '>hyperlink

    ', - 'names' => array('href', 'target', 'rel', 'media'), - ), - ), - array('tag' => 'em', 'testcase' => '

    emphasized

    '), - array('tag' => 'strong', 'testcase' => '

    strong

    '), - array('tag' => 'i', 'testcase' => '

    italicized

    '), - array('tag' => 'b', 'testcase' => '

    bold

    '), - array('tag' => 'u', 'testcase' => '

    underlined

    '), - array('tag' => 'cite', 'testcase' => '

    He said "WOW, Drupal rocks!"

    '), - array('tag' => 'q', 'testcase' => '

    He said "WOW, Drupal rocks!"

    '), - array('tag' => 'br', 'testcase' => '

    First line
    Second line

    '), - array( - 'tag' => 'code', - 'testcase' => '

    Hello, world!

    ', - 'attributes' => array( - 'prefix' => '

    '>Hello, world!

    ', - 'names' => array('lang'), - ), - ), - array( - 'tag' => 'img', - 'testcase' => '

    ', - 'attributes' => array( - 'prefix' => '

    ' />

    ', - 'names' => array('src', 'alt', 'title') - ), - ), - ); -} - -/** - * Calculates the allowed HTML tags. - * - * @param string $format_id - * A text format ID. - * @return - * @see aloha_get_allowed_html_by_format() + * Returns list of allowed tags for a particular format. Do not call directly. * - * @todo Refactor. - * @todo Unit tests. Start from http://drupal.org/node/1782838#comment-6562024. + * @see filter_allowed_tag_list(). */ -function _aloha_calculate_allowed_html($format_id) { - // For testing whether any attribute is allowed. - $any = 'thisIsAnExtremelyUnlikelyToExistAttributeName'; - - // For testing whether data- attributes are allowed. - $data = 'data-' . chr(mt_rand(65, 90)); - - // Helper function to assert the filtered text still contains the original. - $assert_match = function ($original, $format_id) { - $filtered = check_markup($original, $format_id); - $result = preg_replace("/>\s+<", $filtered); - return stripos($result, $original) !== FALSE; - }; - - // Run all the above tagtests to determine which tags and attributes are - // allowed in the given text format. - $allowed_html = array_reduce( - _aloha_allowed_html_tests(), - function($result, $tagtest) use ($assert_match, $format_id, $any, $data) { - $tag = $tagtest['tag']; - if ($assert_match($tagtest['testcase'], $format_id)) { - $result[$tag] = array(); +function _aloha_calculate_allowed_tags($format_name) { + $allowed_tags = array(); + $tag_information = _aloha_tag_information(); - // Break early if there are no attributes to test. - if (!isset($tagtest['attributes'])) { - return $result; - } + // Determine which tags and attributes are allowed in the given text format. + foreach ($tag_information as $tag_name => $tag_info) { + $test_case = _aloha_tag_test_case($tag_name, $tag_info); + $test_result = check_markup($test_case, $format_name, NULL, FALSE, array(FILTER_TYPE_MARKUP_LANGUAGE)); + if (strcasecmp($test_result, $test_case) === 0) { + // The tag is allowed. + $allowed_tags[$tag_name] = array(); - // Anonymous function to generate attribute test cases. - $generate_attr_testcase = function($attr) use ($tagtest) { - $case = ''; - $case .= $tagtest['attributes']['prefix']; - $case .= $attr . '="' . $attr . ' attribute test"'; - $case .= $tagtest['attributes']['suffix']; - return $case; - }; + // Make sure required attributes are included. + if (isset($tag_test['required'])) { + $allowed_tags[$tag_name] = array_keys($tag_test['required']); + } - // The tag test passed, then we can also do the attribute tests. First, - // check if any attribute is allowed, if that's not the case, then - // figure out which specific attributes are allowed. - if ($assert_match($generate_attr_testcase($any), $format_id)) { - $result[$tag][] = '*'; - } - else { - $attrs = array_merge(array($data), $tagtest['attributes']['names']); - foreach ($attrs as $attr) { - if ($assert_match($generate_attr_testcase($attr), $format_id)) { - $result[$tag][] = ($attr === $data) ? 'data-' : $attr; - } + // Check which particular attributes are allowed. + list($tag_prefix, $tag_suffix1, $tag_suffix2) = preg_split('/([ >])/', $test_case, 2, PREG_SPLIT_DELIM_CAPTURE); + $tag_suffix = $tag_suffix1 . $tag_suffix2; + if (isset($tag_test['optional'])) { + foreach ($tag_test['optional'] as $attribute => $attribute_value) { + $attribute_string = $attribute . (isset($attribute_value) ? '="' . $attribute_value . '"' : ''); + $attribute_testcase = $tag_prefix . " $attribute_string" . $tag_suffix; + $test_result = check_markup($attribute_testcase, $format_name, NULL, FALSE, array(FILTER_TYPE_MARKUP_LANGUAGE)); + if (strcasecmp($test_result, $attribute_testcase) === 0) { + $allowed_tags[$tag_name][] = $attribute; } } } - return $result; - }, - array() + // Check for data-* attributes + $data = 'data-extremelyUnlikelyDataAttribute'; + $attribute_testcase = $tag_prefix . ' ' . $data . '=""' . $tag_suffix; + $test_result = check_markup($attribute_testcase, $format_name, NULL, FALSE, array(FILTER_TYPE_MARKUP_LANGUAGE)); + if (strcasecmp($test_result, $attribute_testcase) === 0) { + $allowed_tags[$tag_name][] = 'data-'; + } + } + } + + return $allowed_tags; +} + +/** + * Provides the test cases used by _filter_calculate_allowed_tags(). + * + * This set of tags does not include: + * - Form elements such as ,