Tuesday, April 22, 2014

Trace IP with Gedmo IpTraceable behavior

As a strong user of Doctrine2 since I'm working with Symfony2, I also use Gedmo DoctrineExtensions. This library provides a great set of useful features we commonly need when developping web project :
  • Timestampable entities (created_at and updated_at columns on tables),
  • Sluggable entities,
  • Trace creators and modificators on contents (Blameable behavior)
  • ...
Since I more often work for sensitive e-commerce project, I needed a feature to trace IPs on some data (mainly orders and payments). So I decided to develop this functionality, based on existing work from Timestampable and Blameable in Gedmo DoctrineExtensions.

This is called IpTraceable and it is well explained in the Github repository documentation.

Symfony bundle implementation is under merge review in Stof DoctrineExtensionsBundle project, and you can use it with your own listener yet. Feel free to comment and add your support to  this PR on github : http://github.com/stof/StofDoctrineExtensionsBundle/pull/233

Tuesday, August 27, 2013

The need to state EntityManager name in a UniqueEntity validation constraint [Symfony2, Doctrine2]

I need to code a backoffice that manages many entities of my sites. Each of my sites has its own bundle with its own database, and a dependency to a common bundle with its own "common" database (for users, logging, billing).

For the same reasons, avoid to access default EntityManager from Container, without naming it... It could lead to an error if you embed your bundle in another application that states another EntityManager as default one...

Doctrine config for site#1

# Doctrine Configuration
doctrine:
    dbal:
        default_connection: site1
        connections:
            site1:
                ...
            common:
                ...
    orm:
        auto_generate_proxy_classes: %kernel.debug%

        default_entity_manager: site1
        entity_managers:
            site1:
                connection: site1
                ...
            common:
                connection: common
                ...

Doctrine config for site#2

# Doctrine Configuration
doctrine:
    dbal:
        default_connection: site2
        connections:
            site2:
                ...
            common:
                ...
    orm:
        auto_generate_proxy_classes: %kernel.debug%

        default_entity_manager: site2
        entity_managers:
            site2:
                connection: site2
                ...
            common:
                connection: common
                ...

Doctrine config for backoffice

# Doctrine Configuration
doctrine:
    dbal:
        default_connection: common
        connections:
            common:
                ...
            site12:
                ...
            site2:
                ...
    orm:
        auto_generate_proxy_classes: %kernel.debug%

        default_entity_manager: common
        entity_managers:
            common:
                connection: common
                ...
            site1:
                connection: site1
                ...
            site2:
                connection: site2
                ...

If in site#1 I handle this entity with UniqueEnity constraint, I need to state entityManager name if I don't want to get an error like "Call to a member function getClassMetadata() on a non-object" (Doctrine does not throw Exception Registry::getManagerForClass($class) gives no result).


namespace AlterPHP\Site1Bundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
 * AlterPHP\Site1Bundle\Entity\EntityForSite1
 * @ORM\Entity() *
 * @UniqueEntity(fields={"slug"}, em="site1")
*/
class EntityForSite1
{
    ...
}

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
      );
   }

}