, // Richard Bennett and Jeff Robbins /** * @file * Integrate the TinyMCE editor (http://tinymce.moxiecode.com/) into Drupal. */ /** * Implementation of hook_menu(). */ function tinymce_menu() { $items['admin/settings/tinymce'] = array( 'title' => 'TinyMCE', 'description' => 'Configure the rich editor.', 'page callback' => 'tinymce_admin', 'access arguments' => array('administer tinymce'), 'file' => 'tinymce.admin.inc', ); return $items; } /** * Implementation of hook_theme() */ function tinymce_theme() { return array( // This button table is used in TinyMCE admininstration. 'tinymce_admin_button_table' => array('arguments' => array('form')), 'tinymce_theme' => array('arguments' => array('init', 'textarea_name', 'theme_name', 'is_running')) ); } /** * Implementation of hook_help(). */ function tinymce_help($path, $arg) { switch ($path) { case 'admin/settings/tinymce#pages': return "node/*\nuser/*\ncomment/*"; case 'admin/settings/tinymce': case 'admin/help#tinymce' : return t('

$Revision: 1.90.4.23 $ $Date: 2007/05/06 01:41:35 $

'. '

TinyMCE adds what-you-see-is-what-you-get (WYSIWYG) html editing to textareas. This editor can be enabled/disabled without reloading the page by clicking a link below each textarea.

Profiles can be defined based on user roles. A TinyMCE profile can define which pages receive this TinyMCE capability, what buttons or themes are enabled for the editor, how the editor is displayed, and a few other editor functions.

Lastly, only users with the access tinymce permission will be able to use TinyMCE.

