Thursday, November 29, 2012

Symfony on non-standard ports

As Symfony (2.0) doc is not very clear on this subject, I spent some time to help a colleague on the following question : How to make HTTPS port different from 443 ?

Of course it's possible, and this is a simple configuration option in app/config.yml :

framework:

   ...

    # router configuration
    router:
        ...
        http_port:            81
        https_port:           1443

    ...

Official doc shows it in the Reference chapter, but not in table of indexes... Let's see http://symfony.com/doc/2.0/reference/configuration/framework.html#full-default-configuration

Tuesday, October 16, 2012

How to inhibit Soap Security headers "mustUnderstand" attribute in a Symfony2 controller

I never managed to make native PHP SoapServer class to handle "understand" soap:Security header as expected with mustUnderstand attribute. But some WS tools like SoapUI automatically set it to "true" or "1" and I needed to make a work around to avoid Soap Fault : "SOAP-ENV:MustUnderstand / Header not understood"

Here is a controller action that inhib mustUnderstand attribute :

   public function ordersFollowUpAction()
   {
      $request = $this->getRequest();

      //This deletes mustUnderstand attribute from raw request content
      $decodedRequest = preg_replace('/ ([-\w]+\:)?(mustUnderstand=")(1|true)(")/', '', $request->getContent());

      $server = new \SoapServer(__DIR__ . '/../Resources/public/wsdl/MyWsdl.wsdl');

      $server->setObject($this->get('my_webservice'));

      $response = new Response();
      $response->headers->set('Content-Type', 'text/xml; charset=UTF-8');

      try
      {
         ob_start();
         //This explicitely passes clear request content to the service handler
         $server->handle($decodedRequest);
         $response->setContent(@ob_get_clean());
      }
      catch (\Exception $e)
      {
         $this->get('logger')->err($e->getMessage());
         $response->setContent('An error occured, please contact our support service.');
      }

      return $response;
   }

How to read Soap WSSE headers

While following Symfony2 WSSE implementation tutorial, I was annoyed by the suggested WSSE headers interpretation. It only presents HTTP headers, not Soap WSSE headers...

Here is an extract of Symfony2 tutorial, handling HTTP headers :

        $wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/';
        if (!$request->headers->has('x-wsse') || 1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches)) {
            return;
        }

Obviously, it doesn't fit Soap headers that look like that :


  
   2012-10-10T12:00:36.099Z
   2012-10-10T12:05:36.099Z
  
  
   username@email.com
   E2wz/+mTabxWO37Hk6UeqSJF8+o=
   sAg8czv5GmW7rUKgqhm1Qw==
   2012-10-10T12:00:36.099Z
  
 

To read these headers, I use SimpleXML after having formatted the raw request content :

<?php

namespace AlterPHP\SoapBundle\Tools;

use Symfony\Component\HttpFoundation\Request;

/**
 * Description of WsseHeadersDecoder
 *
 * @author pece
 */
class WsseHeadersDecoder
{

   public function getHeaders(Request $request)
   {
      //HTTP headers (as described here : http://symfony.com/doc/2.0/cookbook/security/custom_authentication_provider.html#the-listener
      if ($request->headers->has('x-wsse'))
      {
         $wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/';
         if (1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches))
         {
            return false;
         }
         else
         {
            $username = $matches[1];
            $passwordDigest = $matches[2];
            $nonce = $matches[3];
            $created = $matches[4];
         }
      }
      //Classic SOAP headers
      else
      {
         //Clear XML namespace prefixes to handle with SimpleXML
         $decodedRequest = preg_replace("/(<\/?)([-\w]+):([^>]*>)/", "$1$3", $request->getContent());
         $xmlRequest = simplexml_load_string($decodedRequest);

         if (
            !isset($xmlRequest->Header->Security->UsernameToken->Username)
            || !isset($xmlRequest->Header->Security->UsernameToken->Password)
            || !isset($xmlRequest->Header->Security->UsernameToken->Nonce)
            || !isset($xmlRequest->Header->Security->UsernameToken->Created)
         )
         {
            return false;
         }
         else
         {
            $username = (string) $xmlRequest->Header->Security->UsernameToken->Username;
            $passwordDigest = (string) $xmlRequest->Header->Security->UsernameToken->Password;
            $nonce = (string) $xmlRequest->Header->Security->UsernameToken->Nonce;
            $created = (string) $xmlRequest->Header->Security->UsernameToken->Created;
         }
      }

      return array (
              'username' => $username, 'passwordDigest' => $passwordDigest, 'nonce' => $nonce, 'created' => $created
      );
   }

}

Monday, August 6, 2012

How to deal with asynchronous request with Symfony2 and other PHP frameworks

As well as most of PHP frameworks, Symfony2 manages user session by locking it during request treatment. It avoids concurrent access to session file but makes requests to be queued until session lock is freed...

This behaviour is really well explained in this post from Gareth McCumskey.

As Gareth describes his solution, we need to manually free session lock after all session modifications happen in the request treatment. Here is a pattern for controllers that handle AJAX calls (solution provided by Matt on stackoverflow) :

<php

    // ...

    public function myControllerAction()
    {
        //Actions that modify session
        // ...
     
        //We free session lock as no more session modifications will happen in next actions !
        $session = $this->get('session');
        $session->save();
        // session_write_close(); // Only required before Symfony 2.1
     
        //Actions that don't modify session
        // ...
     
        $this->return('...');
    }

Here is the result for an action that process a 10 seconds sleep() :
Before :
After :

Tuesday, June 26, 2012

I need help on groups validation !

