An alternative file manager for CKEditor (c5filemanager)

Here are my instructions for doing away with the CKFinder module and its "Demo Version" banners and use a simple editor that has been written by someone else. I have also tried 'elfinder' and whilst it had a lot of features I was left extremely unimpressed by its seemingly broken css (such as it only filling half the window that had been opened for it).

This one looks simple and clean and so I've had a go at integrating it into my drupal install.  I'm quite novice when it comes to the drupal architecture so this solution is quite hackish - you have been warned!  Although it does achieve its goal and adds additional functionality which I haven't seem elsewhere by sandboxing each user into their own directory tree and applying a quota to the sum of their files, which is checked during uploads and will deny the upload if they are over quota.

I am happy, quite pleased in fact, that I managed to get it working and enhanced it with the following: -

  • Fixed a PHP date issue and integrated it with the CKEditor module
  • Added drupal role permission for using the file manager - was open to all previously.
  • Added specific user upload directories so they are segregated from each other.
  • Added quota checking on file upload.

It is nice to be free of 'those' banners (otherwise I'd have happily stayed with CKFinder).  I've now extended my integration so that you can give drupal roles permission to use the editor and local users into their own upload area so that they don't interfere with each other and have implemented crude quotas so that users can't upload any more files once they have exceeded a pre-defined quota (site-wide at present).

There is some information (not extensive documentation) here: -

http://labs.corefive.com/2009/10/30/an-open-file-manager-for-ckeditor-3-0/
http://labs.corefive.com/Projects/FileManager/

Install

NOTE: These instructions are for installing and configuring the c5 File-Manager and intrgrated with CKEditor 3.x on Drupal 7x - where CKEditor is itself integrated with Drupal using the CKEditor plugin and not WYSIWYG (I'm not using WYSIWYG module as I had layout issues when I tried it and so went for the native CKEditor module).

1. Check out a copy of the c5 FileManager from the repository using Git :

git clone http://github.com/simogeo/Filemanager.git

or download the archive from Github : http://github.com/simogeo/Filemanager/archives/master

extract/copy it to drupal7install/sites/all/modules/ckeditor/ so you get something like drupal7install/sites/all/modules/ckeditor/c5filemanager

2. Within your c5filemanager install, make a copy of the default configuration file ("filemanager.config.js.default" located in the scripts directory), removing the '.default' from the end of the filename.  If you want english then you don't want to change any of the settings.

Please replace: -

var am = document.location.pathname.substring(1, document.location.pathname
                .lastIndexOf('/') + 1);
// Set this to the directory you wish to manage.
var fileRoot = '/' + am + 'userfiles/';

With

var fileRoot = '/';

We are going to change to location of where files are uploaded later in the auth() php connector function.

You should be able to access the new file manager by browsing directly to it's install location. e.g. http://www.practicalclouds.com/sites/all/modules/ckeditor/c5filemanager/...

3. FIX the php date() issue.

When you browse to the c5 file manager's index page it does't load properly and you get this message in the apache error logs: -

[Sat Jul 09 17:38:07 2011] [error] [client 127.0.0.1] PHP Warning:  date(): It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected 'Europe/London' for 'BST/1.0/DST' instead in /var/www/html/drupal-7.4/sites/all/modules/ckeditor/c5filemanager/connectors/php/filemanager.class.php on line 379, referer: http://www.practicalclouds.com/sites/all/modules/ckeditor/c5filemanager/index.html

to fix this cd c5filemanager/connectors/php and vi "filemanager.config.php" and anywhere in the file please use the php_default_timezone_set function to set your timezone: -

e.g.

date_default_timezone_set('Europe/London');

Now the browser will load properly and you can test uploading an manipulating files etc..

4.  Integrate the c5 file manager with CKEditor

I spent a lot of time working out how to get this file manager to work with CKEditor drupal module which involve a couple of hacks to the CKEditor module!  For example. I borrow the permissioning scheme from CKEditor to allow us to set permission for our new file manager.  There is also some work that I've needed to do to the filemanager's php connector in order to implement the authorization, allow users to be sandboxed in their own directory path and to add user quota checking on file uploads.  I only make two simple changes to the CKEditor module itself.

In sites/all/modules/ckeditor/ckeditor.module do a search for 'finder', the single match should be in the function ckeditor_permission.  I want you to replace the whole If statement: -

   if (file_exists(drupal_get_path('module', 'ckeditor') . "/ckfinder")) {
        $arr['allow CKFinder file uploads'] = array(
            'title' => t('CKFinder access'),
            'description' => t('Allow users to use CKFinder')
        );
    }
    return $arr;
}

with...

    #if (file_exists(drupal_get_path('module', 'ckeditor') . "/ckfinder")) {
        $arr['allow C5 FileManager access'] = array(
            'title' => t('C5 FileManager access'),
            'description' => t('Allow users to use the C5 FileManager.')
        );
    #}
    return $arr;
}

