Use tinymce in CakePHP with a behavior

Hey !
I went through the problem of using WYSIWYG to have more nicer news, comments … well giving a new possibility to the users to personalize what they want to write.

I tried two possibilities : making my own BBCODE or using a WYSIWYG editor. I will talk a “bit” about these two options, if you only want the explanations about how to set it for your CakePHP, skip it and go to this section Tinymce, HtmlPurifier and CakePHP behavior.

BBCODE
It is a very good way to be protected from xss or any other malicious content because you convert directly the bbcode into (x)html. Though, it’s a bit annoying to use/do and limits the possibilities. Also, one good point against is the fact that you have to “decode” (i.e to convert it in (x)html) each time you want to display it, if you do it once when you save the content, you can’t reverse the process, and you get (x)html when you want to edit your content. To be true, you could reverse it, but it’s not the wisest way.

WYSIWYG
Well, it is pretty much better here. We directly work with (x)html which is easier. We save directly (x)html in the database when we create our news, comment, … and we just display the content when we want, no process. BUT ! it’s plain (x)html we have and it means the users can write what they want, your layout can be broken, or worst they can write some javascript and mess your website.

Obviously I ended up by using a WYSIWYG editor, tinymce but as I said previously, you have some good advantages, the good looking, the ease of use but a vulnerability : xss attack. So the goal is to prevent it and secure the content the users will write. There we won’t use htmlentities because the goal is to “execute” the html by the browser, so we have to get some tags, and get rid of the others. We could use strip_tags, but let’s see it’s not the safer way.

Let’s admit we allow the tag <p>, here is what we could do.

    $data = "<p onmouseover=alert("xss")>blablabla</p>";
    $data = strip_tags($data, '<p>');
    echo $data;

Put your mouseover the <p> tag (i.e blablabla) and you will see that “xss” pop up. Well it’s logic because we allow the <p> tag, would be the same with <span> and so on. It means we can allow some tags but not be totally safe.

HtmlPurifier

So let’s use HtmlPurifier, it’s a filtering PHP library which “purifies” your html, it even deals with malformed tags and follows the specifications ! It’s THE massive weapon ! You can do a whitelist of the acceptable tags and even decide which attributes of these tags are allowed. Well I discovered it few days ago, so I’m no expert but we’ll get down to some configuration.

Tinymce, HtmlPurifier and CakePHP behavior

I’ll explain here how to set tinymce and htmlpurifier with CakePHP using a behavior. First we need a copy of tinymce, you can find it on their website : tinymce, take the latest production version. Then you can put it in your webroot/js or you create your own folder “lib” in the webroot, do as you wish it doesn’t matter as long as it is in the webroot. Next step is to configure the tinymce editor to fit our needs. Create a file “config.js” in your tinymce folder and put what follows into.

 
tinyMCE.init
(
    {
        // General options
        mode : "textareas",
        theme : "advanced",
        plugins : "paste",
        width : "100%",
        height : "100%",

       // Theme options
       theme_advanced_buttons1 : "bold,italic,underline,strikethrough,forecolor,|,justifyleft,justifycenter,justifyright,justifyfull,|,bullist,numlist,|,undo,redo",
       theme_advanced_toolbar_location : "top",
       theme_advanced_toolbar_align : "left",
       theme_advanced_statusbar_location : "bottom",
       theme_advanced_resizing : false,

      // Paste plugins configuration
      paste_auto_cleanup_on_paste : true,
      paste_remove_styles: true,
      paste_remove_styles_if_webkit: true,
      paste_strip_class_attributes: true,
      paste_retain_style_properties : "none",
      theme_advanced_disable : "styleselect",
      convert_fonts_to_spans: false,

      // No css
      content_css : false
    }
);

Here what’s important is the field “theme_advanced_buttons1” which is the list of the tags we display in our editor, so the ones we let our users use. It has to fit with the configuration of HtmlPurifier we will do just after. What we accept is basic things such as list, text position, underline, bold, etc …

P.S : I added the plugin “paste”, very useful when u copy/paste stuff in the text editor, there it will clean the content, or keep what’s allowed.