I handle a multi-tab form that is validated by one group per form-tab. When I get errors while binding request, I'd like to know the first tab (ie validation group) that is not valid.

Is it possible with Symfony 2.0, and how ?

Please feel free to post your answer on StackOverflow, I will then publish it here. Thanks in advance !

Sunday, April 15, 2012

How to set a related entity with Doctrine2

When you want to set a related entity to another one, no need request the related object !

Doctrine ORM provides EntityManager::getReference($entityName, $id) method that only returns a reference to a record, not the entity itself... Let's see it in action :

<php

// ...

//This line gets the reference to the Comment record with primary key is #5
$comment = $em->getReference('AlterPHPDemoBundle:Comment', 5);

$article->addComment($comment);

// ...

Wednesday, April 11, 2012

Set up a specific log file for connections with Symfony2

Symfony2 basically embeds a full-featured logger, Monolog. A default implementation is available as 'logger' service. But you may need to log to a specifi file instead of traditional dev/prod/test.log... This article shows how to define a specific logger for connections.

I don't need to re-code a logger, the default one perfectly works. I just need to define a new file to log in, so a Monolog handler and to instanciate a new logger service based on Monolog Logger class. Let's have a look at this services.yml file :

parameters:
    connections_logs_path: %kernel.logs_dir%/connections.log

services:

    alterphp.connection.logger:
        class: Symfony\Bridge\Monolog\Logger
        arguments: [CONNECTION]
        calls:
            - [pushHandler, ['@alterphp.connection.logger_handler']]

    alterphp.connection.logger_handler:
        class: Monolog\Handler\StreamHandler
        arguments: [%connections_logs_path%, 200]

Note that connections_logs_path parameter can be directly set in app/configparameters.ini !

Then, let's see how I will use this logger to log users connections. I pass alterphp.connection.logger service in my AuthenticationSuccessHandler constructor (Read a past article about AuthenticationSuccessHandler implementation).

Service configuration file defining my AuthenticationSuccessHandler, services.yml :

services:

    security.authentication.success_handler:
        class: %security.authentication.success_handler.class%
        public: false
        tags:
            - { name: monolog.logger, channel: security }
        arguments:  ['@router', '@security.user.entity_manager', '@logger', '@alterphp.connection.logger']

And my AuthenticationSuccessHandler :

<php

namespace Acme\DemoBundle\Security\Http\Authentication;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
use Doctrine\ORM\EntityManager;
use Symfony\Component\HttpKernel\Log\LoggerInterface;

/**
 * Custom authentication success handler
 */
class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{

   private $router;
   private $em;
   private $logger;
   private $connectionLogger;

   /**
    * AuthenticationSuccessHandler constructor
    * @param RouterInterface   $router
    * @param EntityManager     $em
    * @param LoggerInterface   $logger
    * @param LoggerInterface   $connectionLogger
    */
   public function __construct(RouterInterface $router, EntityManager $em, LoggerInterface $logger, LoggerInterface $connectionLogger)
   {
      $this->router = $router;
      $this->em = $em;
      $this->logger = $logger;
      $this->connectionLogger = $connectionLogger;
   }

   /**
    * This is called when an interactive authentication attempt succeeds. This
    * is called by authentication listeners inheriting from AbstractAuthenticationListener.
    * @param Request        $request
    * @param TokenInterface $token
    * @return Response The response to return
    */
   function onAuthenticationSuccess(Request $request, TokenInterface $token)
   {
      //On logge spécialement les connexions à Sellermania (login principal)
      $logMess = 'UserID : ' . $token->getUser()->getId();
      $logMess .= '; Username : ' . $token->getUser()->getUsername();
      $logMess .= '; IP : ' . $request->getClientIp();

      $this->connectionLogger->info($logMess);

      return new RedirectResponse($this->router->generate('_home');
   }

And here is what we can read in app/logs/connections.log :


[2012-04-04 16:30:14] CONNECTION.INFO: UserID : 6523; Username : alterphp; IP : 127.0.0.1 [] []

Custom form validation constraint with Callback in Symfony2

Symfony2 forms provides many embedded validators to constrain data passed in form fields. Here is the way to define a constraint in a callback function, useful to determine a data validity relative to another field...

Given a form asking the user to chose its favorite payment ways, in a shopping application :
<?php

namespace Acme\DemoBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

/**
 * Selection of preferred payment ways form
 */
class PreferredPaymentWays extends AbstractType
{

   public function buildForm(FormBuilder $builder, array $options)
   {
      $builder
         ->add(
            'paypal', 'checkbox',
            array (
                 'label' => 'PayPal',
                 'required' => false,
            )
         )
         ->add(
            'cc', 'checkbox',
            array (
                 'label' => 'CreditCard',
                 'required' => false,
            )
         )
         ->add(
            'banktransfer', 'checkbox',
            array (
                 'label' => 'Bank Transfer',
                 'required' => false,
            )
         )
         ->add(
            'check', 'checkbox',
            array (
                 'label' => 'Personal Check',
                 'required' => false,
            )
         )
         ->add(
            'paypalEmailAddress', 'email',
            array (
                 'label' => 'PayPal email',
                 'required' => false
            )
         )
      ;
   }

   /**
    * Returns the name of this type.
    *
    * @return string The name of this type
    */
   public function getName()
   {
      return 'acme_demo_payment_ways';
   }

}

Monday, February 20, 2012

No underscores in subdomains !

After a few hours spent on pulling out my hair (typical french-speaking), I discovered that underscores in web subdomains are not RFC compliant, and that, for once, Internet Explorer strictly follows RFC recommendations ! No session persistance in IE with an underscore in your subdomain !