sábado, 28 de abril de 2012

Crear un RSS con CakePHP 1.3

Hace tiempo escribí un tutorial sobre cómo generar feeds RSS en CakePHP pero ya está muy desactualizado. Así que ahí va otro un poco más moderno, que vale para la versión 1.3. Tampoco es que esté muy actualizado, ya que aún no me he planteado migrar a CakePHP 2, pero bueno, espero que  pueda ser útil.

Además, como bonus, intentaré explicar cómo voy resolviendo el tema de que el feed sea validado en el feed validator del W3C, cosa que tiene su dificultad ya que la especificación RSS es un tanto laxa en algunos aspectos.


Paso 1: preparar la aplicación Cake para responder a las peticiones de feeds

Básicamente hay que decirle a CakePHP que queremos parsear urls con extensiones, de la forma: /controller/action.rss. Esto es así porque la versión 1.2 de CakePHP puede seleccionar al vuelo diferentes vistas en función de la extensión que le pasemos. Por lo tanto, en un paso posterior crearemos una vista específica para el feed y la pondremos en el sitio adecuado para que la use Cake.

En tu /app/config/routes.php tienes que añadir:


Router::parseExtensions('rss');

Esto le indica a Cake que si se encuentra una URL con la extensión 'rss' busque las vistas en la carpeta /views/models/rss. De este modo, puedes tener una misma acción que responde de manera diferente a una petición html o a una petición de rss.

Podemos no especificar ninguna extensión, para que parsee cualquiera, o bien dar una lista limitada de ellas 'rss', 'xml'...

Otra ventaja es que CakePHP seleccionará automáticamente un layout específico para rss, que podemos sobreescribir en /views/layouts/rss/default.ctp, o crear otros alternativos con otros nombres. En ese último caso hay que recordar poner en la acción del controlador un paso $this->layout = 'nombre del layout alternativo'; para que la aplicación sepa cuál queremos usar.

Paso 2: cosas para hacer en el Controller

Hay varias, así que vayamos por partes.

Lo primero es habilitar el uso del componente RequestHandler en el controlador. Aparte de para este tema de los feeds es útil siempre que necesites obtener información sobre la petición que está recibiendo el controlador. Lo segundo es indicar que quieres utilizar el RSSHelper, claro.

var $components = array('RequestHandler');
var $helpers = array ('RSS');

Una cosa opcional: El componente HandleRequest te permite comprobar si el usuario está pidiendo un RSS (u otra cosa) y actuar en consecuencia. Por ejemplo, si es para el feed obtener sólo los últimos 15 items que cumplan ciertas condiciones y si no obtener todos. En cualquier caso, usarías:

if ($this->RequestHandler->prefers('rss')) {
    // Cosas para hacer si es rss
} else {
    // Cosas para hacer si no lo es
}

Lo segundo es enviarle los datos a la vista del feed (que aún no hemos creado, ¡ojo!). Lo puedes hacer así:

Configure::write('debug', '0');
$data = $this->Paginate ();
$this->set('channelData', array ('title' => 'Ejemplo',
  'link' => 'http://localhost:8888/micake/',
  'description' => 'Frases y citas célebres para que las puedas leer'
));
$this->set ('frases', $data);

Lo explico un poco:

La primera línea es para desactivar el Debug (en el caso de que lo tengas distinto de 0, como ocurre en un entorno de pruebas o de desarrollo). Con Debug, Cake añade cosas a la salida generada y los feeds no validarían de ninguna forma aunque estuviesen bien construidos. Opcionalmente puedes poner un if para controlar si es necesario hacerlo o no.

La segunda línea es para recabar los datos de la manera habitual.

La tercera línea ajusta los valores para el channel. El layout por defecto para feeds rss espera que definas una variable $channelData como un array con claves que serán los elementos del channel (como title, link, description y los demás). Si no la defines, CakePHP se busca la vida para poner algunos. Sin embargo, puede que en la vista tengamos que hacer algunos ajustes a esta información antes de enviarla al Layout.

La cuarta línea pasa el array de datos que serán los ítems del modelo. En este caso, la variable es $frases, pero se supone que esto ya sabías hacerlo. Aquí no vamos a hacer nada más.

Paso 3. Preparando los datos