P.S2 : Tinymce has a lot of parameters, you can really set a lot of things. There I show a “little” example.

Now HtmlPurifier ! First, we need the latest copy that we can download here. I decided to wrap it in a CakePHP behavior.  Why? Because I think it’s “smarter”, when we add/edit a comment, post, … we purify the content before to save it in the database. If you read well, you saw before and save. And yes it’s a callback method of the CakePHP model. Consequently, we will create a behavior that we will link to all our models that need it and through the beforeSave callback, we will purify.

Thus, let’s put our HtmlPurifier folder in the vendors. Rename your folder “HtmlPurifier-blabla” to only “htmlpurifier”, it’s just more convenient.

We can create a behavior in app/Model/Behavior, under the name of PurifyBehavior.php, and it should look like this.

 
class PurifyBehavior extends ModelBehavior
{
    function setup(Model $model, $settings = array())
    {
        //field is the field we purify by default it is called content
        $field = (isset($settings['field']))?$settings['field']:'content';
        $this->settings[$model->alias] = array('field' => $field);
    }

    /*
        cleaning before saving
    */
    function beforeSave(Model $model)
    {
        //convenient to get the name of the field to clean
        $field = $this->settings[$model->alias]['field'];

        //check if we are working on the field
        if(isset($model->data[$model->alias][$field]))
        {
            //get htmlpurifier => http://htmlpurifier.org/
            App::import('Vendor', null, array
            (
                'file' => 'htmlpurifier'.DS.'library'.DS.'HTMLPurifier.auto.php'
             ));

            //set its configuration
            $config = HTMLPurifier_Config::createDefault();
            $config->set('Core.Encoding', 'UTF-8');
            $config->set('AutoFormat.RemoveEmpty', true);
            $config->set('Core.EscapeNonASCIICharacters', false);

            /*
            allowed HTML elements according to tinymce's configuration => p[align] to allow p and the attribute align
            */
            $allowedHTML = "em,strong,p[style],ul,ol,li,span[style]";
            $config->set('HTML.Allowed', $allowedHTML);

            //cleaning
            $purifier = new HTMLPurifier($config);
            $model->data[$model->alias][$field] = $purifier->purify($model->data[$model->alias][$field]);
        }

        return true;
    }
}

We use the setup method to tell the behavior which field we want to purify, let’s say you do a news, you call the content field “body” and if you do a media, you could call it “description”, it’s why I did it this way. We will see how to set it in the model below. By default, the field value would be “content”. Then we use the callback beforeSave to purify. First we put in a variable the name of the field, it’s “more readable”. We check that the field exists. Why? For one good reason. The callback is fired each time you save something. Let’s say you want to save one field only with saveField, the behavior will fail because the field of the PurifyBehavior will not be set. We import the htmlpurifier library from the vendors.  Then create a default configuration. We set few parameters, encoding, removeEmpty (let’s say you have a span looking this <span></span>, htmlpurifier will erase it), etc … check the documentation for more parameters. Important there is the allowedHTML where we write all the tags and theirs attributes according to tinymce’s previous configuration ! It’s pretty easy to understand and to change, right? Finally we purify our field of the current model. Ensure of returning true or the save will fail !

P.S : You can of course add some conditions to check and fail the save by returning false. You may use $model->invalidate(‘name of the field’, ‘message to display’) to invalidate the field in your form and display an error message.

Last but not least, we have to do a small modification of all our models using the behavior. To tell them they have to behave with the purifybehavior. Let’s see how to do this, it’s the easiest and smallest part.

    public $actsAs = array('Purify' => array('field' => 'description'));

Pretty easy ! We just tell our model to use the PurifyBehavior and we set the field to “description” here. If your field’s name is “content” you don’t even have to set the array ! It would give something like this.

    public $actsAs = array('Purify');

Et voilà, you have proper html saved in your database, your users are less limited and your website is safer.

P.S : I didn’t say totally safe !

Using pass parameters with the Paginator component

CakePHP’s paginator is a great component allowing you to paginate what you want.