', array('!url' => url('admin/user/permissions')) ); } } /** * Implementation of hook_perm(). */ function tinymce_perm() { $array = array('administer tinymce', 'access tinymce'); $tinymce_mod_path = drupal_get_path('module', 'tinymce'); if (is_dir($tinymce_mod_path .'/tinymce/jscripts/tiny_mce/plugins/imagemanager/')) { $array[] = 'access tinymce imagemanager'; } if (is_dir($tinymce_mod_path .'/tinymce/jscripts/tiny_mce/plugins/filemanager/')) { $array[] = 'access tinymce filemanager'; } return $array; } /** * Implementation of hook_elements(). */ function tinymce_elements() { $type = array(); if (user_access('access tinymce')) { // Let TinyMCE potentially process each textarea. $type['textarea'] = array('#process' => array('tinymce_process_textarea')); } return $type; } /** * Implementation of hook_form_alter() */ function tinymce_form_alter(&$form, &$form_state) { // disable 'teaser' textarea unset($form['body_field']['teaser_js']); $form['body_field']['teaser_include'] = array(); } /** * Attach tinymce to a textarea */ function tinymce_process_textarea($element) { static $is_running = FALSE; global $user; static $profile_name; //$element is an array of attributes for the textarea but there is no just 'name' value, so we extract this from the #id field $textarea_name = substr($element['#id'], strpos($element['#id'], '-') + 1); // Since tinymce_config() makes a db hit, only call it when we're pretty sure // we're gonna render tinymce. if (!$profile_name) { $profile_name = db_result(db_query('SELECT s.name FROM {tinymce_settings} s INNER JOIN {tinymce_role} r ON r.name = s.name WHERE r.rid IN (%s)', implode(',', array_keys($user->roles)))); if (!$profile_name) { return $element; } } $profile = tinymce_profile_load($profile_name); $init = tinymce_config($profile); $init['elements'] = 'edit-'. $textarea_name; if (_tinymce_page_match($profile)) { // Merge user-defined TinyMCE settings. $init = (array) theme('tinymce_theme', $init, $textarea_name, $init['theme'], $is_running); // If $init array is empty no need to execute rest of code since there are no textareas to theme with TinyMCE if (count($init) < 1) { return $element; } $settings = array(); foreach ($init as $k => $v) { $v = is_array($v) ? implode(',', $v) : $v; // Don't wrap the JS init in quotes for boolean values or functions. if (strtolower($v) != 'true' && strtolower($v) != 'false' && $v[0] != '{') { $v = '"'. $v .'"'; } $settings[] = $k .' : '. $v; } $tinymce_settings = implode(",\n ", $settings); $enable = t('enable rich-text'); $disable = t('disable rich-text'); $tinymce_invoke = << img_assist = document.getElementById('img_assist-link-edit-$textarea_name'); if (img_assist) { var img_assist_default_link = img_assist.innerHTML; if ('$img_assist_link' == 'yes') { img_assist.innerHTML = tinyMCE.getEditorId('edit-$textarea_name') == null ? '' : img_assist_default_link; } else { img_assist.innerHTML = tinyMCE.getEditorId('edit-$textarea_name') == null ? img_assist_default_link : ''; } } if (typeof(document.execCommand) == 'undefined') { img_assist.innerHTML = img_assist_default_link; document.write('
$no_wysiwyg
'); } else { document.write("
$link_text
"); } EOD; // We only load the TinyMCE js file once per request if (!$is_running) { $is_running = TRUE; $tinymce_mod_path = drupal_get_path('module', 'tinymce'); if (is_dir($tinymce_mod_path .'/tinymce/jscripts/tiny_mce/plugins/imagemanager/') && user_access('access tinymce imagemanager') ) { // if tinymce imagemanager is installed drupal_add_js($tinymce_mod_path .'/tinymce/jscripts/tiny_mce/plugins/imagemanager/jscripts/mcimagemanager.js'); } if (is_dir($tinymce_mod_path .'/tinymce/jscripts/tiny_mce/plugins/filemanager/') && user_access('access tinymce filemanager') ) { // if tinymce filemanager is installed drupal_add_js($tinymce_mod_path .'/tinymce/jscripts/tiny_mce/plugins/filemanager/jscripts/mcfilemanager.js'); } // TinyMCE Compressor 1.0.9 and greater if (file_exists($tinymce_mod_path .'/tinymce/jscripts/tiny_mce/tiny_mce_gzip.js')) { drupal_add_js($tinymce_mod_path .'/tinymce/jscripts/tiny_mce/tiny_mce_gzip.js'); drupal_add_js($tinymce_gz_invoke, 'inline'); } // TinyMCE Compressor (versions < 1.0.9) elseif (file_exists($tinymce_mod_path .'/tinymce/jscripts/tiny_mce/tiny_mce_gzip.php')) { drupal_add_js($tinymce_mod_path .'/tinymce/jscripts/tiny_mce/tiny_mce_gzip.php'); } else { // For some crazy reason IE will only load this JS file if the absolute reference is given to it. drupal_add_js($tinymce_mod_path .'/tinymce/jscripts/tiny_mce/tiny_mce.js'); } drupal_add_js($js_toggle, 'inline'); // We have to do this becuase of some unfocused CSS in certain themes. See http://drupal.org/node/18879 for details drupal_set_html_head(''); } // Load a TinyMCE init for each textarea. if ($init) drupal_add_js($tinymce_invoke, 'inline'); // Set resizable to false to avoid drupal.js resizable function from taking control of the textarea $element['#resizable'] = FALSE; } return $element; } /** * Implementation of hook_user(). */ function tinymce_user($type, &$edit, &$user, $category = NULL) { if ($type == 'form' && $category == 'account' && user_access('access tinymce')) { $profile = tinymce_user_get_profile($user); // because the settings are saved as strings we need to test for the string 'true' if ($profile->settings['user_choose'] == 'true') { $form['tinymce'] = array( '#type' => 'fieldset', '#title' => t('TinyMCE rich-text settings'), '#weight' => 10, '#collapsible' => TRUE, '#collapsed' => TRUE ); $form['tinymce']['tinymce_status'] = array( '#type' => 'select', '#title' => t('Default state'), '#default_value' => isset($user->tinymce_status) ? $user->tinymce_status : (isset($profile->settings['default']) ? $profile->settings['default'] : 'false'), '#options' => array('false' => t('disabled'), 'true' => t('enabled')), '#description' => t('Should rich-text editing be enabled or disabled by default in textarea fields?') ); return array('tinymce' => $form); } } if ($type == 'validate') { return array('tinymce_status' => $edit['tinymce_status']); } } /** * @addtogroup themeable * @{ */ /** * Customize a TinyMCE theme. * * @param init * An array of settings TinyMCE should invoke a theme. You may override any * of the TinyMCE settings. Details here: * * http://tinymce.moxiecode.com/wrapper.php?url=tinymce/docs/using.htm * * @param textarea_name * The name of the textarea TinyMCE wants to enable. * * @param theme_name * The default tinymce theme name to be enabled for this textarea. The * sitewide default is 'simple', but the user may also override this. * * @param is_running * A boolean flag that identifies id TinyMCE is currently running for this * request life cycle. It can be ignored. */ function theme_tinymce_theme($init, $textarea_name, $theme_name, $is_running) { // uncomment to debug this /* print_r($init); print_r($textarea_name); print_r($theme_name); print_r($is_running); */ switch ($textarea_name) { // Disable tinymce for these textareas case 'log': // book and page log case 'img_assist_pages': case 'caption': // signature case 'pages': case 'access_pages': //TinyMCE profile settings. case 'user_mail_welcome_body': // user config settings case 'user_mail_approval_body': // user config settings case 'user_mail_pass_body': // user config settings case 'synonyms': // taxonomy terms case 'description': // taxonomy terms unset($init); break; // Force the 'simple' theme for some of the smaller textareas. case 'signature': case 'site_mission': case 'site_footer': case 'site_offline_message': case 'page_help': case 'user_registration_help': case 'user_picture_guidelines': $init['theme'] = 'simple'; foreach ($init as $k => $v) { if (strstr($k, 'theme_advanced_')) unset($init[$k]); } break; } /* Example, add some extra features when using the advanced theme. // If $init is available, we can extend it if (isset($init)) { switch ($theme_name) { case 'advanced': $init['extended_valid_elements'] = array('a[href|target|name|title|onclick]'); break; } } */ // Always return $init return $init; } /** @} End of addtogroup themeable */ /** * Grab the themes available to TinyMCE. * * TinyMCE themes control the functionality and buttons that are available to a * user. Themes are only looked for within the default TinyMCE theme directory. * * NOTE: This function is not used in this release. We are only using advanced theme. * * @return * An array of theme names. */ function _tinymce_get_themes() { static $themes = array(); if (!$themes) { $theme_loc = drupal_get_path('module', 'tinymce') .'/tinymce/jscripts/tiny_mce/themes/'; if (is_dir($theme_loc) && $dh = opendir($theme_loc)) { while (($file = readdir($dh)) !== false) { if (!in_array($file, array('.', '..', 'CVS')) && is_dir($theme_loc . $file)) { $themes[$file] = $file; } } closedir($dh); asort($themes); } } return $themes; } /** * Return plugin metadata from the plugin registry. * * We also scrape each plugin's *.js file for the human friendly name and help * text URL of each plugin. * * @return * An array for each plugin. */ function _tinymce_get_buttons($skip_metadata = TRUE) { include_once(drupal_get_path('module', 'tinymce') .'/plugin_reg.php'); $plugins = _tinymce_plugins(); if ($skip_metadata == FALSE && is_array($plugins)) { foreach ($plugins as $name => $plugin) { $file = drupal_get_path('module', 'tinymce') .'/tinymce/jscripts/tiny_mce/plugins/'. $name .'/editor_plugin_src.js'; // Grab the plugin metadata by scanning the *.js file. if (file_exists($file)) { $lines = file($file); $has_longname = FALSE; $has_infourl = FALSE; foreach ($lines as $line) { if ($has_longname && $has_infourl) break; if (strstr($line, 'longname')) { $start = strpos($line, "'") + 1; $end = strrpos($line, "'") - $start; $metadata[$name]['longname'] = substr($line, $start, $end); $has_longname = TRUE; } elseif (strstr($line, 'infourl')) { $start = strpos($line, "'") + 1; $end = strrpos($line, "'") - $start; $metadata[$name]['infourl'] = substr($line, $start, $end); $has_infourl = TRUE; } } } // Find out the buttons a plugin has. foreach ($plugin as $k => $v) { if (strstr($k, 'theme_advanced_buttons')) { if (!isset($metadata[$name]['buttons'])) $metadata[$name]['buttons'] = $plugin[$k]; else $metadata[$name]['buttons'] = array_merge((array) $metadata[$name]['button'], $plugin[$k]); } } // add list of default buttons // source: http://wiki.moxiecode.com/index.php/TinyMCE:Control_reference $name = 'default'; $buttons = array( "bold", "italic", "underline", "strikethrough", "justifyleft", "justifycenter", "justifyright", "justifyfull", "bullist", "numlist", "outdent", "indent", "cut", "copy", "paste", "undo", "redo", "link", "unlink", "image", "cleanup", "help", "code", "hr", "removeformat", "formatselect", "fontselect", "fontsizeselect", "styleselect", "sub", "sup", "forecolor", "backcolor", "forecolorpicker", "backcolorpicker", "charmap", "visualaid", "anchor", "newdocument", "blockquote" ); foreach ($buttons as $button) { $metadata['default']['buttons'][] = $button; } } return $metadata; } return $plugins; } /******************************************************************** * Module Functions :: Public ********************************************************************/ /** * Return an array of initial tinymce config options from the current role. */ function tinymce_config($profile) { global $user; // Drupal theme path. $themepath = path_to_theme() .'/'; $host = base_path(); $settings = $profile->settings; // Build a default list of TinyMCE settings. // Is tinymce on by default? $status = tinymce_user_get_status($user, $profile); $status = 'true'; $init['mode'] = $status == 'true' ? 'exact' : 'none'; // $init['mode'] = "textareas"; $init['theme'] = $settings['theme'] ? $settings['theme'] : 'advanced'; $init['relative_urls'] = 'false'; $init['document_base_url'] = "$host"; $init['language'] = $settings['language'] ? $settings['language'] : 'en'; $init['safari_warning'] = $settings['safari_message'] ? $settings['safari_message'] : 'false'; $init['entity_encoding'] = 'raw'; $init['verify_html'] = $settings['verify_html'] ? $settings['verify_html'] : 'false'; $init['preformatted'] = $settings['preformatted'] ? $settings['preformatted'] : 'false'; $init['convert_fonts_to_spans'] = $settings['convert_fonts_to_spans'] ? $settings['convert_fonts_to_spans'] : 'false'; $init['remove_linebreaks'] = $settings['remove_linebreaks'] ? $settings['remove_linebreaks'] : 'true'; $init['apply_source_formatting'] = $settings['apply_source_formatting'] ? $settings['apply_source_formatting'] : 'true'; $init['theme_advanced_resize_horizontal'] = 'false'; $init['theme_advanced_resizing_use_cookie'] = 'false'; $tinymce_mod_path = drupal_get_path('module', 'tinymce'); if (is_dir($tinymce_mod_path .'/tinymce/jscripts/tiny_mce/plugins/imagemanager/') && user_access('access tinymce imagemanager')) { // we probably need more security than this $init['file_browser_callback'] = "mcImageManager.filebrowserCallBack"; } if (is_dir($tinymce_mod_path .'/tinymce/jscripts/tiny_mce/plugins/filemanager/') && user_access('access tinymce filemanager')) { // we probably need more security than this $init['file_browser_callback'] = "mcImageManager.filebrowserCallBack"; } if ($init['theme'] == 'advanced') { $init['plugins'] = array(); $init['theme_advanced_toolbar_location'] = $settings['toolbar_loc'] ? $settings['toolbar_loc'] : 'bottom'; $init['theme_advanced_toolbar_align'] = $settings['toolbar_align'] ? $settings['toolbar_align'] : 'left'; $init['theme_advanced_path_location'] = $settings['path_loc'] ? $settings['path_loc'] : 'bottom'; $init['theme_advanced_resizing'] = $settings['resizing'] ? $settings['resizing'] : 'true'; $init['theme_advanced_blockformats'] = $settings['block_formats'] ? $settings['block_formats'] : 'p,address,pre,h1,h2,h3,h4,h5,h6'; if (is_array($settings['buttons'])) { // This gives us the $plugins variable. $plugins = _tinymce_get_buttons(); // Find the enabled buttons and the mce row they belong on. Also map the // plugin metadata for each button. $plugin_tracker = array(); foreach ($plugins as $rname => $rplugin) { // Plugin name foreach ($rplugin as $mce_key => $mce_value) { // TinyMCE key foreach ($mce_value as $k => $v) { // Buttons if (isset($settings['buttons'][$rname .'-'. $v])) { // Font isn't a true plugin, rather it's buttons made available by the advanced theme if (!in_array($rname, $plugin_tracker) && $rname != 'font') $plugin_tracker[] = $rname; $init[$mce_key][] = $v; } } } // Some advanced plugins only have an $rname and no buttons if (isset($settings['buttons'][$rname])) { if (!in_array($rname, $plugin_tracker)) $plugin_tracker[] = $rname; } } // Add the rest of the TinyMCE config options to the $init array for each button. if (is_array($plugin_tracker)) { foreach ($plugin_tracker as $pname) { if ($pname != 'default') $init['plugins'][] = $pname; foreach ($plugins[$pname] as $mce_key => $mce_value) { // Don't overwrite buttons or extended_valid_elements if ($mce_key == 'extended_valid_elements') { // $mce_value is an array for extended_valid_elements so just grab the first element in the array (never more than one) $init[$mce_key][] = $mce_value[0]; } else if (!strstr($mce_key, 'theme_advanced_buttons')) { $init[$mce_key] = $mce_value; } } } } // Cleanup foreach ($init as $mce_key => $mce_value) { if (is_array($mce_value)) $mce_value = array_unique($mce_value); $init[$mce_key] = $mce_value; } // Shuffle buttons around so that row 1 always has the most buttons, // followed by row 2, etc. Note: These rows need to be set to NULL otherwise // TinyMCE loads it's own buttons inherited from the theme. if (!isset($init['theme_advanced_buttons1'])) $init['theme_advanced_buttons1'] = array(); if (!isset($init['theme_advanced_buttons2'])) $init['theme_advanced_buttons2'] = array(); if (!isset($init['theme_advanced_buttons3'])) $init['theme_advanced_buttons3'] = array(); // bweh - this isn't right! // some buttons should go in a specific order: // row 1 // - cut // - copy // - paste // - pasteword // - separator // - undo // - redo // - separator // - bold // - italic // - underline // - separator // row 2 // - formatselect // - fontselect // - fontsizeselect // - separator // - justifyleft // - justifycenter // - justifyright // - justifyfull // - separator // - numlist // - bullist // - indent // - outdent $buttons = array_merge($init['theme_advanced_buttons1'], $init['theme_advanced_buttons2'], $init['theme_advanced_buttons3']); $init['theme_advanced_buttons1'] = array(); $init['theme_advanced_buttons2'] = array(); $init['theme_advanced_buttons3'] = array(); $row[] = array( array('newdocument', 'save'), array('bold', 'italic', 'underline'), array('undo', 'redo'), array('cut', 'copy', 'paste', 'pasteword'), array('link', 'unlink'), array('image', 'charmap'), array('code') ); $row[] = array( array('formatselect', 'fontselect', 'fontsizeselect'), array('justifyleft', 'justifycenter', 'justifyright', 'justifyfull'), array('numlist', 'bullist', 'indent', 'outdent') ); foreach ($row as $r_index=>$r) { $row_buttons = array(); foreach ($r as $rg_index=>$rowgroup) { $selected = array_intersect($rowgroup, $buttons); if (count($selected)>0) { if (count($row_buttons)>0) { $row_buttons[] = "separator"; } $row_buttons = array_merge($row_buttons, $selected); } $buttons = array_diff($buttons, $selected); } $init['theme_advanced_buttons' . ($r_index+1)] = $row_buttons; } if (count($buttons)>0) { // some buttons are left -> append them to row 3 $init['theme_advanced_buttons3'] = array_merge($init['theme_advanced_buttons3'], $buttons); } $min_btns = 5; // Minimum number of buttons per row. $num1 = count($init['theme_advanced_buttons1']); $num2 = count($init['theme_advanced_buttons2']); $num3 = count($init['theme_advanced_buttons3']); if ($num3 < $min_btns) { $init['theme_advanced_buttons2'][] = 'separator'; $init['theme_advanced_buttons2'] = array_merge($init['theme_advanced_buttons2'], $init['theme_advanced_buttons3']); $init['theme_advanced_buttons3'] = array(); $num2 = count($init['theme_advanced_buttons2']); } if ($num2 < $min_btns) { $init['theme_advanced_buttons1'][] = 'separator'; $init['theme_advanced_buttons1'] = array_merge($init['theme_advanced_buttons1'], $init['theme_advanced_buttons2']); // Squish the rows together, since row 2 is empty $init['theme_advanced_buttons2'] = $init['theme_advanced_buttons3']; $init['theme_advanced_buttons3'] = array(); $num1 = count($init['theme_advanced_buttons1']); } if ($num1 < $min_btns) { $init['theme_advanced_buttons1'] = array_merge($init['theme_advanced_buttons1'], $init['theme_advanced_buttons2']); // Squish the rows together, since row 2 is empty $init['theme_advanced_buttons2'] = $init['theme_advanced_buttons3']; $init['theme_advanced_buttons3'] = array(); } } } if ($settings['css_classes']) $init['theme_advanced_styles'] = $settings['css_classes']; if ($settings['css_setting'] == 'theme') { $css = $themepath .'style.css'; if (file_exists($css)) { $init['content_css'] = $host . $css; } } else if ($settings['css_setting'] == 'self') { $init['content_css'] = str_replace(array('%h', '%t'), array($host, $themepath), $settings['css_path']); } return $init; } /** * Load all profiles. Just load one profile if $name is passed in. */ function tinymce_profile_load($name = '') { static $profiles = array(); if (!$profiles) { $roles = user_roles(); $result = db_query('SELECT * FROM {tinymce_settings}'); while ($data = db_fetch_object($result)) { $data->settings = unserialize($data->settings); $result2 = db_query("SELECT rid FROM {tinymce_role} WHERE name = '%s'", $data->name); $role = array(); while ($r = db_fetch_object($result2)) { $role[$r->rid] = $roles[$r->rid]; } $data->rids = $role; $profiles[$data->name] = $data; } } return ($name ? $profiles[$name] : $profiles); } /******************************************************************** * Module Functions :: Private ********************************************************************/ /** * Determine if TinyMCE has permission to be used on the current page. * * @return * TRUE if can render, FALSE if not allowed. */ function _tinymce_page_match($edit) { $page_match = FALSE; // This piece of code sometimes fires on textareas that are just // plain HTML; so I disabled it. I want my, I want my, I want my MCE.... // // Kill TinyMCE if we're editing a textarea with PHP in it! // // PHP input formats are #2 in the filters table. // if (is_numeric(arg(1)) && arg(2) == 'edit') { // $node = node_load(arg(1)); // if ($node->format == 2) { // return FALSE; // } // } if ($edit->settings['access_pages']) { // If the PHP option wasn't selected if ($edit->settings['access'] < 2) { $path = drupal_get_path_alias($_GET['q']); $regexp = '/^('. preg_replace(array('/(\r\n?|\n)/', '/\\\\\*/', '/(^|\|)\\\\($|\|)/'), array('|', '.*', '\1'. preg_quote(variable_get('site_frontpage', 'node'), '/') .'\2'), preg_quote($edit->settings['access_pages'], '/')) .')$/'; $page_match = !($edit->settings['access'] xor preg_match($regexp, $path)); } else { $page_match = drupal_eval($edit->settings['access_pages']); } } // No pages were specified to block so show on all else { $page_match = TRUE; } return $page_match; } function tinymce_user_get_profile($account) { $profile_name = db_result(db_query('SELECT s.name FROM {tinymce_settings} s INNER JOIN {tinymce_role} r ON r.name = s.name WHERE r.rid IN (%s)', implode(',', array_keys($account->roles)))); if ($profile_name) { return tinymce_profile_load($profile_name); } else { return FALSE; } } function tinymce_user_get_status($user, $profile) { $settings = $profile->settings; if ($settings['user_choose']) { $status = isset($user->tinymce_status) ? $user->tinymce_status : (isset($settings['default']) ? $settings['default'] : 'false'); } else { $status = isset($settings['default']) ? $settings['default'] : 'false'; } return $status; }