This removes the check for the CKFinder and cteates a "C5 FileManager access" permission we can grant in /admin/people/permissions!

You wont be able to use your file manager unless you disable the check for the ckfinder being installed.  Edit sites/all/modules/ceditor/ckeditor.install and replace the _ckeditor_requirements_ckfinder_config_check function with: -

function _ckeditor_requirements_ckfinder_config_check($profile_name) {
    return FALSE;
}

That's it for changes to the CKEditor module, the rest of the changes are made to the C5 Filemanager software itself.

5. Make the following changes or alternatively install this archive file into the directory sites/all/modules/ckeditor and then set PATHTOUSERCONTENT in connectors/php/filemanager.config.php (unless you also save your user-content to /user-content below your drupal root, and you can set the quota to whatever you wish.

c5filemanager.tgz

The C5 Filemanager is written in Javascript and has a number of connectors written in other languages that can be used to integrate it with other programs.  I'll make use of the php connector and make changes to this in order to integrate with Drupal 7 and add a few essential features I needed such as sand boxes and quotas.  Most of my efforts lie in the authentication function, where most of my features lie - but I have also had to go through and alter a number of other functions within the connector to make it work correctly with my changes.  My authentication function will authenticate to drupal, and will work out the directory which this user is allowed to save into (and create it, if it doesn't already exist).  Note: -

  • I assume that the apache document root is the same as the root of the drupal install.
  • You can define your own PATHTOUSERCONTENT if you want to store the content under another path.
  • I append the UID and the name of the user to the save directory but you could invent your own scheme
  • If the directory does not exist, then I create it (the user has permission so should have one).
  • The auth() function MUST be called when the filemanager.config.php is included so that the docroot gets set correctly!
  • I set an extra variable 'content_path' in the config array so that we can use it in the getinfo function within filemanager.class.php to return the full path to the user content being selected, otherwise only the relative path is passed back and ckeditor does not get the correct path.

By adding our data/time fix, our own auth() function and then calling it to make sure that the docroot is set, the top of the connectors/php/filemanager.config.php now looks like this: -

<?php
/**
 *      Filemanager PHP connector configuration
 *
 *      filemanager.config.php
 *      config for the filemanager.php connector
 *
 *      @license        MIT License
 *      @author         Riaan Los <mail (at) riaanlos (dot) nl>
 *      @author         Simon Georget <simon (at) linea21 (dot) com>
 *      @copyright      Authors
 */

date_default_timezone_set('Europe/London');

define('PATHTOUSERCONTENT','/user-content');

/**
 *    Check if user is authorized
 *
 *    @return boolean true is access granted, false if no access
 */
function auth() {
    static $authenticated;
    global $config;

    // assume that drupal root is the same as the webservers docroot
    $drupal_root=$_SERVER['DOCUMENT_ROOT'];
    if (!defined('DRUPAL_ROOT')){
            define('DRUPAL_ROOT', $drupal_root);
        }
    // do the drupal bootstrap
    require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
    drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

    //check if we have permission to use the filemanager
    $authenticated=user_access('allow C5 FileManager access');
    if ($authenticated) {
        $myuid=$GLOBALS["user"]->uid;
        $myname=$GLOBALS["user"]->name;
        $myname=preg_replace("/[^a-zA-Z0-9]\s/", "", $myname);
        $myuserloc= $myuid . "_" . $myname;
        $mypath= PATHTOUSERCONTENT . "/" . $myuserloc ."/";
        $myfullpath=$drupal_root . $mypath;

        // create the directory if it doesn't already exist
        if (! is_dir("$myfullpath")) {
            $myresult=mkdir("$myfullpath",0755,true);
        }

        // set the C5 filemanager doc_root to the path of our content directory.
        $config['doc_root']=$myfullpath;
        $config['content_path']= $mypath;
        // set a quota for each user in kilobytes
        // e.g. 500Mb
        $config['quota']= 5120;
    }

    return $authenticated;
}

// call auth() now to set the doc_root and content_path
auth();