Though, I went through a “problem”. I wanted to make custom routes and I didn’t find how to set the paginator to use the pass parameters (example : /posts/index/1) instead of the named params (example : /posts/index/page:1) or the query string (example : /posts/index/?page=1). The problem was occuring to get the links using the methods next and prev of the paginator in the view, always giving me named parameters or query string.

My aim was to write a route like this /news-page-1 where 1 is the page number, and a pass parameter !

I did a small hack providing a custom route with pass parameters. This problem is maybe due to my lack of knowledge concerning this component, but I couldn’t find anything to use pass parameters for it.

We will see how to do it through an example with Post.

PostsController

 
   //using the Paginator component
   public $components = array("Paginator");

   //index method
   public function index($page = 1)
   {
        $this->Paginator->settings = array
        (
            'Post' => array
            (
                'limit' => 2,
                'page' => $page,
                'conditions' => array
                (
                    'published' => 1
                ),
                'order' => array
                (
                    'created' => 'DESC'
                ),
            )
        );

        $posts = $this->paginate();           
	$this->set('posts', $posts);
    }

It’s pretty much easy to see what we are doing here. We limit the number of Posts on each page to 2, we want only the published posts and we want them ordered by created desc. I added the $page variable which is there to say which page we are looking for.

Let’s deal with the custom route we wanted, remember? We want /news-page-1 (feel free to do the route you want :p)

app/Config/routes.php

    //setting the route with the pass parameter page
    Router::connect
    (
        '/news-page-:page', 
        array('controller' => 'posts'), 
        array
        (
            'pass' => array('page'), 
            'page' => '[0-9]+'
        )
    );

Let’s see how to generate the link for the view.
View/Posts/index.ctp

    //put this where you want the previous link to be            
    if($this->Paginator->hasPrev()):

        //grab the current page number           
        $page = $this->params['paging']['Post']['page'];
        
        //decrement to get the previous page
        $page--;

        //generating the link
        //giving $page as a pass parameter
        echo $this->Html->link
        (
            '← Previous', 
            array
            (
                'controller' => 'posts', 
                'action' => 'index', 
                'page' => $page
            ),
            array('escape' => false)
        ); 

    endif;

You can notice we don’t use the prev method of the paginator to generate the link. You can also write the next link, the same way than for the previous, but use the hasNext method to check if there is a next page and increment $page. Also change the name of the link 😉

Et voilà we have a nice url using pass parameters with the paginator component.

P.S : feel free to give feedback about the paginator, if you found a way to get it working with pass parameters, thanks.

Make your own in_array to go deeper

The function in_array allows you to search a value inside an array, but the problem is that it is not a recursive function, it means that if you have an array into an array, an the value you are looking for is inside it, the function will fail finding it.

Example

//our array in an array with its first value 0
$array = array(array(0));

//searching 0 in the array
if(in_array(0, $array))
echo 'ok';
else
echo 'no';

and of course it fails.

So the best is to make our own in_array function to be sure to match all the time, we do need recursion !

//I have taken the same prototype as the in_array function

function r_in_array($needle, $haystack, $strict = false)
{
static $found = false;

foreach($haystack as $line)
{
if(is_array($line))
r_in_array($needle, $line, $strict);
else if($line == $needle)
{
if($strict)
{
if(gettype($line) === gettype($needle))
$found = true;
}
else
$found = true;
}
}

return $found;
}

This function calls itself to find, if there is, the value ($needle) in the array ($haystack) you give it. I added the $strict which checks if the type is the same when set to true.

Now with examples

//well we want to be sure recursion works :p
$array = array(array(0, 2, array(array(array(array(1, array(4)))))), array(3), 0);

//searching 4 (integer) in $array
if(r_in_array(4, $array))
echo 'found';
else
echo 'not found';

It works !

//searching 4 (string) in $array
if(r_in_array("4", $array))
echo 'found';
else
echo 'not found';

Works too !

//searching 4 (integer) in $array
if(r_in_array("4", $array, true))
echo 'found';
else
echo 'not found';

It doesn’t work because we use the strict mode.

Voilà, you can use this function as you wish, enjoy 🙂