Your own basic cakephp “file manager” for CK editor

Who hasn’t faced this issue at least once? There are a lot of great online html editors out there (I really like CKEditor), each one with his solution for file and image upload and management. Often, it’s not free.

Sometime you don’t need a full-blown file manager. Sometime it’s simply better to give the user a limited set of choices, and prevent him from messing with: image resize, link to original version or downloadable file, styling, or using deprecated properties to align inside the surrounding text.

Let’s see how a cakephp rookie and javascript beginner used cake and the new ck editor api to implement this feature.

This is a 3 or 4 parts series  (we’ll see) covering:

1)      Attached files.
Inject your customized html in ckeditor (while preventing the user from providing broken links and such)

2)      Insert images.
Not too many choices  and options! Just implement in your image browser the ability to insert a small thumbnail -aligned left or right-, or a medium centered thumbnail. Oh, and with the link to the original image

3)      and  4) Webservices. Use some webservice api to implement external media injection (i.e youtube clip, or flickr imageset). Well, it’s easier than it sounds.

– Basic filebrowser

All I want is a simple popup listing saved media/files and a way to insert a link for downloading the selected media- file in the current text inside a ck editor “textarea”. (well, it actually is a javascript object replacing the textarea, but let’s call it this way) .

In my content editing view  (that calls the “file browser”) I have something like:

contents.ctp element:

  1. <?php
  2. $javascript->link('ckeditor/ckeditor', false); // started this with cakephp 1.2, so I’m still using javascript and ajax helper..
  3. ?>
  4.  
  5. [..]
  6.  
  7. <?php
  8. echo $form->input('summary', array('type' => 'textarea'));
  9.  
  10. // [..]
  11. echo $html->link($html->image('icons_big/attachment.png',
  12. array('align' => 'absmiddle'))
  13. . __(' Files ', true),
  14.  
  15. 'javascript:;',
  16.  
  17. array('escape' => false ,
  18.  
  19. 'onclick' => "javascript:window.open('".$html->url(array(
  20.  
  21. 'controller' => 'assets',
  22.  
  23. 'action'=>'filebrowser',
  24.  
  25. $Type.'Summary' // opener instance
  26.  
  27. // $Type is the model name (this element is used by different controllers and actions), so this is the rendered form helper field name / id; i.e. NewsSummary, EventSummary, PageWholeContent
  28.  
  29. )
  30.  
  31. )."','_blank', 'toolbar=0,scrollbars=1,location=0,status=1,menubar=0,resizable=1,width=800,height=680'); return false;"
  32. ),
  33. null
  34. );
  35.  
  36. // [..]
  37.  
  38. ?>
  39.  
  40. <script type="text/javascript">
  41.  
  42. var editor = CKEDITOR.replace( "<?php echo $Type ?>Summary" , {customConfig : "/js/ckeditor/alternate_config.js", height : "150px"});
  43.  
  44. CKEDITOR.add
  45.  
  46. CKEDITOR.config.contentsCss = '<?php echo $html->webroot('/js/ckeditor/mycontents.css') ?>' ;
  47.  
  48. </script>

We have our ck editor instance and the link for the popup.

The Model