Los feeds rss tienen una estructura relativamente sencilla, puedes consultar la especificación para tener todos los detalles. Cada uno lleva la información de un canal (que puede ser un blog), el cual puede contener varios ítems (cada una de las entradas del blog). Cada uno de estos elementos puede contener otros, como descripción, autor, link y algunos más. Según el caso puede que tengas la información del canal en un modelo, y la de los ítems en un modelo relacionado.
Canal

El canal debería proporcionar como mínimo los siguientes campos:
  • title: El título o nombre del canal
  • description: Un texto breve que describa o resuma el contenido del canal
  • link: El enlace a la página principal del canal
Aparte de eso, hay un par de campos que son recomendables:

docs: Un enlace a la especificación de RSS (Yo uso http://blogs.law.harvard.edu/tech/rss)
atom:link: Este elemento está recomendado para mejorar la interoperabilidad del feed con las aplicaciones lectoras.

$channel['atom:link'] = array(
     'attrib' => array( 
             'href' => Router::url(null, true), 
             'rel' => 'self', 
             'type' => 'application/rss+xml' 
));

Este elemento contiene varios atributos y, como puedes ver, se indican en forma de array asociativo. Básicamente href es un enlace al propio feed, que podemos obtener fácilmente usando la clase Router, y pasándolo el parámetro nulo para que utilice la url que genera el propio feed.

En mi caso particular, he añadido estos campos en el layout /views/layouts//rss/default.ctp ya que se generan igual para cualquier feed de mi aplicación y, de este modo, me puedo olvidar de ellos en cada vista concreta.

Otra cuestión que hay que controlar es que ni title ni description deberían contener HTML. Respecto a title, suele ser normal tener un campo de título de texto puro. Pero en description puede ocurrir que los usuarios añadan algo de HTML si el sistema lo permite, como es mi caso. Lo mejor es utilizar la función de php strip_tags, que realiza el trabajo sin más problemas:

$channel['description'] = strip_tags($channel['description']);

Esta transformación también la hago en la vista (no en el controlador) porque se refiere al modo en que se va a mostrar la información en el documento final.

Items

Los items se han de pasar a la vista en forma de array de arras a partir de datos del modelo que corresponda. El proceso  de los items en la vista se realiza mediante un bucle foreach, y la tarea que hay que hacer con cada ítem es generar correctamente los elementos hijos necesarios:
  • title
  • description
  • link
  • guid
  • pubDate
  • author
  • enclosure
RSSHelper incluye un método que nos devuelve un elemento ítem correctamente formado si le pasamos un array asociativo con estas claves.

Vamos a ver ahora qué tipo de contenido tiene cada clave y como habría que prepararlo.

Title es una cadena de texto puro, así que no debería tener más complicaciones. Usa strip_tags si es necesario.
Description sería un resumen del contenido. Sin embargo, puedes querer incluir el contenido completo del artículo en cuestión, incluyendo el HTML. El RSSHelper te ayudará a codificar correctamente el HTML, pero tendrás que asegurarte de que algunas etiquetas no son incluidas, como iframe, object o script. La solución es pasar este campo por Sanitize::stripTags

$item['description'] = Sanitize::stripTags($post['Post']['content'], 'iframe', 'object', 'param', 'script');

link: es un enlace al artículo. Puedes usar una URL en forma de array de CakePHP, ya que el RSSHelper lo reconoce y lo procesa, y te beneficias del Reverse Routing si tienes rutas personalizadas.
guid: se trataría de un identificador global único para el artículo, el cual podría ser perfectamente su url. Pero para hacerlo bien debes pasar un formato particular:

$item['guid']  = array( 
    'url' => array(
             'controller' => 'posts', 
             'action' => 'view', 
             $post['Post']['slug'] ), 
    'isPermaLink' => 'true'
));

El atributo isPermalink indica que el guid se corresponde con el enlace permanente del recurso.
pubDate: se trata de la fecha de publicación del ítem.

Author se refiere al autor o autora del artículo. El dato principal es su email, de modo que el formato recomendado sería:

    correo@example.com (Autor del Item)

Sin embargo, no consigo que valide si pongo más de un autor.

Enclosure es un elemento opcional para indicar un contenido multimedia asociado. Tiene tres atributos, por lo que se pasa en forma de array con las claves:
  • url: que nos indicaria la URL del recurso multimedia
  • length: su tamaño
  • type: el mime type del recurso

Paso 4: preparando la vista, o cómo pasar tu modelo al feed