7.  We also need to make a number of edits to filemanager.class.php within the php connector.  In order to support Drupal we must adjust the paths which are used in some places.  We need to make the filemanager return the whole path of a selected file back to Drupal and not just the relative path from it's doc_root (which we have set as the users' file area); e.g. return /user-content/1_DaveMcCormick/myfolder/1.jpg and not just /myfolder/1.jpg.  Drupal needs the full path in order to create a proper link to where the file is located below the apache docroot and it isn't aware of changes I've made to restrict each user to their own sandbox in order to adjust this.  It's easier for me to fix this stuff within the php connector for the C5 Filemanager.

Find the function getinfo and change: -

public function getinfo() {
...

    $array = array(
                        'Path'=> $this->get['path'],
                        'Filename'=>$this->item['filename'],
                        'File Type'=>$this->item['filetype'],

To

public function getinfo() {
...

    $array = array(
                        // 'Path'=> $this->get['path'],
                        'Path'=> $this->config['content_path'] . $this->get['path'],
                        'Filename'=>$this->item['filename'],
                        'File Type'=>$this->item['filetype'],
                        'Preview'=>$this->item['preview'],
                        'Properties'=>$this->item['properties'],
                        'Error'=>"",
                        'Code'=>0
    );
    $value=$array['Path'];
    return $array;
  }

8. Edit the function 'add' in filemanager.class.php so that we check the config['quota'] limit we set in the auth() function when uploading files.  Change: -

  public function add() {
...
   if(!$this->config['upload']['overwrite']) {
      $_FILES['newfile']['name'] = $this->checkFilename($this->doc_root . $this->post['currentpath'],$_FILES['newfile']['name']);
    }
    move_uploaded_file($_FILES['newfile']['tmp_name'], $this->doc_root . $this->post['currentpath'] . $_FILES['newfile']['name']);

    $response = array(
            'Path'=>$this->post['currentpath'],
            'Name'=>$_FILES['newfile']['name'],
            'Error'=>"",
            'Code'=>0
    );

To

  public function add() {
...
   if(!$this->config['upload']['overwrite']) {
      $_FILES['newfile']['name'] = $this->checkFilename($this->doc_root . $this->post['currentpath'],$_FILES['newfile']['name']);
    }
     if (isset($this->config['quota'])) {
        $usedspace=system("du -sxk \"" . $this->config['doc_root'] . "\" | awk '{print \$1}'");

        if ($usedspace >= $this->config['quota']) {
                $this->error("Sorry, you are over your quota - please remove files before uploading.",true);
        } else {
            move_uploaded_file($_FILES['newfile']['tmp_name'], $this->doc_root . $this->post['currentpath'] . $_FILES['newfile']['name']);
        }
    }

    $response = array(
                        'Path'=>$this->post['currentpath'],
                        'Name'=>$_FILES['newfile']['name'],
                        'Error'=>"",
                        'Code'=>0
    );

9 Damn, now that I'm returning the full path from the getinfo function so that Drupal is happy, I've killed the delete, rename and download functions.  No fear, we'll have to amend these functions slightly too, in filemanager.class.php.  We are basically replacing references to doc_root with DRUPAL_ROOT (as the doc_root includes our userfiles path in it and so does our path!)

Change

  public function delete() {

    if(is_dir($this->doc_root . rawurldecode($this->get['path']))) {
      $this->unlinkRecursive($this->doc_root . rawurldecode($this->get['path']));
      $array = array(
                                'Error'=>"",
                                'Code'=>0,
                                'Path'=>$this->get['path']
      );
      return $array;
    } else if(file_exists($this->doc_root . rawurldecode($this->get['path']))) {
      unlink($this->doc_root . rawurldecode($this->get['path']));
      $array = array(
                                'Error'=>"",
                                'Code'=>0,
                                'Path'=>$this->get['path']
      );
      return $array;
    } else {
      $this->error(sprintf($this->lang('INVALID_DIRECTORY_OR_FILE')));
    }
  }

To

  public function delete() {

    if(is_dir( DRUPAL_ROOT . rawurldecode($this->get['path']))) {
      $this->unlinkRecursive( DRUPAL_ROOT . rawurldecode($this->get['path']));
      $array = array(
                                'Error'=>"",
                                'Code'=>0,
                                'Path'=>$this->get['path']
      );
      return $array;
    } else if(file_exists( DRUPAL_ROOT . rawurldecode($this->get['path']))) {
      unlink( DRUPAL_ROOT . rawurldecode($this->get['path']));
      $array = array(
                                'Error'=>"",
                                'Code'=>0,
                                'Path'=>$this->get['path']
      );
      return $array;
    } else {
      $this->error(sprintf($this->lang('INVALID_DIRECTORY_OR_FILE')));
    }
  }

In the function rename() change: -

    if(file_exists ($this->doc_root . $path . '/' . $this->get['new'])) {
      if($suffix=='/' && is_dir($this->doc_root . $path . '/' . $this->get['new'])) {
        $this->error(sprintf($this->lang('DIRECTORY_ALREADY_EXISTS'),$this->get['new']));
      }
      if($suffix=='' && is_file($this->doc_root . $path . '/' . $this->get['new'])) {
        $this->error(sprintf($this->lang('FILE_ALREADY_EXISTS'),$this->get['new']));
      }
    }

    if(!rename($this->doc_root . $this->get['old'],$this->doc_root . $path . '/' . $this->get['new'])) {
      if(is_dir($this->get['old'])) {
        $this->error(sprintf($this->lang('ERROR_RENAMING_DIRECTORY'),$filename,$this->get['new']));
      } else {
        $this->error(sprintf($this->lang('ERROR_RENAMING_FILE'),$filename,$this->get['new']));
      }
    }

To

    if(file_exists (DRUPAL_ROOT . $path . $this->get['new'])) {
      if($suffix=='/' && is_dir(DRUPAL_ROOT . $path . '/' . $this->get['new'])) {
        $this->error(sprintf($this->lang('DIRECTORY_ALREADY_EXISTS'),$this->get['new']));
      }
      if($suffix=='' && is_file(DRUPAL_ROOT . $path . '/' . $this->get['new'])) {
        $this->error(sprintf($this->lang('FILE_ALREADY_EXISTS'),$this->get['new']));
      }
    }

    if(!rename(DRUPAL_ROOT . $this->get['old'],DRUPAL_ROOT . $path . '/' . $this->get['new'])) {
      if(is_dir($this->get['old'])) {
        $this->error(sprintf($this->lang('ERROR_RENAMING_DIRECTORY'),$filename,$this->get['new']));
      } else {
        $this->error(sprintf($this->lang('ERROR_RENAMING_FILE'),$filename,$this->get['new']));
      }
    }

To Fix the download function change: -

   public function download() {

    if(isset($this->get['path']) && file_exists( $this->doc_root . rawurldecode($this->get['path']))) {
      header("Content-type: application/force-download");
      header('Content-Disposition: inline; filename="' . basename(rawurldecode($this->get['path'])) . '"');
      header("Content-Transfer-Encoding: Binary");
      header("Content-length: ".filesize($this->doc_root . rawurldecode($this->get['path'])));
      header('Content-Type: application/octet-stream');
      header('Content-Disposition: attachment; filename="' . basename(rawurldecode($this->get['path'])) . '"');
      readfile($this->doc_root . $this->get['path']);
    } else {
       $this->error(sprintf($this->lang('FILE_DOES_NOT_EXIST'),rawurldecode($this->get['path'])));
    }
  }

To

    public function download() {

    if(isset($this->get['path']) && file_exists( DRUPAL_ROOT . rawurldecode($this->get['path']))) {
      header("Content-type: application/force-download");
      header('Content-Disposition: inline; filename="' . basename(rawurldecode($this->get['path'])) . '"');
      header("Content-Transfer-Encoding: Binary");
      header("Content-length: ".filesize(DRUPAL_ROOT . rawurldecode($this->get['path'])));
      header('Content-Type: application/octet-stream');
      header('Content-Disposition: attachment; filename="' . basename(rawurldecode($this->get['path'])) . '"');
      readfile(DRUPAL_ROOT . $this->get['path']);
    } else {
       $this->error(sprintf($this->lang('FILE_DOES_NOT_EXIST'),rawurldecode($this->get['path'])));
    }
  }

9.  The final step - make the CKEditor module call our alternative file manager when the user clicks on a browse link.

Go to /config/content/ckeditor and edit the text profile you want to change, e.g. "Full HTML".  In the FILE BROWSER SETTINGS select "none".

Then in the ADVANCED OPTIONS section

Add the CLEditor config in the "Custom javascript configuration": -

config.filebrowserBrowseUrl = '/sites/all/modules/ckeditor/c5filemanager/index.html';

If you get a "Page not Found" error back then you probably didn't set the file manager to 'none'!

Thats it, well done!  We now have a working CKEditor coupled with a very clean and simple file manager. 

We can set access to use the finder in the "permissions" administration and we check authentication and set a where the user is allowed to save files in the auth() function!

WARNING:  Be careful testing authentication with the site owner user  (uid=1) as this is always granted permission.

I think it goes without saying that in order to get this to work and add some useful functionality, we've edited files in both the ckeditor module and the c5filemanager module.  If you try and upgrade either of these modules you are going to have to re-apply your changes again!!!  But the end results are worth it!  I now have a file manager that I'm happy to go live with, knowing that it is simple to use for my users and offers me safe guards to protect my site (such as permissions and quota).

In the future I may turn this into a proper drupal module and integrate it with ckfinder module a lot more cleanly and also work on additional features such as allowing us to set per user quota's via drupal administration interface etc.  Now that I've done this I'm finding adding images to my entries very easy and so I'm going through all of my old content and livening them up with the images which, until recently, were a pain to add!

Update: Another Recommended patch (allow dashes, spaces and dots in filenames)

After making the filemanager work, I was surprised to discover that whenever I rename a file or create a directory, then any '-' characters get removed!  I like to use these characters in my filenames and don't understand why I'm not allowed to!  So I fixed it so I could have my dashes back, and then I noticed that the dots were also being remove for some reason, and spaces have always been translated to '_' s.  I don't understand why we need to do that.

We can fix all of these shot-comings by editing the filemanager.class.php in the connector, but we must also make a change to the filemanager.js file as well.

In connectors/php/filemanager.class.php in function cleanString change: -

        if (is_array($string)) {

          $cleaned = array();

          foreach ($string as $key => $clean) {
            $clean = strtr($clean, $mapping);
            $clean = preg_replace("/[^{$allow}_a-zA-Z0-9]/", '', $clean);
            $cleaned[$key] = preg_replace('/[_]+/', '_', $clean); // remove double underscore
          }
        } else {
          $string = strtr($string, $mapping);
          $string = preg_replace("/[^{$allow}_a-zA-Z0-9]/", '', $string);
          $cleaned = preg_replace('/[_]+/', '_', $string); // remove double underscore
        }
        return $cleaned;

To

        if (is_array($string)) {

          $cleaned = array();

          foreach ($string as $key => $clean) {
            $clean = strtr($clean, $mapping);
            $clean = preg_replace("/[^{$allow}_a-zA-Z0-9-. ]/", '', $clean);
            $cleaned[$key] = preg_replace('/[_]+/', '_', $clean); // remove double underscore
          }
        } else {
          $string = strtr($string, $mapping);
          $string = preg_replace("/[^{$allow}_a-zA-Z0-9-. ]/", '', $string);
          $cleaned = preg_replace('/[_]+/', '_', $string); // remove double underscore
        }
        return $cleaned;

Note the added '-' , '.' and ' ' on the end of the regex string.  Also remove the ' '=>'_' entry from the $mapping = array() definition to prevent all the spaces being translated to underscores.

In scripts/filemanager.js in function cleanString change: -

cleaned = cleaned.replace(/[^_a-zA-Z0-9]/g, "");

To

cleaned = cleaned.replace(/[^_a-zA-Z0-9-. ]/g, "");

Comments

Roger's picture

Thanks for sharing this with us. I was looking for weeks now to find this solution. I have one problem with it. When launching Filemanager it spins but does not load the content of the user-content area. My best guess it that I definded PATHTOUSERCONTENT incorrectly. I am using mamp with osx. Still I believe the apache root is equivalent to drupal root. Any suggestions?

Dave McCormick's picture

I have to admit that my solution is very hacked up (although working nicely for this site).  I have my documentroot and DRUPALROOT set to "/var/www/html/drupal" and I set PATHTOUSERCONTENT to "/user-content".  There are quite a few places you need to update this->doc_root, so please check those.  Can you run firebug and see what the browser is doing whist it is spinning?  Whilst I was debugging this I found it useful to log the values of variables to a log file.  Have a look at http://codefury.net/2008/07/klogger-a-simple-logging-class-for-php/.

regards

Dave

Jürg Schulthess's picture

Thanks a lot for this article. I wasn't happy with the ckfinder licensing and this is a brilliant alternative.
I made one small change that might be helpful. Under 5. you detaul setting the PATHTOUSERCONTENT to a single directory in the drupal install root. In a multisite install this might be limiting. In a scenario where the sites live in subdirectories of sites (eg. sites/www.example.com) the user-content directory can be easily created in the site directory and configured in filemanager.config.php with:
define('PATHTOUSERCONTENT','/sites/'.$_SERVER['HTTP_HOST'].'/user-content');
That way every site has it's own set of files managed with c5filemanager.
Hope this is helpful. Thanks again for a very useful article.
Regards,
Jürg Schulthess