Implementing WordPress TinyMCE plugins for multisite configurations

Note: This discussion pertains to V3.* of WP using an older TinyMCE implementation.  Details might be different now.

WordPress Multi-site configurations often present some unique challenges for WordPress plugin authors.  One I just finished dealing with is how WordPress handles a WP plugin that is defining a TinyMCE editor plugin.  What follows is going to be very technical so read on if you want to learn more about writing TinyMCE editor plugins in a WordPress environment.

For my wife’s food blog I’m writing a recipe plugin to WordPress and have an editor plugin to assist in the formatting of the ingredients. The TinyMCE editor popup I’ve created works fine in a WP single site install, but breaks in a multi-site configuration when the WP Admin screens are setup to use a mapped domain name.  When configured this way, WP internals provide the following:

  • WP_PLUGIN_URL = ""
  • plugins_url("path") returns ""

How the site breaks will depend on how the WP plugin has been coded to supply URLs for the embedded content. To understand how things fail it’s important to understand a little about how TinyMCE works in the context of the Admin screen.

When TinyMCE launches a pop-up window, it uses an IFRAME element to load the target HTML file supplying the pop-up contents.  Within the IFRAME, access to the outside Javascript tinymce instance is required for the functions of the popup dialog to interact with editor content.  To allow Javascript loaded within the IFRAME access to outside elements, the HTML contents of the IFRAME must use the same domain name in the URL as the outside HTML.  If this requirement is violated, the result is a permission violation that might be visible in the Javascript console window of the browser.  This error is a Cross Site Scripting (XSS) permission violation.

The mce_external_plugins WordPress filter is used to add TinyMCE editor plugins.  Because the plugin is using the network domain name and the Admin screen is using the mapped domain name, the following code will cause an XSS permission violation when the related TinyMCE plugin pop-up is loaded.

function add_tinymce_plugins($plugin_array) {
	$plugin_array['mce-plugin'] = WP_PLUGIN_URL . '/plugin/mce-plugin/editor_plugin.js';
	return $plugin_array;
add_filter('mce_external_plugins', 'add_tinymce_plugins');

The solution to the XSS violation is to use the function plugins_url() introduced in WordPress 2.6 as in the following:

function add_tinymce_plugins($plugin_array) {
	$plugin_array['mce-plugin'] = plugins_url('plugin/mce-plugin/editor_plugin.js');
	return $plugin_array;
add_filter('mce_external_plugins', 'add_tinymce_plugins');

There’s a new problem if the TinyMCE plugin has been written to provide i18n support using typical TinyMCE methods.  When the request for the edit screen is being handled, one part of processing is to find and load the language files for TinyMCE plugins.  This is done by the routine wp_tiny_mce() in wp-admin/includes/posts.php.  At least up to WP v3.2.1, wp_tiny_mce() assumes that the TinyMCE external plugins provided via the mce_external_plugins filter uses WP_PLUGIN_URL to locate the TinyMCE plugin files.  As a result, the language files located in plugin/mce-plugin/langs/ will not be found due to the mismatch in usage and the expected translations will not take place.

Now we have a conundrum.  Use WP_PLUGIN_URL and the pop-up plugin scripts won’t be able to access the TinyMCE instance in the editor window, rendering the pop-up essentially non-functional.  Or, use plugins_url() and the i18n translation is borked.  Thankfully there’s a workaround available which I found by exploring how other plugins dealt with this situation; specifically how the TinyMCE Advanced plugin was able to work when mine didn’t.

In wp_tiny_mce(), before the function attempts to find the translation files for the defined locale, there is a section that makes use of the mce_external_languages filter to load language files.  This filter supplies a PHP file that will be able to locate the required language files for the pop-up i18n translation. The referenced PHP file is interpreted inline by wp_tiny_mce() and must supply the necessary javascript code used to define the language translations in the variable $strings when it completes.

In an appropriate file in the WP plugin, code like the following is needed (this code is partially lifted from Andrew Ozz’s excellent TinyMCE Advanced plugin):

function add_tinymce_langs($langs)
	// File system path to MCE plugin languages PHP script
	$langs[$plugin] = WP_PLUGIN_DIR . 'plugin/mce-plugin/langs/langs.php';
	return $langs;
add_filter('mce_external_languages', 'add_tinymce_langs');

// Used by langs.php to retrieve contents of named file
function retrieve_file_contents($path) {
	if ( function_exists('realpath') )
		$path = realpath($path);

	if ( ! $path || ! @is_file($path) )
		return '';

	if ( function_exists('file_get_contents') )
		return @file_get_contents($path);

	$content = '';
	$fp = @fopen($path, 'r');

	if ( ! $fp )
		return '';

	while ( ! feof($fp) )
		$content .= fgets($fp);

	return $content;

And in plugin/mce-plugin/langs/langs.php:

$lang_file = dirname(__FILE__) . '/' . $mce_locale . '_dlg.js';

if ( is_file($lang_file) && is_readable($lang_file) )
	// Found language file for defined locale, use it
	$strings = retrieve_file_contents($lang_file);
else {
	// Use english locale file, treat it as the defined locale
	$strings = retrieve_file_contents(dirname(__FILE__) . '/en_dlg.js');
	$strings = preg_replace( '/([\'"])en\./', '$1'.$mce_locale.'.', $strings, 1 );

Posted by Ken

Toastmaster, Woodworker, Craft Beer Enthusiast and dutiful supporter of three demanding house cats. I'm also an experienced Software Engineering Manager with a demonstrated history of working in the Information Technology and Services industry. Skilled in Operating System Development, Unix, SPARC Servers, Wordpress and PHP.

Leave a Reply

Your email address will not be published. Required fields are marked *