Very Simple File Upload Plugin for TinyMCE 3.x Development

TinyMCE is a pretty nice, powerful WYSIWYG editor for the web and it has come quite a long way from just a few years ago in terms of functionality.

Besides having it integrated in my Drupal installation, I also use TinyMCE as the primary editor in the custom CMSs I develop for our clients. Typically, I have to scale down the functionality quite a bit to avoid confusion, as for non-technical people, more is not always better, and frequently quite on contrary.

However, one basic feature that’s missing in TinyMCE right “out of the box” is the file upload functionality. This is understandable, as they provide premium file (MCFileManager) and image (MCImageManager) management plugins for TinyMCE for which they charge, hence no sense in competing with yourself.

These plugins look really good.. but, as I mentioned before, sometimes elaborate functionality is not the goal and thus it would be nice to have an easy, simple alternative.

Naturally, I googled a bit for an open-source solution, but found little that fit the bill. Some plugins were not updated for the TinyMCE version 3.x, which I think is quite superior to 2.x releases. Others were trying hard to mimic a file manager as opposed to just provide a simple upload solution. So yet again, I set out to try and do it myself.

What I was looking for was dirt simple:

  • Ability to upload a file using a simple browse and upload PHP form
  • Integration with advimg and advlink plugins for TinyMCE, so users can upload a file as they are adding an image to the page
  • Simple configuration/integration – I wanted to have relevant config in one place as opposed to various config files, and integration to be as simple as dropping something into TinyMCE directory and modifying TinyMCE initialization script.

At this point, let me admit, that I had no idea (and still fell this way a bit) what I was doing in terms of writing a TinyMCE plugin. So I just read some of their wiki-stuff, and trial and errored various things until it just worked.

The Old Way

Of course, having file upload functionality is not something that came just now, and I had a “solution” for previous incarnations of my CMS, which was terribly hacky and it usually took me extra time to figure out how to integrate it into new environments. Notably, I just modified the advimg and advlink plugins (the HTML files that displayed the forms) to include a link to a PHP script that I would open as a popup.

The PHP File Upload Script

First, let me say that this piece has not changed much. I wanted to keep it simple, and it does not get more simple/dangerous than this upload PHP script of mine – if you do end up using it, feel free to modify it to modify it as you see fit.

I originally wrote it to work as a standalone page – uploder.php. It accepted some parameters in the GET string, namely:

  • d – upload directory relative to the location of the uploder.php script itself
  • resize_x – if the file was an image, and this parameter was present/non zero, I’d attempt to resize the image to the specified width
  • resize_y – if the file was an image, and this parameter was present/non zero, I’d attempt to resize the image to the specified height
  • replace_file – if set to “no”, it would not overwrite a file if one was already present. Default behavior was to overwrite

The script displayed a form, that would submit to itself, adding another GET element that would signal that a file has been browsed and ready to be uploaded. Here is the form Code:

<form action="uploader.php?<?php echo $_SERVER["QUERY_STRING"]; ?>&q=upload" 
    method="post" enctype="multipart/form-data" onsubmit="">
     File Name: <br/>
     <input type="file" size="70" name="upload_file" ID="File1"/><br/>
     <input type="submit" name="Upload File" value="Upload File" style="width: 150px;"
onclick="document.getElementById('progress_div').style.visibility='visible';"/>            
<div id="progress_div" style="visibility: hidden;">
        <img src="progress.gif" alt="wait..." style="padding-top: 5px;">
     </div>        
</form>

As you can see, very simple… So on submit to self, the form append ‘&q=upload’ to tell the script that the time has come to process the upload. When the script found that $_GET[‘q’] was set to upload, it called the upload_content_file(…) function.

So.. Basically, I wanted to reuse the same uploader with TinyMCE so I pulled the following hack:

  • pass more paramters in the GET query to include an ID of the element that should receive the path to uploaded file
  • add another GET parameter to allow substitution of relative path with an aboslute path (since you don’t always know which page the content will be referenced from)
  • Modify the “htm” file in advlink and advimg plugins to include an ‘upload’ button that would popup a window and pass parameters to the uploader script..