Nothing special here, really. It depends on your needs. I have this Assets model which essentially sets up the configuration for the media and transfer behavior: I’m using David Persson’s Media Plugin (http://github.com/davidpersson/media) v 0.60 with some tweaks.
Any file upload / image resize code will work.

I like to store the file info in the database (and so Media plugin does). It’s not strictly mandatory for this proof of concept, but I’m using it because it’s much more easier (and fast) to use a single db table for searching saved files than scanning a tree in the filesystem.

The Controller

Here is a simplified version of the actual controller/action I’m using.

Just for reference here is the controller declaration and some properties. More on the basic settings later

assets_controller.php:

  1. <?php
  2.  
  3. class AssetsController extends AppController {
  4.  
  5. var $name = 'Assets';
  6.  
  7. var $helpers = array("Html", "Form", "Javascript", "Ajax", "Time", "Text", "Utility", "Media.Medium", "Number");
  8.  
  9. var $components = array('Auth', 'Cookie', 'Toggle', 'Session', 'RequestHandler','Flickr');
  10.  
  11. var $paginate = array('contain' => array(
  12. 'Site' => array('fields' => array('title', 'slug')),
  13. 'Content'=> array('fields' => array('title','slug','publish')))
  14. , 'order' => 'Asset.id DESC'
  15. );
  16.  
  17. ?>

Here is the action that will handle the popup for searching / uploading / selecting files to embed.

Still nothing special here, just note the $opener_instance parameter: it’s used to identify the editor object calling the popup; it’s possible to have different instances of ck editor on the same page.

In AssetsController.php:

  1. <?php
  2.  
  3. /**
  4. * file browser, renders the (filtered) popup list of files for the current site
  5. * @param <string> $opener_instance, name of the field / ckeditor instance
  6. * that will get the injected html for image display.
  7. * You can use multiple ck editor instances on teh same page
  8. */
  9.  
  10. function admin_filebrowser($opener_instance,$type='all') {
  11.  
  12. if (isset($this->params['named']['type']))
  13. $type = $this->params['named']['type'];
  14.  
  15. if($this->admin_index($type, true)) {
  16. $this->set('opener_instance', $opener_instance);
  17. $this->render('admin_filebrowser', 'basic');
  18. }
  19.  
  20. }
  21.  
  22. ?>

The action just passes the needed values to the admin_index function and sets the variables for the view.

Admin_index is our work horse. It’s both a normal action in the usual admin screen, and a function returning the filtered/paginated file records to our popup filebrowser action. (and imagebrowser too, but this is another post).

In assets_controller.php:

  1. <?php
  2.  
  3. /**
  4. * function admin_index
  5. *
  6. * displays a filtered (by type, search terms, site) lists of files;
  7. *
  8. * used as an action for assets management, or returns data for
  9. * file/imagebrowser used in ckeditor
  10. *
  11. *
  12. * @param <string> $type - type of media
  13. * @param <boolean> $return false, it's the normal asset controller action,
  14. * true, sets data for the popup ckeditor "fielbrowser" and skips
  15. * admin_index view rendering
  16. * @return <boolean>
  17. */
  18.  
  19. function admin_index($type = null, $return = null) {
  20.  
  21. $this->Asset->recursive = 0;
  22.  
  23. // [..]
  24. // all logic for filtering records
  25.  
  26. $this->Asset->contain($this->defaultContain);
  27.  
  28. $assets = $this->paginate(null,$filter);
  29.  
  30. $assets = Sanitize::clean($assets);
  31.  
  32. $this->set('assets', $assets);
  33.  
  34. //[..]
  35. // if it's called by imagebrowser or filebrowser
  36.  
  37. if($return) return true;
  38. // else render index view..
  39.  
  40. }
  41. ?>

I’ve stripped out the not relevant parts (the code for preparing filter / pagination). What is left is a simple get that data function. It’s the view where all the magic happens.. well, it’s really simple.

I didn’t even look deeply into the ck editor javascript api or developer guide. To keep it as simple as possible I only needed a function for inserting formatted html into the current ck editor “textarea”.

In admin_filebrowser.ctp:

  1. <script type="text/javascript">
  2. <!--
  3. function InsertHTML(passed)
  4. {
  5. //get the correct editor object instance (in my case, i.e. if news controller: NewsSummary or
  6. //NewsWholeContent, if events controller -> EventsSummary.. etc. )
  7.  
  8. var oEditor = opener.CKEDITOR.instances.<?php echo $opener_instance ?>;
  9.  
  10. // Check the active editing mode.
  11.  
  12. if ( oEditor.mode == 'wysiwyg' )
  13. {
  14.  
  15. // Insert the desired HTML.
  16. oEditor.insertHtml( passed ) ;
  17.  
  18. }
  19. else
  20. alert('<?php echo __('You must be on WYSIWYG mode!', true); ?>') ;
  21.  
  22. window.close();
  23. }
  24. -->
  25. </script>

That’s the point. Calling this javascript snippet will insert the desired html in the correct ck editor box. Now all we need to do is to generate the correct html (filling the js “passed” parameter for every file).

(Still stripping all the non relevant code –pagination filtering, file data display based on file type, thumbnail or type icon / bells and whistles)

In admin_filebrowser.ctp:

  1. <?php
  2. foreach ($assets as $asset):
  3. ?>
  4. <tr>
  5. <td>
  6. <?php echo $asset['Asset']['id']; ?>, <strong><?php echo $asset['Asset']['name']; ?></strong>
  7. </td>
  8. <td>
  9. <?php
  10. // HERE WE ARE
  11. // generated HTML to be embedded in CKeditor
  12. $insert = $html->link(
  13. $html->image('icons_small/attachment.png',
  14. array('width' => 14, 'height' => 14, 'align' => 'absmiddle',
  15. 'alt' => __('attached file', true),
  16. 'title' => __('attached file', true),'border' => 0
  17. )
  18. ) . ' ' . __( $asset['Asset']['name'], true),
  19. array('controller' => 'assets', 'action' => 'show',$asset['Asset']['id'], 'admin' => false),
  20. array('escape' => false),null,false
  21. );
  22.  
  23. //then the javascript lnk that will inject the generated html in ck editor
  24.  
  25. echo $html->link(
  26. $html->image('icons/attachment.png', array('alt' => 'allega', 'title' => 'allega', 'border' => 0, 'align' => 'absmiddle')) .
  27. $html->image('icons/link.png', array('alt' => 'allega', 'title' => 'allega', 'border' => 0, 'align' => 'absmiddle')) .
  28. __('Embed this file', true),
  29.  
  30. //'javascript:;',
  31. 'javascript:InsertHTML(\''. $insert .'\');',
  32.  
  33. array('escape' => false,
  34.  
  35. //'onclick' => 'javascript:InsertHTML(\''. $insert .'\');'
  36. ),
  37. null, false
  38. );
  39. ?>
  40.  
  41. </td>
  42. </tr>
  43. <?php endforeach; ?>

That’s it. It’s all we need to inject what we want in the editor.  In this case, a link showing the file name and pointing to the show action (an action that uses cakephp media view to display / download the file)

assets_controller .php

  1. <?php
  2. function show ($id) {
  3. $this->view = 'Media';
  4. $this->Asset->recursive = -1;
  5.  
  6. $asset = $this->Asset->find('first', array('conditions' => array('Asset.id' => $id)));
  7.  
  8. //ClassRegistry::init('Inflector');
  9.  
  10. $name = Inflector::slug(substr($asset['Asset']['name'], 0, -3));
  11.  
  12. $download = false;
  13.  
  14. if($asset['Asset']['medium_type'] != 'img') $download = true;
  15.  
  16. $params = array(
  17. 'id' => $asset['Asset']['basename'],
  18. 'name' => $name,
  19. 'download' => $download,
  20. 'extension' => substr($asset['Asset']['basename'], -3),
  21. 'path' => 'media' . DS . $asset['Asset']['dirname'] . DS,
  22. 'cache' => true
  23. );
  24.  
  25. $this->set($params);
  26.  
  27. $this->render();
  28. }
  29.  
  30. ?>
file browser popup

On the image is a screenshot of the actual file popup.

(yes, I need a designer, I really do. But I have no money to hire one, and this is not the point of this article. Well, the template of this blog itself is made with artisteer, in 30 minutes. Be kind)

If someone is interested I’ll post the whole relevant code. The app will eventually be released as open source (when it’s ready, cleaned code, and I’ll figure out which license to use).

Stay tuned for the next issues on this series (featuring image, youtube and flickr browser)

And, as always: I’m not a cakephp master – surely there are better ways and cleaner code to do this – suggestions are welcome.