Preparing new release.
[project/wysiwyg.git] / wysiwyg.module
index 46c588a..771cbd7 100644 (file)
 <?php
-// $Id$
 
 /**
  * @file
- * Integrate client-side editors with Drupal.
+ * Integrates client-side editors with Drupal.
  */
 
 /**
+ * Implements hook_entity_info().
+ */
+function wysiwyg_entity_info() {
+  $types['wysiwyg_profile'] = array(
+    'label' => t('Wysiwyg profile'),
+    'base table' => 'wysiwyg',
+    'controller class' => 'WysiwygProfileController',
+    'fieldable' => FALSE,
+    // When loading all entities, DrupalDefaultEntityController::load() ignores
+    // its static cache. Therefore, wysiwyg_profile_load_all() implements a
+    // custom static cache.
+    'static cache' => FALSE,
+    'entity keys' => array(
+      'id' => 'format',
+    ),
+  );
+  return $types;
+}
+
+/**
+ * Controller class for Wysiwyg profiles.
+ */
+class WysiwygProfileController extends DrupalDefaultEntityController {
+  /**
+   * Overrides DrupalDefaultEntityController::attachLoad().
+   */
+  function attachLoad(&$queried_entities, $revision_id = FALSE) {
+    // Unserialize the profile settings.
+    foreach ($queried_entities as $key => $record) {
+      $queried_entities[$key]->settings = unserialize($record->settings);
+    }
+    // Call the default attachLoad() method.
+    parent::attachLoad($queried_entities, $revision_id);
+  }
+}
+
+/**
  * Implementation of hook_menu().
  */
 function wysiwyg_menu() {
-  $items = array();
-  $items['admin/settings/wysiwyg/profile'] = array(
-    'title' => 'Wysiwyg',
-    'page callback' => 'wysiwyg_admin',
-    'description' => 'Configure client-side editor profiles.',
+  $items['admin/config/content/wysiwyg'] = array(
+    'title' => 'Wysiwyg profiles',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('wysiwyg_profile_overview'),
+    'description' => 'Configure client-side editors.',
+    'access arguments' => array('administer filters'),
+    'file' => 'wysiwyg.admin.inc',
+  );
+  $items['admin/config/content/wysiwyg/profile'] = array(
+    'title' => 'List',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+  );
+  $items['admin/config/content/wysiwyg/profile/%wysiwyg_profile/edit'] = array(
+    'title' => 'Edit',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('wysiwyg_profile_form', 5),
+    'access arguments' => array('administer filters'),
+    'file' => 'wysiwyg.admin.inc',
+    'tab_root' => 'admin/config/content/wysiwyg/profile',
+    'tab_parent' => 'admin/config/content/wysiwyg/profile/%wysiwyg_profile',
+    'type' => MENU_LOCAL_TASK,
+  );
+  $items['admin/config/content/wysiwyg/profile/%wysiwyg_profile/delete'] = array(
+    'title' => 'Remove',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('wysiwyg_profile_delete_confirm', 5),
     'access arguments' => array('administer filters'),
     'file' => 'wysiwyg.admin.inc',
+    'tab_root' => 'admin/config/content/wysiwyg/profile',
+    'tab_parent' => 'admin/config/content/wysiwyg/profile/%wysiwyg_profile',
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 10,
+  );
+  $items['wysiwyg/%'] = array(
+    'page callback' => 'wysiwyg_dialog',
+    'page arguments' => array(1),
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+    'file' => 'wysiwyg.dialog.inc',
   );
   return $items;
 }
 
 /**
  * Implementation of hook_theme().
+ *
+ * @see drupal_common_theme(), common.inc
+ * @see template_preprocess_page(), theme.inc
  */
 function wysiwyg_theme() {
   return array(
-    'wysiwyg_profile_overview' => array('arguments' => array('form' => NULL)),
-    'wysiwyg_admin_button_table' => array('arguments' => array('form' => NULL)),
+    'wysiwyg_profile_overview' => array(
+      'render element' => 'form',
+    ),
+    'wysiwyg_admin_button_table' => array(
+      'render element' => 'form',
+    ),
+    'wysiwyg_dialog_page' => array(
+      'variables' => array('content' => NULL, 'show_messages' => TRUE),
+      'file' => 'wysiwyg.dialog.inc',
+      'template' => 'wysiwyg-dialog-page',
+    ),
   );
 }
 
@@ -36,129 +116,137 @@ function wysiwyg_theme() {
  */
 function wysiwyg_help($path, $arg) {
   switch ($path) {
-    case 'admin/settings/wysiwyg/profile':
-      if (empty($arg[4])) {
-        $output = '<p>'. t('A Wysiwyg profile can be associated to an input format. A Wysiwyg profile defines, which client-side editor is loaded, can define what buttons or themes are enabled for the editor, how the editor is displayed, and a few other editor-specific functions.') .'</p>';
-        return $output;
-      }
-      break;
+    case 'admin/config/content/wysiwyg':
+      $output = '<p>' . t('A Wysiwyg profile is associated with an input format. A Wysiwyg profile defines which client-side editor is loaded with a particular input format, what buttons or themes are enabled for the editor, how the editor is displayed, and a few other editor-specific functions.') . '</p>';
+      return $output;
   }
 }
 
 /**
  * Implementation of hook_form_alter().
- *
- * Before Drupal 7, there is no way to easily identify form fields that are
- * input format enabled. As a workaround, we assign a form #after_build
- * processing callback that is executed on all forms after they have been
- * completely built, so form elements are in their effective order
- * and position already.
- *
- * @see wysiwyg_process_form()
  */
 function wysiwyg_form_alter(&$form, &$form_state) {
-  $form['#after_build'][] = 'wysiwyg_process_form';
-  // Disable 'teaser' textarea.
+  // Teaser splitter is unconditionally removed and NOT supported.
   if (isset($form['body_field'])) {
     unset($form['body_field']['teaser_js']);
-    $form['body_field']['teaser_include'] = array();
-  }
-}
-
-/**
- * Process a textarea for Wysiwyg Editor.
- *
- * This way, we can recurse into the form and search for certain, hard-coded
- * elements that have been added by filter_form(). If an input format selector
- * or input format guidelines element is found, we assume that the preceding
- * element is the corresponding textarea and use it's #id for attaching
- * client-side editors.
- *
- * @see wysiwyg_elements(), filter_form()
- */
-function wysiwyg_process_form(&$form) {
-  // Iterate over element children; resetting array keys to access last index.
-  if ($children = array_values(element_children($form))) {
-    foreach ($children as $index => $item) {
-      $element = &$form[$item];
-
-      // filter_form() always uses the key 'format'. We need a type-agnostic
-      // match to prevent false positives. Also, there must have been at least
-      // one element on this level.
-      if ($item === 'format' && $index > 0) {
-        // Make sure we either match a input format selector or input format
-        // guidelines (displayed if user has access to one input format only).
-        if ((isset($element['#type']) && $element['#type'] == 'fieldset') || isset($element['format']['guidelines'])) {
-          // The element before this element is the target form field.
-          $field = &$form[$children[$index - 1]];
-
-          // Disable #resizable to avoid resizable behavior to hi-jack the UI,
-          // but load the behavior, so the 'none' editor can attach/detach it.
-          $extra_class = '';
-          if (!empty($field['#resizable'])) {
-            // Due to our CSS class parsing, we can add arbitrary parameters
-            // for each input format.
-            $extra_class = ' wysiwyg-resizable-1';
-            $field['#resizable'] = FALSE;
-            drupal_add_js('misc/textarea.js');
-          }
+  }
+}
 
-          // Determine the available input formats. The last child element is a
-          // link to "More information about formatting options". When only one
-          // input format is displayed, we also have to remove formatting
-          // guidelines, stored in the child 'format'.
-          $formats = element_children($element);
-          array_pop($formats);
-          if (($key = array_search('format', $formats)) !== FALSE) {
-            unset($formats[$key]);
-          }
-          foreach ($formats as $format) {
-            // Default to 'none' editor (Drupal's default behaviors).
-            $editor = 'none';
-            $status = 1;
-            // Fetch the profile associated to this input format.
-            $profile = wysiwyg_get_profile($format);
-            if ($profile) {
-              $editor = $profile->editor;
-              $status = (int)wysiwyg_user_get_status($profile);
-              // Check editor theme (and reset it if not/no longer available).
-              $theme = wysiwyg_get_editor_themes($profile, (isset($profile->settings['theme']) ? $profile->settings['theme'] : ''));
-
-              // Add profile settings for this input format.
-              wysiwyg_add_editor_settings($profile, $theme);
-              // Add plugin settings for this input format.
-              wysiwyg_add_plugin_settings($profile);
-            }
-
-            // Use a prefix/suffix for a single input format, or attach to input
-            // format selector radio buttons.
-            if (isset($element['format']['guidelines'])) {
-              $element['format']['guidelines']['#prefix'] = '<div class="wysiwyg wysiwyg-format-'. $format .' wysiwyg-editor-'. $editor .' wysiwyg-field-'. $field['#id'] .' wysiwyg-status-'. $status . $extra_class .'">';
-              $element['format']['guidelines']['#suffix'] = '</div>';
-              // Edge-case: Default format contains no input filters.
-              if (empty($element['format']['guidelines']['#value'])) {
-                $element['format']['guidelines']['#value'] = ' ';
-              }
-            }
-            else {
-              if (isset($element[$format]['#attributes']['class'])) {
-                $element[$format]['#attributes']['class'] .= ' ';
-              }
-              else {
-                $element[$format]['#attributes']['class'] = '';
-              }
-              $element[$format]['#attributes']['class'] .= 'wysiwyg wysiwyg-format-'. $format .' wysiwyg-editor-'. $editor .' wysiwyg-field-'. $field['#id'] .' wysiwyg-status-'. $status . $extra_class;
-            }
-          }
-        }
-        // If this element is 'format', do not recurse further.
-        continue;
+/**
+ * Implements hook_element_info_alter().
+ */
+function wysiwyg_element_info_alter(&$types) {
+  $types['text_format']['#pre_render'][] = 'wysiwyg_pre_render_text_format';
+}
+
+/**
+ * Process a text format widget to load and attach editors.
+ *
+ * The element's #id is used as reference to attach client-side editors.
+ */
+function wysiwyg_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'];
+  $settings = array(
+    'field' => $field['#id'],
+  );
+
+  // If this textarea is #resizable and we will load at least one
+  // editor, then only load the behavior and let the 'none' editor
+  // attach/detach it to avoid hi-jacking the UI. Due to our CSS class
+  // parsing, we can add arbitrary parameters for each input format.
+  // The #resizable property will be removed below, if at least one
+  // profile has been loaded.
+  $resizable = 0;
+  if (!empty($field['#resizable'])) {
+    $resizable = 1;
+    drupal_add_js('misc/textarea.js');
+  }
+  // Determine the available text formats.
+  foreach ($format_field['format']['#options'] as $format_id => $format_name) {
+    $format = 'format' . $format_id;
+    // Initialize default settings, defaulting to 'none' editor.
+    $settings[$format] = array(
+      'editor' => 'none',
+      'status' => 1,
+      'toggle' => 1,
+      'resizable' => $resizable,
+    );
+
+    // Fetch the profile associated to this text format.
+    $profile = wysiwyg_get_profile($format_id);
+    if ($profile) {
+      $loaded = TRUE;
+      $settings[$format]['editor'] = $profile->editor;
+      $settings[$format]['status'] = (int) wysiwyg_user_get_status($profile);
+      if (isset($profile->settings['show_toggle'])) {
+        $settings[$format]['toggle'] = (int) $profile->settings['show_toggle'];
       }
-      // Recurse into children.
-      wysiwyg_process_form($element);
+      // Check editor theme (and reset it if not/no longer available).
+      $theme = wysiwyg_get_editor_themes($profile, (isset($profile->settings['theme']) ? $profile->settings['theme'] : ''));
+
+      // Add plugin settings (first) for this text format.
+      wysiwyg_add_plugin_settings($profile);
+      // Add profile settings for this text format.
+      wysiwyg_add_editor_settings($profile, $theme);
     }
   }
-  return $form;
+  // Use a hidden element for a single text format.
+  if (!$format_field['format']['#access']) {
+    $format_field['wysiwyg'] = array(
+      '#type' => 'hidden',
+      '#name' => $format_field['format']['#name'],
+      '#value' => $format_id,
+      '#attributes' => array(
+        'id' => $format_field['format']['#id'],
+        'class' => array('wysiwyg'),
+      ),
+    );
+    $format_field['wysiwyg']['#attached']['js'][] = array(
+      'data' => array(
+        'wysiwyg' => array(
+          'triggers' => array(
+            $format_field['format']['#id'] => $settings,
+          ),
+        ),
+      ),
+      'type' => 'setting',
+    );
+  }
+  // Otherwise, attach to text format selector.
+  else {
+    $format_field['format']['#attributes']['class'][] = 'wysiwyg';
+    $format_field['format']['#attached']['js'][] = array(
+      'data' => array(
+        'wysiwyg' => array(
+          'triggers' => array(
+            $format_field['format']['#id'] => $settings,
+          ),
+        ),
+      ),
+      'type' => 'setting',
+    );
+  }
+
+  // If we loaded at least one editor, then the 'none' editor will
+  // handle resizable textareas instead of core.
+  if (isset($loaded) && $resizable) {
+    $field['#resizable'] = FALSE;
+  }
+
+  return $element;
 }
 
 /**
@@ -176,7 +264,7 @@ function wysiwyg_process_form(&$form) {
  * @see wysiwyg_load_editor(), wysiwyg_get_editor()
  */
 function wysiwyg_get_profile($format) {
-  if ($profile = wysiwyg_load_profile($format)) {
+  if ($profile = wysiwyg_profile_load($format)) {
     if (wysiwyg_load_editor($profile)) {
       return $profile;
     }
@@ -209,16 +297,27 @@ function wysiwyg_load_editor($profile) {
       // @todo Allow to configure the library/execMode to use.
       if (isset($profile->settings['library']) && isset($editor['libraries'][$profile->settings['library']])) {
         $library = $profile->settings['library'];
-        $files = $editor['libraries'][$profile->settings['library']]['files'];
+        $files = $editor['libraries'][$library]['files'];
       }
       else {
-        // Fallback to the first by default (external libraries can change).
+        // Fallback to the first defined library by default (external libraries can change).
         $library = key($editor['libraries']);
         $files = array_shift($editor['libraries']);
         $files = $files['files'];
       }
-      foreach ($files as $file) {
-        drupal_add_js($editor['library path'] . '/' . $file);
+      foreach ($files as $file => $options) {
+        if (is_array($options)) {
+          $options += array('type' => 'file', 'scope' => 'header', 'defer' => FALSE, 'cache' => TRUE, 'preprocess' => TRUE);
+          drupal_add_js($editor['library path'] . '/' . $file, $options);
+        }
+        else {
+          drupal_add_js($editor['library path'] . '/' . $options);
+        }
+      }
+      // If editor defines an additional load callback, invoke it.
+      // @todo Isn't the settings callback sufficient?
+      if (isset($editor['load callback']) && function_exists($editor['load callback'])) {
+        $editor['load callback']($editor, $library);
       }
       // Load JavaScript integration files for this editor.
       $files = array();
@@ -234,18 +333,17 @@ function wysiwyg_load_editor($profile) {
         $files = $editor['css files'];
       }
       foreach ($files as $file) {
-        drupal_add_css($editor['css path'] . '/' . $file, 'module', 'screen');
+        drupal_add_css($editor['css path'] . '/' . $file);
       }
 
       drupal_add_js(array('wysiwyg' => array(
-        'configs' => array($editor['name'] => array()),
-        // @todo Move into profile settings.
-        'showToggle' => isset($profile->settings['show_toggle']) ? $profile->settings['show_toggle'] : TRUE,
-        // @todo Move into (global) editor settings.
-        // If JS compression is enabled, at least TinyMCE is unable to determine
-        // its own base path and exec mode since it can't find the script name.
-        'editorBasePath' => base_path() . $editor['library path'],
-        'execMode' => $library,
+        'configs' => array($editor['name'] => array('global' => array(
+          // @todo Move into (global) editor settings.
+          // If JS compression is enabled, at least TinyMCE is unable to determine
+          // its own base path and exec mode since it can't find the script name.
+          'editorBasePath' => base_path() . $editor['library path'],
+          'execMode' => $library,
+        ))),
       )), 'setting');
 
       $loaded[$name] = TRUE;
@@ -259,17 +357,19 @@ function wysiwyg_load_editor($profile) {
   if (!isset($settings_added) && $loaded[$name]) {
     drupal_add_js(array('wysiwyg' => array(
       'configs' => array(),
+      'plugins' => array(),
       'disable' => t('Disable rich-text'),
       'enable' => t('Enable rich-text'),
     )), 'setting');
 
+    $path = drupal_get_path('module', 'wysiwyg');
     // Initialize our namespaces in the *header* to do not force editor
     // integration scripts to check and define Drupal.wysiwyg on its own.
-    drupal_add_js(wysiwyg_get_path('wysiwyg.init.js'), 'core');
+    drupal_add_js($path . '/wysiwyg.init.js', array('group' => JS_LIBRARY));
 
     // The 'none' editor is a special editor implementation, allowing us to
     // attach and detach regular Drupal behaviors just like any other editor.
-    drupal_add_js(wysiwyg_get_path('editors/js/none.js'));
+    drupal_add_js($path . '/editors/js/none.js');
 
     // Add wysiwyg.js to the footer to ensure it's executed after the
     // Drupal.settings array has been rendered and populated. Also, since editor
@@ -277,7 +377,7 @@ function wysiwyg_load_editor($profile) {
     // and Drupal.wysiwygInit() must be executed AFTER editors registered
     // their callbacks and BEFORE Drupal.behaviors are applied, this must come
     // last.
-    drupal_add_js(wysiwyg_get_path('wysiwyg.js'), 'module', 'footer');
+    drupal_add_js($path . '/wysiwyg.js', array('scope' => 'footer'));
 
     $settings_added = TRUE;
   }
@@ -295,39 +395,117 @@ function wysiwyg_add_editor_settings($profile, $theme) {
     $config = wysiwyg_get_editor_config($profile, $theme);
     // drupal_to_js() does not properly convert numeric array keys, so we need
     // to use a string instead of the format id.
-    drupal_add_js(array('wysiwyg' => array('configs' => array($profile->editor => array('format'. $profile->format => $config)))), 'setting');
+    if ($config) {
+      drupal_add_js(array('wysiwyg' => array('configs' => array($profile->editor => array('format' . $profile->format => $config)))), 'setting');
+    }
     $formats[$profile->format] = TRUE;
   }
 }
 
 /**
  * Add settings for external plugins.
- * 
+ *
+ * Plugins can be used in multiple profiles, but not necessarily in all. Because
+ * of that, we need to process plugins for each profile, even if most of their
+ * settings are not stored per profile.
+ *
+ * Implementations of hook_wysiwyg_plugin() may execute different code for each
+ * editor. Therefore, we have to invoke those implementations for each editor,
+ * but process the resulting plugins separately for each profile.
+ *
+ * Drupal plugins differ to native plugins in that they have plugin-specific
+ * definitions and settings, which need to be processed only once. But they are
+ * also passed to the editor to prepare settings specific to the editor.
+ * Therefore, we load and process the Drupal plugins only once, and hand off the
+ * effective definitions for each profile to the editor.
+ *
  * @param $profile
  *   A wysiwyg editor profile.
+ *
+ * @todo Rewrite wysiwyg_process_form() to build a registry of effective
+ *   profiles in use, so we can process plugins in multiple profiles in one shot
+ *   and simplify this entire function.
  */
 function wysiwyg_add_plugin_settings($profile) {
-  static $plugins_added = array();
-  
-  if (!isset($plugins_added[$profile->editor])) {
-    $plugins = array();
-    $editor = wysiwyg_get_editor($profile->editor);
-    // Collect editor plugins provided via hook_wysiwyg_plugin().
-    $info = module_invoke_all('wysiwyg_plugin', $editor['name'], $editor['installed version']);
-    // Only keep enabled plugins in this profile.
-    foreach ($info as $plugin => $meta) {
-      if (!isset($profile->settings['buttons'][$plugin])) {
-        unset($info[$plugin]);
+  static $plugins = array();
+  static $processed_plugins = array();
+  static $processed_formats = array();
+
+  // Each input format must only processed once.
+  // @todo ...as long as we do not have multiple profiles per format.
+  if (isset($processed_formats[$profile->format])) {
+    return;
+  }
+  $processed_formats[$profile->format] = TRUE;
+
+  $editor = wysiwyg_get_editor($profile->editor);
+
+  // Collect native plugins for this editor provided via hook_wysiwyg_plugin()
+  // and Drupal plugins provided via hook_wysiwyg_include_directory().
+  if (!array_key_exists($editor['name'], $plugins)) {
+    $plugins[$editor['name']] = wysiwyg_get_plugins($editor['name']);
+  }
+
+  // Nothing to do, if there are no plugins.
+  if (empty($plugins[$editor['name']])) {
+    return;
+  }
+
+  // Determine name of proxy plugin for Drupal plugins.
+  $proxy = (isset($editor['proxy plugin']) ? key($editor['proxy plugin']) : '');
+
+  // Process native editor plugins.
+  if (isset($editor['plugin settings callback'])) {
+    // @todo Require PHP 5.1 in 3.x and use array_intersect_key().
+    $profile_plugins_native = array();
+    foreach ($plugins[$editor['name']] as $plugin => $meta) {
+      // Skip Drupal plugins (handled below).
+      if ($plugin === $proxy) {
+        continue;
+      }
+      // Only keep native plugins that are enabled in this profile.
+      if (isset($profile->settings['buttons'][$plugin])) {
+        $profile_plugins_native[$plugin] = $meta;
       }
     }
+    // Invoke the editor's plugin settings callback, so it can populate the
+    // settings for native external plugins with required values.
+    $settings_native = call_user_func($editor['plugin settings callback'], $editor, $profile, $profile_plugins_native);
 
-    if (isset($editor['plugin settings callback']) && function_exists($editor['plugin settings callback'])) {
-      $plugins = $editor['plugin settings callback']($editor, $profile, $info);
+    if ($settings_native) {
+      drupal_add_js(array('wysiwyg' => array('plugins' => array('format' . $profile->format => array('native' => $settings_native)))), 'setting');
     }
+  }
 
-    drupal_add_js(array('wysiwyg' => array('plugins' => array($profile->editor => $plugins))), 'setting');
+  // Process Drupal plugins.
+  if ($proxy && isset($editor['proxy plugin settings callback'])) {
+    $profile_plugins_drupal = array();
+    foreach (wysiwyg_get_all_plugins() as $plugin => $meta) {
+      if (isset($profile->settings['buttons'][$proxy][$plugin])) {
+        // JavaScript and plugin-specific settings for Drupal plugins must be
+        // loaded and processed only once. Plugin information is cached
+        // statically to pass it to the editor's proxy plugin settings callback.
+        if (!isset($processed_plugins[$proxy][$plugin])) {
+          $profile_plugins_drupal[$plugin] = $processed_plugins[$proxy][$plugin] = $meta;
+          // Load the Drupal plugin's JavaScript.
+          drupal_add_js($meta['js path'] . '/' . $meta['js file']);
+          // Add plugin-specific settings.
+          if (isset($meta['settings'])) {
+            drupal_add_js(array('wysiwyg' => array('plugins' => array('drupal' => array($plugin => $meta['settings'])))), 'setting');
+          }
+        }
+        else {
+          $profile_plugins_drupal[$plugin] = $processed_plugins[$proxy][$plugin];
+        }
+      }
+    }
+    // Invoke the editor's proxy plugin settings callback, so it can populate
+    // the settings for Drupal plugins with custom, required values.
+    $settings_drupal = call_user_func($editor['proxy plugin settings callback'], $editor, $profile, $profile_plugins_drupal);
 
-    $plugins_added[$profile->editor] = TRUE;
+    if ($settings_drupal) {
+      drupal_add_js(array('wysiwyg' => array('plugins' => array('format' . $profile->format => array('drupal' => $settings_drupal)))), 'setting');
+    }
   }
 }
 
@@ -362,7 +540,7 @@ function wysiwyg_get_editor_themes(&$profile, $selected_theme = NULL) {
   // Check optional $selected_theme argument, if given.
   if (isset($selected_theme)) {
     // If the passed theme name does not exist, use the first available.
-    if (!isset($themes[$profile->editor][$selected_theme])) {
+    if (!in_array($selected_theme, $themes[$profile->editor])) {
       $selected_theme = $profile->settings['theme'] = $themes[$profile->editor][0];
     }
   }
@@ -387,11 +565,17 @@ function wysiwyg_get_plugins($editor_name) {
     if (isset($editor['plugin callback']) && function_exists($editor['plugin callback'])) {
       $plugins = $editor['plugin callback']($editor);
     }
-    // Load our own plugins.
-    include_once drupal_get_path('module', 'wysiwyg') .'/wysiwyg.plugins.inc';
-  
     // Add editor plugins provided via hook_wysiwyg_plugin().
     $plugins = array_merge($plugins, module_invoke_all('wysiwyg_plugin', $editor['name'], $editor['installed version']));
+    // Add API plugins provided by Drupal modules.
+    // @todo We need to pass the filepath to the plugin icon for Drupal plugins.
+    if (isset($editor['proxy plugin'])) {
+      $plugins += $editor['proxy plugin'];
+      $proxy = key($editor['proxy plugin']);
+      foreach (wysiwyg_get_all_plugins() as $plugin_name => $info) {
+        $plugins[$proxy]['buttons'][$plugin_name] = $info['title'];
+      }
+    }
   }
   return $plugins;
 }
@@ -404,69 +588,180 @@ function wysiwyg_get_editor_config($profile, $theme) {
   $settings = array();
   if (!empty($editor['settings callback']) && function_exists($editor['settings callback'])) {
     $settings = $editor['settings callback']($editor, $profile->settings, $theme);
+
+    // Allow other modules to alter the editor settings for this format.
+    $context = array('editor' => $editor, 'profile' => $profile, 'theme' => $theme);
+    drupal_alter('wysiwyg_editor_settings', $settings, $context);
   }
   return $settings;
 }
 
 /**
- * Load all profiles. Just load one profile if $name is passed in.
+ * Retrieve stylesheets for HTML/IFRAME-based editors.
+ *
+ * This assumes that the content editing area only needs stylesheets defined
+ * for the scope 'theme'.
+ *
+ * @return
+ *   An array containing CSS files, including proper base path.
+ */
+function wysiwyg_get_css() {
+  static $files;
+
+  if (isset($files)) {
+    return $files;
+  }
+  // In node form previews, the theme has not been initialized yet.
+  if (!empty($_POST)) {
+    drupal_theme_initialize();
+  }
+
+  $files = array();
+  foreach (drupal_add_css() as $filepath => $info) {
+    if ($info['group'] >= CSS_THEME && $info['media'] != 'print') {
+      if (file_exists($filepath)) {
+        $files[] = base_path() . $filepath;
+      }
+    }
+  }
+  return $files;
+}
+
+/**
+ * Loads a profile for a given text format.
+ *
+ * Since there are commonly not many text formats, and each text format-enabled
+ * form element will possibly have to load every single profile, all existing
+ * profiles are loaded and cached once to reduce the amount of database queries.
+ */
+function wysiwyg_profile_load($format) {
+  $profiles = wysiwyg_profile_load_all();
+  return (isset($profiles[$format]) ? $profiles[$format] : FALSE);
+}
+
+/**
+ * Loads all profiles.
  */
-function wysiwyg_load_profile($format = '') {
-  static $profiles;
+function wysiwyg_profile_load_all() {
+  // entity_load(..., FALSE) does not re-use its own static cache upon
+  // repetitive calls, so a custom static cache is required.
+  // @see wysiwyg_entity_info()
+  $profiles = &drupal_static(__FUNCTION__);
 
   if (!isset($profiles)) {
-    $profiles = array();
-    $result = db_query('SELECT * FROM {wysiwyg}');
-    while ($profile = db_fetch_object($result)) {
-      $profile->settings = unserialize($profile->settings);
-      $profiles[$profile->format] = $profile;
+    // Additional database cache to support alternative caches like memcache.
+    if ($cached = cache_get('wysiwyg_profiles')) {
+      $profiles = $cached->data;
+    }
+    else {
+      $profiles = entity_load('wysiwyg_profile', FALSE);
+      cache_set('wysiwyg_profiles', $profiles);
     }
   }
 
-  return ($format && isset($profiles[$format]) ? $profiles[$format] : ($format ? FALSE : $profiles));
-}
-
-/**
- * Implementation of hook_user().
- */
-function wysiwyg_user($type, &$edit, &$user, $category = NULL) {
-  if ($type == 'form' && $category == 'account') {
-    // @todo http://drupal.org/node/322433
-    $profile = new stdClass;
-    if (isset($profile->settings['user_choose']) && $profile->settings['user_choose']) {
-      $form['wysiwyg'] = array(
-        '#type' => 'fieldset',
-        '#title' => t('Wysiwyg Editor settings'),
-        '#weight' => 10,
-        '#collapsible' => TRUE,
-        '#collapsed' => TRUE,
-      );
-      $form['wysiwyg']['wysiwyg_status'] = array(
-        '#type' => 'checkbox',
-        '#title' => t('Enable editor by default'),
-        '#default_value' => isset($user->wysiwyg_status) ? $user->wysiwyg_status : (isset($profile->settings['default']) ? $profile->settings['default'] : FALSE),
-        '#return_value' => 1,
-        '#description' => t('If enabled, rich-text editing is enabled by default in textarea fields.'),
-      );
-      return array('wysiwyg' => $form);
+  return $profiles;
+}
+
+/**
+ * Deletes a profile from the database.
+ */
+function wysiwyg_profile_delete($format) {
+  db_delete('wysiwyg')
+    ->condition('format', $format)
+    ->execute();
+}
+
+/**
+ * Clear all Wysiwyg profile caches.
+ */
+function wysiwyg_profile_cache_clear() {
+  entity_get_controller('wysiwyg_profile')->resetCache();
+  drupal_static_reset('wysiwyg_profile_load_all');
+  cache_clear_all('wysiwyg_profiles', 'cache');
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function wysiwyg_form_user_profile_form_alter(&$form, &$form_state, $form_id) {
+  $account = $form['#user'];
+  $user_formats = filter_formats($account);
+  $options = array();
+  $options_default = array();
+  foreach (wysiwyg_profile_load_all() as $format => $profile) {
+    // Only show profiles that have user_choose enabled.
+    if (!empty($profile->settings['user_choose']) && isset($user_formats[$format])) {
+      $options[$format] = check_plain($user_formats[$format]->name);
+      if (wysiwyg_user_get_status($profile, $account)) {
+        $options_default[] = $format;
+      }
     }
   }
-  elseif ($type == 'validate' && isset($edit['wysiwyg_status'])) {
-    return array('wysiwyg_status' => $edit['wysiwyg_status']);
+  if (!empty($options)) {
+    $form['wysiwyg']['wysiwyg_status'] = array(
+      '#type' => 'checkboxes',
+      '#title' => t('Text formats enabled for rich-text editing'),
+      '#options' => $options,
+      '#default_value' => $options_default,
+    );
   }
 }
 
-function wysiwyg_user_get_status($profile) {
+/**
+ * Implements hook_user_insert().
+ *
+ * Wysiwyg's user preferences are normally not exposed on the user registration
+ * form, but in case they are manually altered in, we invoke
+ * wysiwyg_user_update() accordingly.
+ */
+function wysiwyg_user_insert(&$edit, $account, $category) {
+  wysiwyg_user_update($edit, $account, $category);
+}
+
+/**
+ * Implements hook_user_update().
+ */
+function wysiwyg_user_update(&$edit, $account, $category) {
+  if (isset($edit['wysiwyg_status'])) {
+    db_delete('wysiwyg_user')
+      ->condition('uid', $account->uid)
+      ->execute();
+    $query = db_insert('wysiwyg_user')
+      ->fields(array('uid', 'format', 'status'));
+    foreach ($edit['wysiwyg_status'] as $format => $status) {
+      $query->values(array(
+        'uid' => $account->uid,
+        'format' => $format,
+        'status' => (int) (bool) $status,
+      ));
+    }
+    $query->execute();
+  }
+}
+
+function wysiwyg_user_get_status($profile, $account = NULL) {
   global $user;
 
-  if ($profile->settings['user_choose'] && isset($user->wysiwyg_status)) {
-    $status = $user->wysiwyg_status;
+  if (!isset($account)) {
+    $account = $user;
+  }
+
+  // Default wysiwyg editor status information is only required on forms, so we
+  // do not pre-emptively load and attach this information on every user_load().
+  if (!isset($account->wysiwyg_status)) {
+    $account->wysiwyg_status = db_query("SELECT format, status FROM {wysiwyg_user} WHERE uid = :uid", array(
+      ':uid' => $account->uid,
+    ))->fetchAllKeyed();
+  }
+
+  if (!empty($profile->settings['user_choose']) && isset($account->wysiwyg_status[$profile->format])) {
+    $status = $account->wysiwyg_status[$profile->format];
   }
   else {
     $status = isset($profile->settings['default']) ? $profile->settings['default'] : TRUE;
   }
 
-  return $status;
+  return (bool) $status;
 }
 
 /**
@@ -509,8 +804,8 @@ function wysiwyg_get_all_editors() {
       'title' => '',
       'vendor url' => '',
       'download url' => '',
-      'editor path' => wysiwyg_get_path($properties['name']),
-      'library path' => wysiwyg_get_path($properties['name']),
+      'editor path' => wysiwyg_get_path($editors[$editor]['name']),
+      'library path' => wysiwyg_get_path($editors[$editor]['name']),
       'libraries' => array(),
       'version callback' => NULL,
       'themes callback' => NULL,
@@ -518,16 +813,16 @@ function wysiwyg_get_all_editors() {
       'plugin callback' => NULL,
       'plugin settings callback' => NULL,
       'versions' => array(),
-      'js path' => $properties['path'] .'/js',
-      'css path' => $properties['path'] .'/css',
+      'js path' => $editors[$editor]['path'] . '/js',
+      'css path' => $editors[$editor]['path'] . '/css',
     );
     // Check whether library is present.
-    if (!($editors[$editor]['installed'] = file_exists($properties['library path']))) {
+    if (!($editors[$editor]['installed'] = file_exists($editors[$editor]['library path']))) {
       continue;
     }
     // Detect library version.
     if (function_exists($editors[$editor]['version callback'])) {
-      $editors[$editor]['installed version'] = $editors[$editor]['version callback']($properties);
+      $editors[$editor]['installed version'] = $editors[$editor]['version callback']($editors[$editor]);
     }
     if (empty($editors[$editor]['installed version'])) {
       $editors[$editor]['error'] = t('The version of %editor could not be detected.', array('%editor' => $properties['title']));
@@ -543,19 +838,58 @@ function wysiwyg_get_all_editors() {
       }
     }
     if (!$version) {
-      $editors[$editor]['error'] = t('The installed version %version of %editor is not supported.', array('%version' => $editors[$editor]['installed version'], '%editor' => $properties['title']));
+      $editors[$editor]['error'] = t('The installed version %version of %editor is not supported.', array('%version' => $editors[$editor]['installed version'], '%editor' => $editors[$editor]['title']));
       $editors[$editor]['installed'] = FALSE;
       continue;
     }
     // Apply library version specific definitions and overrides.
     $editors[$editor] = array_merge($editors[$editor], $editors[$editor]['versions'][$version]);
     unset($editors[$editor]['versions']);
-    $editors[$editor]['title'] = $editors[$editor]['title'] . ' ' . $editors[$editor]['installed version'];
   }
   return $editors;
 }
 
 /**
+ * Invoke hook_wysiwyg_plugin() in all modules.
+ */
+function wysiwyg_get_all_plugins() {
+  static $plugins;
+
+  if (isset($plugins)) {
+    return $plugins;
+  }
+
+  $plugins = wysiwyg_load_includes('plugins', 'plugin');
+  foreach ($plugins as $name => $properties) {
+    $plugin = &$plugins[$name];
+    // Fill in required/default properties.
+    $plugin += array(
+      'title' => $plugin['name'],
+      'vendor url' => '',
+      'js path' => $plugin['path'] . '/' . $plugin['name'],
+      'js file' => $plugin['name'] . '.js',
+      'css path' => $plugin['path'] . '/' . $plugin['name'],
+      'css file' => $plugin['name'] . '.css',
+      'icon path' => $plugin['path'] . '/' . $plugin['name'] . '/images',
+      'icon file' => $plugin['name'] . '.png',
+      'dialog path' => $plugin['name'],
+      'dialog settings' => array(),
+      'settings callback' => NULL,
+      'settings form callback' => NULL,
+    );
+    // Fill in default settings.
+    $plugin['settings'] += array(
+      'path' => base_path() . $plugin['path'] . '/' . $plugin['name'],
+    );
+    // Check whether library is present.
+    if (!($plugin['installed'] = file_exists($plugin['js path'] . '/' . $plugin['js file']))) {
+      continue;
+    }
+  }
+  return $plugins;
+}
+
+/**
  * Load include files for wysiwyg implemented by all modules.
  *
  * @param $type
@@ -570,18 +904,18 @@ function wysiwyg_get_all_editors() {
 function wysiwyg_load_includes($type = 'editors', $hook = 'editor', $file = NULL) {
   // Determine implementations.
   $directories = wysiwyg_get_directories($type);
-  $directories['wysiwyg_'] = wysiwyg_get_path($type);
+  $directories['wysiwyg'] = drupal_get_path('module', 'wysiwyg') . '/' . $type;
   $file_list = array();
   foreach ($directories as $module => $path) {
-    $file_list[$module] = drupal_system_listing("$file" . '.inc$', $path, 'name', 0);
+    $file_list[$module] = drupal_system_listing("/{$file}.inc\$/", $path, 'name', 0);
   }
 
   // Load implementations.
   $info = array();
   foreach (array_filter($file_list) as $module => $files) {
     foreach ($files as $file) {
-      include_once './' . $file->filename;
-      $result = _wysiwyg_process_include('wysiwyg', $module . $file->name, dirname($file->filename), $hook);
+      include_once './' . $file->uri;
+      $result = _wysiwyg_process_include($module, $module . '_' . $file->name, dirname($file->uri), $hook);
       if (is_array($result)) {
         $info = array_merge($info, $result);
       }
@@ -591,21 +925,101 @@ function wysiwyg_load_includes($type = 'editors', $hook = 'editor', $file = NULL
 }
 
 /**
- * Helper function to build module/file paths.
+ * Helper function to build paths to libraries.
  *
- * @param $file
- *   A file or directory in a module to return.
+ * @param $library
+ *   The external library name to return the path for.
  * @param $base_path
  *   Whether to prefix the resulting path with base_path().
- * @param $module
- *   The module name to use as prefix.
  *
  * @return
- *   The path to the specified file in a module.
+ *   The path to the specified library.
+ *
+ * @ingroup libraries
  */
-function wysiwyg_get_path($file = '', $base_path = FALSE, $module = 'wysiwyg') {
-  $base_path = ($base_path ? base_path() : '');
-  return $base_path . drupal_get_path('module', $module) . '/' . $file;
+function wysiwyg_get_path($library, $base_path = FALSE) {
+  static $libraries;
+
+  if (!isset($libraries)) {
+    $libraries = wysiwyg_get_libraries();
+  }
+  if (!isset($libraries[$library])) {
+    // Most often, external libraries can be shared across multiple sites.
+    return 'sites/all/libraries/' . $library;
+  }
+
+  $path = ($base_path ? base_path() : '');
+  $path .= $libraries[$library];
+
+  return $path;
+}
+
+/**
+ * Return an array of library directories.
+ *
+ * Returns an array of library directories from the all-sites directory
+ * (i.e. sites/all/libraries/), the profiles directory, and site-specific
+ * directory (i.e. sites/somesite/libraries/). The returned array will be keyed
+ * by the library name. Site-specific libraries are prioritized over libraries
+ * in the default directories. That is, if a library with the same name appears
+ * in both the site-wide directory and site-specific directory, only the
+ * site-specific version will be listed.
+ *
+ * @return
+ *   A list of library directories.
+ *
+ * @ingroup libraries
+ */
+function wysiwyg_get_libraries() {
+  global $profile;
+
+  // When this function is called during Drupal's initial installation process,
+  // the name of the profile that is about to be installed is stored in the
+  // global $profile variable. At all other times, the regular system variable
+  // contains the name of the current profile, and we can call variable_get()
+  // to determine the profile.
+  if (!isset($profile)) {
+    $profile = variable_get('install_profile', 'default');
+  }
+
+  $directory = 'libraries';
+  $searchdir = array();
+  $config = conf_path();
+
+  // The 'profiles' directory contains pristine collections of modules and
+  // themes as organized by a distribution.  It is pristine in the same way
+  // that /modules is pristine for core; users should avoid changing anything
+  // there in favor of sites/all or sites/<domain> directories.
+  if (file_exists("profiles/$profile/$directory")) {
+    $searchdir[] = "profiles/$profile/$directory";
+  }
+
+  // Always search sites/all/*.
+  $searchdir[] = 'sites/all/' . $directory;
+
+  // Also search sites/<domain>/*.
+  if (file_exists("$config/$directory")) {
+    $searchdir[] = "$config/$directory";
+  }
+
+  // Retrieve list of directories.
+  // @todo Core: Allow to scan for directories.
+  $directories = array();
+  $nomask = array('CVS');
+  foreach ($searchdir as $dir) {
+    if (is_dir($dir) && $handle = opendir($dir)) {
+      while (FALSE !== ($file = readdir($handle))) {
+        if (!in_array($file, $nomask) && $file[0] != '.') {
+          if (is_dir("$dir/$file")) {
+            $directories[$file] = "$dir/$file";
+          }
+        }
+      }
+      closedir($handle);
+    }
+  }
+
+  return $directories;
 }
 
 /**
@@ -625,7 +1039,7 @@ function wysiwyg_get_directories($plugintype) {
   foreach (module_implements('wysiwyg_include_directory') as $module) {
     $result = module_invoke($module, 'wysiwyg_include_directory', $plugintype);
     if (isset($result) && is_string($result)) {
-      $directories[$module .'_'] = drupal_get_path('module', $module) .'/'. $result;
+      $directories[$module] = drupal_get_path('module', $module) . '/' . $result;
     }
   }
   return $directories;
@@ -663,4 +1077,3 @@ function _wysiwyg_process_include($module, $identifier, $path, $hook) {
 /**
  * @} End of "defgroup wysiwyg_api".
  */
-