Code snippets for symfony 1.x

Navigation

Snippets by user J. Philip

Re-apply Symfony 1.2.7 security fix

This snippet tries to fix a problem that I had with Symfony deleting related data in a very sneaky way when you post an admin generated form.

The problem

This is related to the security fix posted here and later reversed here because it had side effects on custom partials among other things.

This happens when you have an admin generated form on a model having one or more many-to-many relationships and you don't want one or more of these relationships to appear in the form.
What you normally have to do is exclude the field xxx_list from the display section of the generator.yml file AND unset the field xxx_list in the configure method of the form.
Now it is very easy for people to forget that second part because they assume that Symfony been a DRY framework, you should not have to configure the same thing in 2 different places.

What happens if you forget to unset the field in the form is that Symfony creates a widget for the field but does not render it so that when the form is posted, it is the same as if an empty list had been posted for field xxx_list and Symfony will delete ALL many-to-many related records that may have existed.
I say this is sneaky because it happens in the background without any warning and your form does not show you any feedback of it because the corresponding widget is not rendered. By the time you realize it, you may have lost a lot of data.

If the security hole described in the links above has a lot of ifs attached to it, the data loss described here is only a matter of when it will happen if you forget to unset a many-to-many relation field.

The solution

This is only for Doctrine, but could be modified for propel.

This snippet re-applies the fix mentioned above, and allows its customization to limit it to just many-to-many relations.

You should still unset the the fields in the form as directed, but if you or another developer forgets, the data loss should not happen.

In the data directory of your application, create the sub-directories /generator/sfDoctrineModule/admin/parts.

Create there a file named configuration.php and replace its content with this snippet:

[?php
 
    /**
     * <?php echo $this->getModuleName() ?> module configuration.
     *
     * @package    ##PROJECT_NAME##
     * @subpackage <?php echo $this->getModuleName()."\n" ?>
     * @author     ##AUTHOR_NAME##
     * @version    SVN: $Id: configuration.php 12474 2008-10-31 10:41:27Z fabien $
     */
    class Base<?php echo ucfirst($this->getModuleName()) ?>GeneratorConfiguration extends sfModelGeneratorConfiguration
    {
    <?php $config = new ProjectConfiguration();
    $pluginConfig = $config->getPluginConfiguration('sfDoctrinePlugin');
    $doctrinePath = $pluginConfig->getRootDir(); ?>
 
    <?php include $doctrinePath.'/data/generator/sfDoctrineModule/admin/parts/actionsConfiguration.php' ?>
 
    <?php include $doctrinePath.'/data/generator/sfDoctrineModule/admin/parts/fieldsConfiguration.php' ?>
 
      /**
       * Gets a new form object.
       *
       * @param  mixed $object
       *
       * @return sfForm
       */
      public function getForm($object = null)
      {
        $class = $this->getFormClass();
 
        $form = new $class($object, $this->getFormOptions()); 
 
        $this->fixFormFields($form); 
 
        return $form; 
      }
 
 
      /**
      * Removes visible fields not included for display.
      *
      * @param sfForm $form
      */
      protected function fixFormFields(sfForm $form)
      {
        $fieldsets = $this->getFormFields($form, $form->isNew() ? 'new' : 'edit');
 
        // flatten fields and collect names
        $fields = call_user_func_array('array_merge', array_values($fieldsets));
        $names = array_map(array($this, 'mapFieldName'), $fields);
 
        foreach ($form as $name => $field)
        {
          if (!$field->isHidden() && !in_array($name, $names))
          {
            unset($form[$name]);
          }
        }
      }
 
 
      /**
       * Gets the form class name.
       *
       * @return string The form class name
       */
      public function getFormClass()
      {
        return '<?php echo isset($this->config['form']['class']) ? $this->config['form']['class'] : $this->getModelClass().'Form' ?>';
    <?php unset($this->config['form']['class']) ?>
      }
 
      public function getFormOptions()
      {
        return array();
      }
 
      public function hasFilterForm()
      {
        return <?php echo !isset($this->config['filter']['class']) || false !== $this->config['filter']['class'] ? 'true' : 'false' ?>;
      }
 
      /**
       * Gets the filter form class name
       *
       * @return string The filter form class name associated with this generator
       */
      public function getFilterFormClass()
      {
        return '<?php echo isset($this->config['filter']['class']) && !in_array($this->config['filter']['class'], array(null, true, false), true) ? $this->config['filter']['class'] : $this->getModelClass().'FormFilter' ?>';
    <?php unset($this->config['filter']['class']) ?>
      }
 
    <?php include $doctrinePath.'/data/generator/sfDoctrineModule/admin/parts/filtersConfiguration.php' ?>
 
    <?php include $doctrinePath.'/data/generator/sfDoctrineModule/admin/parts/paginationConfiguration.php' ?>
 
    <?php include $doctrinePath.'/data/generator/sfDoctrineModule/admin/parts/sortingConfiguration.php' ?>
 
      public function getTableMethod()
      {
        return '<?php echo isset($this->config['list']['table_method']) ? $this->config['list']['table_method'] : null ?>';
    <?php unset($this->config['list']['table_method']) ?>
      }
 
      public function getTableCountMethod()
      {
        return '<?php echo isset($this->config['list']['table_count_method']) ? $this->config['list']['table_count_method'] : null ?>';
    <?php unset($this->config['list']['table_count_method']) ?>
      }
 
      public function getConnection()
      {
        return null;
      }
    }
 

This file will serve as a template to generate the generator's base configuration class for each module in the cache.

It will replace the original one from sfDoctrinePlugin with these differences:

It will unset all fields in the form that are not in the display section of the admin config.

If you want to limit this to only many-to-many relationship fields, you can replace in the fixFormFields method:

if (!$field-&gt;isHidden() &amp;&amp; !in_array($name, $names))
 

with

 if (!$field-&gt;isHidden() &amp;&amp; !in_array($name, $names) &amp;&amp; $form-&gt;getObject()-&gt;getTable()-&gt;hasRelation(preg_replace(&quot;/_list$/&quot;, &quot;&quot;, $name)))
 

Do not forget to clear your cache: symfony cc

Note that if you want to disable or modify this functionality in a particular module, all you have to do is override the getForm() method of the module's generator configuration class found in the /lib directory of the module in the file my_moduleGeneratorConfiguration.class.php.

Note also that in the original fix, the fixFormFields method was in the sfModelGeneratorConfiguration which is the base call for all module's generator configuration and it is now repeated for each module, which is not ideal, but should not cause major problems.

by J. Philip on 2009-07-22, tagged admin  generator