Validando una petición CORS de AMP con Symfony

Recientemente he estado desarrollando una web para una startup dedicada al sector de la organización de eventos para empresa y dado que las secciones apenas tenían complicación me animé a hacerla completamente en AMP con el fin de seguir familiarizándome con esta librería.

Además, decidí usar Symfony ya que varias secciones dependen de contenido creado por medio de un CMS y gracias a que la versión 4 es bastante ligera no le vi mayor problema en meter de nuevo mi framework favorito de PHP.

Dicho esto, el desarrollo fue bastante stándard (Symfony + Webpack Encore + Sonata Admin) hasta llegar a la parte del formulario de contacto, el cual podéis ver aquí:

Básicamente, la complicación a la hora de crear un formulario con AMP es que estas páginas una vez que son indexadas por Google, no se sirven desde nuestro dominio, sino que son enviadas desde un CDN de Google donde éste almacena nuestras páginas, generalmente alguno de estos 3 (notad como convierten el dominio):

- https://eventarte-es.cdn.ampproject.org

- https://eventarte.es.amp.cloudflare.com

- https://cdn.ampproject.org

Dado que en AMP el submit de los formularios se realiza mediante una llamada AJAX, os podéis imaginar el problema: CORS.

Por tanto, es necesario añadir un poco de código en la parte del servidor de cara a que el formulario pueda ser enviado correctamente desde los CDN de Google. Vamos a ello!

Creando el formulario

El formulario de contacto sobre el que trabajaremos tiene la siguiente pinta:

Y su template es algo parecido a ésto:

Como veis, añado al formulario el atributo action-xhr especificando la URL absoluta contra la que se enviarán los datos. Hasta aquí, nada nuevo que no aparezca bien en la documentación propia de AMP Project.

Validando la petición

Ahora es donde viene la parte interesante. Supongamos que alguien ha llegado a nuestra página de contacto por medio de un resultado de búsqueda en Google y que, por tanto, el dominio en el que se encuentra es:

https://eventarte-es.cdn.ampproject.org/contacto

Cuando rellene el formulario, la URL contra la que se se enviarán los datos será:

[POST] https://eventarte.es/contacto

Y si no lo hemos configurado correctamente, dará un fallo de CORS. Veamos cómo solucionarlo.

Para que resulte más sencillo explicarlo, os dejo el código de un servicio de Symfony que se encarga de generar una Response adecuada en el caso de que la validación de los CORS sea correcta. A continuación, explicaré los puntos más importantes:

Lo primero de todo, el constructor recibe el servicio RequestStack con el fin de poder acceder a la request en curso (igual valdría también pasársela como argumento al método) y los siguientes parámetros:

  • allowedOrigins , es decir, un array en yml con todos los dominios aceptados para enviar peticiones CORS. En nuestro caso:
allowedOrigins:
  - https://domain.es
  - https://domain-es.cdn.ampproject.org
  - https://domain-es.amp.cloudflare.com
  - https://cdn.ampproject.org
  • allowedSourceOrigin , es decir, el dominio base, en nuestro ejemplo:
allowedSourceOrigin: https://domain.es

Dicho esto, el método validateRequest primero comprueba si nos fue enviado el parámetro __amp_source_origin , parámetro que contiene el dominio asociado al CDN desde el que está navegando el usuario, en nuestro caso el valor de __amp_source_origin debería ser https://domain.es .

Y comienzan las comprobacaciones.

  • Primero comprueba si la petición llevaba la cabecera amp-same-origin , la cual sirve para indicar que la petición se está enviando desde el mismo dominio y no desde un CDN (de ahí que su valor sea TRUE o FALSE ), por lo que la petición es válida.
  • Si no, se comprueba que el contenido de la cabecera ORIGIN sea un dominio incluido dentro de nuestro array allowedOrigins y que el valor del parámetro GET __amp_source_origin sea el de nuestro dominio ( allowedSourceOrigin , es decir, https://domain.es). Si es así, la petición es válida.

En el caso de que no se cumplan ninguna de las dos condiciones anteriores devolveremos un código de error indicando que la validación CORS no tuvo éxito (en mi caso estoy devolviendo un HTTP_FORBIDDEN .

Si la validación fue correcta, devolveremos las 4 cabeceras indicadas en el código teniendo en cuenta que:

  • Access-Control-Allow-Origin llevará el dominio desde el cual se envió la petición
  • Y AMP-Access-Control-Allow-Source-Origin llevará nuestro dominio original.

Procesando el formulario

Gracias al servicio anterior, nuestro controlador encargado de gestionar el formulario deberá tener un aspecto similar al siguiente:

public function contact(
Request $request,
EntityManagerInterface $em,
AmpService $ampService
) {
  $contact = new Contact();
  $form = $this->createForm(ContactFormType::class, $contact);
  $form->handleRequest($request);
  if ($form->isSubmitted() && $form->isValid()) {
    $response = $ampService->validateRequest();
    if ($response->getStatusCode() == Response::HTTP_FORBIDDEN) {
      return $response;
   }
   .....

Es decir, emplearemos el servicio AMPService para validar los CORS de la llamada y poder recibir formularios correctamente desde nuestras páginas AMP.

Espero que este artículo os sirva para acortar trabajo si tenéis que implementar alguna vez un formulario sobre AMP y si queréis darle una vuelta a la web sobre la que he preparado este artículo, he tenido la suerte de ver algunos de los eventos que organizan y la verdad es que trabajan muy muy bien.