This solution worked… But clearly it’s a hack and would also require me to do some funny relative path calculations every time I deployed the CMS in a new directory structure. So finally, I got fed up and thought I’d do my own TinyMCE plugin that aims to simplify this process and make the deployment easier.

The New Way

I thought that I should be able to:

  • write a TinyMCE plugin that brings up the upload dialog via TinyMCE mechanisms (not my hacky javascript popup)
  • integrate aforementioned plugin with advlink and advimg
  • be able to control most of the configuration options for the plugin in a single location (i.e. tinymce init)

The Plugin

There is a page somewhere on TinyMCE wiki that shows how to write a plugin, and more specifically a file uploader plugin. And while it was pretty informative, it left many questions for someone who is not well versed in the TinyMCE DOM and API structure. I won’t go into too many details as to how I arrived at the final code, but basically here are the outline:

  • Create a directory in the plugins directory of TinyMCE
  • create editor_plugin.js in there – this is the file that TinyMCE looks for when it is loading its plugins
  • write some generic-looking code that exposes functions that let TinyMCE to ‘discover’ and call the plugin as described here
  • Change the plugin name, and change the URL that the plugin opens to our script – ‘uploader.php’
  • Add the plugin to the ‘plugins’ list in tinyMCE.init function
  • Add the plugin to theme_advanced_buttonsX listing (not, you need to create an icon that’s referenced as ‘example.gif’ in that sample code provided by TinyMCE

Phew. Not nearly there. Laughing Doing all of the above and refreshing your page integrated with tinyMCE should make the icon appear in the TinyMCE controls and when you click on it, it will open the uploader dialog.. but now what! The uploader has no arguments, does not know where to put the files (well, by defuault it will upload to the same directory), is not integrated with advimg or advlink, and won’t even close itself.

So, we need to add some custom parameters for our plugin, so that when we open a popup to uploader.php, we can pass them to the script. Simple! In the tinyMCE init, we can add something like:

plugin_ccSimpleUploader_upload_path: ‘../../../../uploads’,
plugin_ccSimpleUploader_upload_substitute_path: ‘/tinymce/uploads/’,

In my environment, the plugin resides in
/tinymce/jscripts/tiny_mce/plugins/ccSimpleUploader
and I created an ‘upload’ dricetory in
/tinymce/uploads/
Hence, the relative upload path is 4 levels up in the uploads directory, and when displayed on a web page, that can be accessed using root path notation and therefore we need not concern ourself relative link/image paths.

Once we have those two lines in the tinyMCE.init function, we can get the values inside our plugin by calling something like:

tinyMCE.activeEditor.getParam('plugin_ccSimpleUploader_upload_path');

 

Armed with this info, we can encode it into the GET query for the popup so that relative upload directory and absolute directory are passed to our PHP script.

Closer, but still no advlink or advimg integration.. and after the file is uploaded, the window won’t close itself or update the calling window with the absolute URL.

AdvImg/AdvLink Integration

Turns out this piece is also simple – well.. in hindsight…. and is generally outlined here .

  • add configuration to tinyMCE.init that tells tinyMCE which function to call when the file needs to be uploaded
  • Inside that function, replicate the functionality of plugin creation, where a popup hosting uploader.php is opened
  • That function will also receive the window object of the caller (advimg or advlink window) and the ID of the element whose value needs to be populated with the result of the upload
  • Once the upload is done, use the ID and window information, to populate the desired field and close yourself (with some inline Javascript

One thing I didn’t like much about this approach is that the functionality was duplicated for standalone plugin instantiation (from the editor itself) and for advimg/advlink instantiation inside the Custom Callback Function..Furthermore, the ‘url’ argument for my Custom Callback Function was never set by advimg/advlink, so I could not tell where the script was runnign from and did not know the location of uploader.php relative to that.

So, I did the “unthinkable”.. I put the custom callback function directly inside my plugin’s editor_plugin.js file (which, by the way elminited the need to include any additional Javascript files in the page hosting TinyMCE). Furthremore, I stored the script’s URL in an instance variable, so it can be retrieved even when it is called from advlink/advimg context. I also abstracted the return case, such that if the parent window was set in the Custom Callback, I would use that to populate the calling windows element. Otherwise, I would assume the plugin is being called from the editor itself, and would insert the resultign upload path directly into the editor… tun dun dun!

So… Here is my editor_plugin.js (without comments):

(function() {
    var strPluginURL;
    tinymce.create('tinymce.plugins.ccSimpleUploaderPlugin', {
        init: function(ed, url) 
        {
            strPluginURL = url;                         // store the URL for future use..
            ed.addCommand('mceccSimpleUploader', function() {
                ccSimpleUploader();              
            });
            ed.addButton('ccSimpleUploader', {
                title: 'ccSimpleUploader',
                cmd: 'mceccSimpleUploader',
                image: url + '/img/ccSimpleUploader.png'
            });
        },
        createControl: function(n, cm) {
            return null;
        },
        getPluginURL: function() {
            return strPluginURL;
        },
        getInfo: function() {
            return {
                longname: 'ccSimpleUploader plugin',                
                author: 'Timur Kovalev',
                authorurl: 'http://www.creativecodedesign.com',
                infourl: 'http://www.creativecodedesign.com',
                version: "0.1"
            };
        }
    });
    tinymce.PluginManager.add('ccSimpleUploader', tinymce.plugins.ccSimpleUploaderPlugin);
})();

function ccSimpleUploader(field_name, url, type, win) {    
    var strPluginPath = tinyMCE.activeEditor.plugins.ccSimpleUploader.getPluginURL();                             
    var strUploaderURL = strPluginPath + "/uploader.php";                                                         
    var strUploadPath = tinyMCE.activeEditor.getParam(
             'plugin_ccSimpleUploader_upload_path');                     
    var strSubstitutePath = tinyMCE.activeEditor.getParam(
             'plugin_ccSimpleUploader_upload_substitute_path');      

    if (strUploaderURL.indexOf("?") < 0)                                                                          
        strUploaderURL = strUploaderURL + "?type=" + type + "&d=" + 
             strUploadPath + "&subs=" + strSubstitutePath; 
    else
        strUploaderURL = strUploaderURL + "&type=" + type + "&d=" + 
             strUploadPath + "&subs=" + strSubstitutePath;

    tinyMCE.activeEditor.windowManager.open({                                                                     
        file            : strUploaderURL,
        title           : 'cc Simple Uploader',
        width           : 400,  
        height          : 100,
        resizable       : "yes", 
        inline          : 1,                                                                                      
        close_previous  : "no"
    }, {
        window : win,
        input : field_name
    });

    return false;
}

function ClosePluginPopup (strReturnURL) {
    var win = tinyMCEPopup.getWindowArg("window");                                               
    if (!win)
        tinyMCE.activeEditor.execCommand('mceInsertContent', false, strReturnURL);
    else
    {
        win.document.getElementById(
              tinyMCEPopup.getWindowArg("input")).value = strReturnURL;    
        if (typeof(win.ImageDialog) != "undefined")                                              
        {        
            if (win.ImageDialog.getImageData) win.ImageDialog.getImageData();                    
            if (win.ImageDialog.showPreviewImage) win.ImageDialog.showPreviewImage(
                  strReturnURL);
        }    
    }
    tinyMCEPopup.close();                                                                         
}

So, after the PHP script is done uploading the file, it simply outputs inine Javascipt code that calles the ClosePluginPopup function with the path of the uploaded file as a parameter. And that just about does it.

A few additional comments on the PHP script:

  • NO SECURITY – will let anything to be uploaded, so don’t use it in its current form in an unprotected environment
  • In the TinyMCE application, it does not any resizing. I left those legacy parameters in there if I ever wanted to extend that (and that can easily be done with plugin configs as show above
  • It’s kind of old and crappy code…Sorry

The nice-ness of this approach, is that you can make your own upload PHP interface and do whatever you like if you just make it work by getting the destination in the GET query, and call the ClosePluginPopup when you are done. I do have a separate Norton-Commander-like online file management solution which I indend to wrap in a TinyMCE plugin, but that will come in the future..

Downloads and configuration information can be found here.

Cheers!