La vista tienes que ponerla en /views/nombre_del_modelo_plural/rss/nombre_de_la_action.ctp
En la vista basicamente lo que vamos a hacer son dos cosas: revisar la información de Channel, y pasarla al Layout, y procesar la información de cada registro para generar el Item correspondiente.

Para el layout, he copiado en APP/views/layouts/rss/default.ctp el layout que viene por defecto con el core de CakePHP, a fin de poder hacer algunas modificaciones. Lo detalle en el Paso 5.

En este caso, la información de Channel la ventilamos rápidamente asegurándonos de que description no contiene HTML. Luego pasamos la variable al layour, pero con el nombre $channelData, que es el que espera.

$channel['description'] = strip_tags($channel['description']);
$this->set('channelData', $channel);


Lo más normal es que hayamos pasado la información sobre los ítems en un array típico de resultados de un find. Tenemos entonces que recorrer ese array, registro a registro, y construir un array asociativo con los datos del ítem, por ejemplo:

foreach ($posts as $post) {
    $item['title'] = $post['Post']['title'];
    $item['description'] = Sanitize::stripTags($item['Post']['content'], 'iframe', 'object', 'param', 'script');

    // Otros campos
    echo $this->Rss->item(array(), $item).chr(10);
}



En algunos casos, los campos del registro pueden pasarse de forma directa. En otros casos, como description, necesitamos hacer algunos ajustes. En nuestro ejemplo, eliminamos algunos elementos HTML del contenido que no validan.


La vista resultante contiene los items del RSS listos para insertar en el layout.

Paso 5. Layout para RSS

A continuación, el código del layout que tengo en APP/views/layouts/rss/default.ctp

Rss->header(); ?>
"http://www.w3.org/2005/Atom");
}
if (!isset($channelData)) {
$channelData = array();
}
if (!isset($channelData['title'])) {
$channelData['title'] = $title_for_layout;
}
$channelData['atom:link'] = array('attrib' => array(
'href' => Router::url(null, true),
'rel' => 'self',
'type' => 'application/rss+xml'
));
$channelData['docs'] = 'http://blogs.law.harvard.edu/tech/rss';

$channel = $this->Rss->channel(array(), $channelData, $content_for_layout);
echo $this->Rss->document($documentData,$channel);
?> 


Lo más interesante de este código es que aprovecho esta instancia para incluir algunos parámetros que mejorarán las posibilidades de validación del feed.

Paso 6: Ya está, ¿cómo suscribirse?

Pues sí, el feed ya está listo para servir. La URL para suscribirse sería:

http://exemple.com/controller/action.rss

Sustituye lo que haga falta. Supongo que se podrían hacer rutas para que ciertas url se sirvan como feeds.

lunes, 23 de abril de 2012

Construyendo un permalink con el Router

Se supone que un Permalink es una URL que apunta a un recurso y que no va a cambiar con el tiempo, o dicho de otro modo, es la URL que nos lleva siempre a cierto recurso dentro de una web. Debe ser una URL absoluta.

En CakPHP la URL habría que construirla obteniendo la URL básica del sitio, añadiendo el controlador, la acción y los parámetros necesarios para identificar ese recurso.

En el supuesto de tener un modelo Post, con un campo "slug" (un slug actúa como un id significativo para un post, generado a partir de su título, lo que hace url del tipo /posts/view/el_libro_de_petete), puedes hacerlo así (este código está en la vista view del post):

Router::url(array ($post['Post']['slug']), true);

Es decir, al router nos basta pasarle dos parámetros: un array con datos sobre la URL y un flag para indicarle si queremos una URL absoluta o relativa.

La gracia del router es que es capaz de averiguar qué controlador y acción tiene que utilizar para completar la URL. En el ejemplo, el código está en la vista para la acción View del controlador Posts. Pero si estoy generando el Router en otra parte, podría pasarle

viernes, 13 de abril de 2012

ModSecurity y Ajax Upload

Al cambiar de hosting un proyecto me encontré con un problema pues dejó de funcionar mi módulo de subida de archivos.
Con la ayuda de los logs del servidor y del soporte técnico de Dinahosting localizamos que el problema tenía que ver con ModSecurity y, después de un buen rato de investigación, llegué a la conclusión de que la cusa tenía que estar en el código javascript, que en mi caso es el Valums Ajax File Uploader
La solcuión final es realmente curiosa. Hay que evitar que la petición Ajax especifique la cabecera Content-Type.