Archivo de la etiqueta: PHP

Mi primer webservice en PHP (chispas)

Tras mucho tiempo consumiendo webservices de otros me ha tocado crear mi primer servidor SOAP en PHP y, la verdad, me ha parecido realmente sencillo e intuitivo. Creas una clase con los métodos que vas a exponer en el ws y se crea automáticamente el servicio sobre ellos, tan sencillo como eso.

<?php
$wsdl="miclase.wsdl";
$soap = new SoapServer($wsdl);
$soap->setClass('MiClase');
$soap->handle();

//clase que gestiona el ws
class MiClase {
    public function MiClase(){
      //tu código
    }

    /**
     *
     * @param string $email
     * @return string
     */
    public function is_email_available($email){
        //tu codigo...
        return "OK";
    }
    /**
    *
    * @param string $phone
    * @param string $email
    * @return string
    */
    public function register_user($phone, $email){
       //tu codigo...
       return "OK";
    }
    /**
    *
    * @param string $phone
    * @return string
    */
    public function downgrade_user($phone){
       //tu codigo...
       return "OK";
    }
}
?>

Con esto se crea automáticamente nuestro webservice con los tres métodos públicos. Pero espera, falta algo, arriba de todo defines un “miclase.wsdl“. ¿Qué es eso? ¿De dónde sale?

En efecto, ese es el principal problema al crear un webservice SOAP con PHP, no se genera el WSDL automáticamente sino que hay que escribirlo ¡a mano!. Para solucionarlo tenemos la librería PHP WSDL Generator a la que únicamente debemos pasarle la clase de la que queremos extraer el WSDL y lo hace por nosotros :). Para que todo funciona bien es necesario que los métodos de nuestra clase estén bien documentados tal y como aparecen en el ejemplo anterior, de esta manera WSDL Generator sabrá configurar los tipos de datos de los parámetros de entrada y salida de los métodos.

Veamos un ejemplo:

<?php
require_once("wsdl2php/WSDLCreator.php");
$test = new WSDLCreator("miclase", "http://ws.tudominio.com/wsdl");
$test->addFile("miclase.php");
$test->setClassesGeneralURL("http://tudominio.com");
$test->addURLToClass("MiClase", "http://ws.tudominio.com/miclase.php");
$test->ignoreMethod(array("MiClase"=>"MiClase"));
$test->createWSDL();
$test->saveWSDL(dirname(__FILE__)."/miclase.wsdl", false);
?>

Este pequeño código nos generará el archivo WSDL de nuestro webservice. Como veis simplemente le indicamos el archivo con nuestra clase (el que escribimos anteriormente), la clase que queremos mapear con la URL del webservice (el endpoint) y, además, le indicamos que ignore el constructor de la clase ya que no será un método de nuestro webservice. Eso es todo.

Si ahora probamos el servicio web, por ejemplo desde el Web Service Explorer de Eclipse:

Tras darle la ruta del wsdl, http://ws.tudominio.com/miclase.php?wsdl, veremos los tres métodos que hemos expuesto y podremos probarlos y utilizarlos.

Nunca había tenido la necesidad de crear un servidor SOAP pero ha sido realmente sencillo. Ahora estoy buscando la manera de devolver tipos de datos complejos, pero eso será en el próximo capítulo :P.

Nuevo proyecto en 5 horas: UsayTira.me, direcciones de correo de usar y tirar explicado paso a paso

Parece que últimamente me aburro mucho :P. Hace unos días, leyendo un artículo, se me ocurrió de nuevo explicar cómo se hacen esos sistemas de correo instantáneos que se suelen utilizar para registrarse en webs y que después no te envíen spam :P. La idea me pareció muy adecuada para poner un ejemplo práctico de algo que vimos hace tiempo sobre otras utilidades para un servidor de correo y, tal y como me ocurrió hace unos meses, lo que era un artículo se convierte en proyecto.

La idea, por tanto, es crear un sistema que, sin necesidad de ningún registro, te permita crear una cuenta de correo y recibir y leer emails en ella por espacio de una hora, al cabo de este tiempo la cuenta se autodestruye y todos los emails serán devueltos. En nuestra aplicación tendremos dos opciones para crear la cuenta, aleatoria o personalizada, creo que no hacen falta más explicaciones. Una vez usas una cuenta puedes volver a ella más tarde cuando la necesites volviendo a crear una cuenta personalizada con el mismo usuario. Esto es útil, por ejemplo, para que te recuerden la contraseña que utilizaste para registrar en aquella web de descarga de películas y de la que ya no te acuerdas ;).

Qué necesitamos

  • Servidor Linux con Qmail como MTA.
  • Apache, PHP y MySQL.
  • Pear MimeDecode: para procesar los correos entrantes con PHP.
  • Una plantilla superchula de FreeCssTemplates
  • Jquery: para todo lo que es ajax y Javascript
  • ZeroClipboard:  para copiar y pegar automáticamente
  • Jquery ScrollTo: pluggin para desplazar el scroll automáticamente.
  • Una imagen de “Cargando” para las acciones ajax que personalizas aquí.
  • Diccionarios de palabras “aleatorias”. Aquí hay unos cuantos.
  • Adodb (opcional) para el acceso a base de datos.

Eso es todo, sólo hay que juntar las piezas adecuadamente.

Preparando Qmail

Nuestro primer paso, antes de ponernos con los temas puramente web, será configurar adecuadamente el servidor de correo para nuestro propósito. Para ello necesitamos crear un usuario del sistema que reciba todo el correo dirigido a un dominio. Esto lo podemos hacer del siguiente modo:

adduser -g users -s /dev/null usaytirame
passwd usaytirame UNACLAVESUPERCOMPLICADA

Añadimos nuestro nombre de host en:

/var/qmail/control/rcpthosts

usaytira.me

Y el usuario al que dirigiremos los correos en:

/var/qmail/control/virtualdomains

usaytira.me:usaytirame

Reiniciando Qmail conseguiremos que los correos enviados a cualquier cuenta del dominio vayan al buzón del usuario indicado, es decir cualquiercosa[arroba]usaytira.me.

Pero no queremos que los correos vayan al buzón del usuario sino simplemente procesarlos. Para eso editamos el archivo:

/home/usaytirame/.qmail-default

|preline /usr/bin/php /home/usaytirame/procesa.php

Sólo con esa línea. Con este comando conseguimos redirigir los correos entrantes a un script en el que podremos recuperarlos y reutilizarlos a nuestro antojo como veremos a continuación.

Procesando los emails entrantes

Vamos a comenzar por crear una base de datos donde guardaremos los emails recibidos. Como queremos poder acceder a los emails dirigidos a cada cuenta independientemente, haremos dos tablas, una (emails) la utilizaremos como tabla maestra para las cuentas de correo que se creen y en la otra (correos) iremos almacenando los emails recibidos para cada una de esas cuentas.  La estructura sería poco más o menos la siguiente:

CREATE TABLE IF NOT EXISTS `emails` (
  `idEmail` int(11) NOT NULL auto_increment,
  `fecha` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
  `email` varchar(255) NOT NULL,
  `ip` varchar(15) NOT NULL,
  PRIMARY KEY  (`idEmail`),
  KEY `email` (`email`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

CREATE TABLE IF NOT EXISTS `correos` (
  `id` int(11) NOT NULL auto_increment,
  `idEmail` int(11) NOT NULL,
  `de` varchar(255) NOT NULL,
  `para` varchar(255) NOT NULL,
  `subject` varchar(255) NOT NULL,
  `fecha` varchar(255) NOT NULL,
  `body` text NOT NULL,
  `fullmail` text NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `idEmail` (`idEmail`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

Creo que es suficientemente descriptiva. El campo fecha de emails nos servirá para controlar los 60 minutos de duración máxima.

Pues ya tenemos todo preparado. Veamos cómo procesamos los emails.

/home/usaytirame/procesa.php

include ("mimeDecode.php");

$email=file("php://stdin");
$email=implode("", $email);

$params['include_bodies'] = true;
$params['decode_bodies'] = true;
$params['decode_headers'] = true;
$params['input'] = $email;
$structure = Mail_mimeDecode::decode($params);

$subject = quoted_printable_decode(trim($structure->headers['subject']));
$ddate = quoted_printable_decode(trim($structure->headers['date']));
$from = quoted_printable_decode(trim($structure->headers['from']));
$to = quoted_printable_decode(trim($structure->headers['to']));

if(ereg("<(.*)>", $to, $p))
    $to=$p[1];
$to=strtolower($to);

Tenemos la primera parte del procesado preparada. De aquí debemos destacar:

$email=file("php://stdin");

Con esto leemos desde la entrada estandar el contenido del email. Recordad que no estamos en una aplicación web sino en un script ejecutado en la consola del sistema.

La otra línea importante es:

$structure = Mail_mimeDecode::decode($params);

Con ella conseguimos procesar el email y separar su estructura en un array asociativo con los distintos parámetros. Os ayudará mucho ver el contenido de ese array:

print_r($structure)

Llegados a este punto podemos hacer una prueba. Necesitamos el contenido completo de un email, podemos sacarlo de nuestro cliente de correo o del propio servidor.

cat prueba.eml | /home/usaytirame/procesa.php

Si todo ha ido bien veremos en pantalla un array con la estructura del correo.

No voy a detallar todo el proceso ya que alargaría mucho el artículo. Vamos con el siguiente paso:

$query="select idEmail from emails where email=".$conn->quote($to);
$rs=$conn->Execute($query);
if($rs->recordcount()==0){
        exit(100);
}else{
        $idEmail=$rs->fields['idEmail'];
        $content = get_content($structure);
        $query="insert into correos
            (idEmail, de, para, subject, fecha, body, fullmail)
            VALUES
           ($idEmail,
            ".$conn->Quote($from).",
            ".$conn->Quote($to).",
            ".$conn->Quote($subject).",
            ".$conn->Quote($ddate).",
            ".$conn->Quote($content).",
            ".$conn->Quote($email).")";
        $rs=$conn->Execute($query);
}

En la primera parte del script comprobamos si la cuenta a la que va destinado el email existe en nuestra base de datos, si no, muy importante, devolvemos un código 100 que indica a qmail que debe devolver el correo ya que no existe el usuario.

Si la cuenta existe recogemos el cuerpo del mensaje. Yo lo hago con la función get_content, que analiza la estructura del correo y devuelve el contenido. Esta parte os la dejo a vosotros. Básicamente consiste en comprobar las distintas partes que puede tener un correo y devolver lo que estimemos oportuno. Un detalle muy importante a tener en cuenta es la codificación tanto del email como de nuestra base de datos y la aplicación web. En mi caso las dos últimas están en UTF-8, con lo que debo convertir todos los textos del email a esta misma codificación. La estructura que teníamos inicialmente en un array tendrá parámetros que nos indican el charset en el que viene el email. Las funciones de conversión de PHP te pueden ser útiles: iconv, utf8_encode, etc.Finalmente introducimos todos los campos del email en la base de datos.

Puedes volver a probar a procesar el email de prueba tal y como hicimos antes. Recuerda que debes añadir el registro de la tabla emails para que  guarde el correo, si no la cuenta no existirá. Una vez te funcione desde la línea de comandos ya puedes probar a enviarte un correo real :).

La aplicación web

No creo que hacer la parte web propiamente dicha necesite muchas explicaciones. Tenemos ya todos los elementos preparados, sólo debemos añadir los formularios para crear la cuenta de correo (aleatoria o personalizada) y, con un poco de ajax, ir cargando los correos a medida que van llegando. No  hay más truco.

Añadiré, eso sí, algunas aclaraciones interesantes.

Para crear las cuentas aleatorias, en vez de utilizar una secuencia aleatoria de números y letras, que daría como resultado algo ininteligible, usamos los diccionarios que comentaba más arriba. Los importamos en una tabla de la base de datos y simplemente tenemos que buscar aleatoriamente una palabra que no esté utilizada todavía como cuenta de correo, sencillo y muy impactante visualmente ya que estás ofreciendo cuentas legibles y con sentido.

Como en cualquier otra aplicación accesible públicamente, hay que añadir algún tipo de mecanismo de seguridad. En mi caso lo he hecho implementando una blacklist de direcciones IP. Cada vez que se crea una cuenta actualizo en una base de datos el número de cuentas que se han creado desde esa IP, si pasa del límite que estimemos oportuno, esa IP se pasa a la tabla de lista negra y cuando intente crear una nueva cuenta no se le dejará.

Nos falta una cosa: Eliminar las cuentas que tienen más de una hora. Muy sencillo, una tarea en el CRON que ejecuta un script que lanza una consulta a la base de datos que elimina las cuentas (y sus correos asociados) que se crearon hace más de 60 minutos.

Finalmente he añadido la opción de reiniciar esos 60 minutos de tiempo, simplemente actualizando el timestamp de la base de datos y algunos efectos visuales para plegar y desplegar los mensajes usando Jquery.

No hay mucho más, en unas cinco horas tenemos la aplicación hecha y funcionando.

Conclusiones

Como conclusión, la misma que hice hace unos meses con el primer proyecto. La copio tal cual porque es igual de válida.

Bueno, y todo este rollo ¿para qué?. Pues muy sencillo, para que veais que hoy en día la tecnología está al alcance de todos, es sencillo y rápido crear un proyecto en Internet, hay de todo por todas las esquinas, la tecnología no es lo importante, lo que verdaderamente cuenta es cómo mueves ese producto tecnológico para rentabilizarlo y obtener un beneficio de él.

Ya tengo mi proyecto superchulo funcionando, sólo me ha costado unas 5 horas de trabajo. Le he puesto un poco de Adsense por aquí y por allí. ¿Y ahora qué? ¿A esperar a que la gente entre y me haga millonario? 😛 Es mucho más complicado que eso como todos sabéis, primero tienes que tener una masa de usuarios elevada que le dé movimiento al proyecto y después tienes que conseguir que la mayoría de ellos sean gente normal, no gente técnica, usuarios avanzados que no pagamos por nada ni pinchamos en publicidad 😛 .

Hoy en día, en Internet, como en cualquier negocio, las técnicas de marketing y venta son mucho más importantes que la tecnología en sí misma, es duro reconocerlo, pero es así. De nada sirve que tengas el mejor producto del mundo mundial si no consigues que la gente lo utilice y se deje dinero, así de claro. Si tienes los conocimientos adecuados para mover el negocio, no te preocupes, la tecnología te la aporta cualquier partner por un módico precio, pero poner en manos de otro toda la estrategia de ventas de tu negocio no está tan claro ¿no?.

Espero que os sirva de algo el artículo. He querido mostrar, fundamentalmente, cómo utilizando algunas librerías que puedes obtener sin coste puedes hacer algo realmente útil y funcional con muy poco esfuerzo. Seguro que sacáis alguna idea.

Perdón por el rollo 😛 , al final me ha costado mucho más escribir el artículo que implementarlo.

Podíamos haber añadido una opción que he visto por ahí que consiste en crear una cuenta automáticamente cada vez que entra un email para una cuenta que no existe, pero estaríamos creando cuentas para todo el spam que recibamos, así que prefiero no hacerlo. Si quisierais hacerlo creo que ya sabéis cómo.

Segmentation fault al instanciar un webservice WCF de .NET desde PHP

Recientemente nos hemos encontrado con un problema que nos ha tenido varios días bloqueados hasta conseguir averiguar el origen. Llevamos mucho tiempo utilizando webservices programados en .NET desde aplicaciones PHP sin ningún problema, pero esta vez estaba hecho con la nueva tecnología WCF (Windows Communication Foundation) de Microsoft .NET 3.5. El problema era que en cuanto lo subimos a producción la aplicación PHP devolvía un pantallazo en blanco sin más información. Analizando los logs vimos que el proceso de Apache provocaba un Segmentation Fault con lo que no llegábamos a ver ninguna excepción.

Tras muchas pruebas conseguimos aislar el error en la línea de código que instanciaba el nuevo webservice, si eliminábamos esa parte todo funcionaba correctamente.

$client=new SoapClient("http://wcf.tudominio.com/webservice/ws.svc?wsdl");

Lo más curioso es que en los entornos de preproducción sí que funcionaba, no entendíamos nada. Analizando las máquinas de los distintos entornos todas eran idénticas en cuanto a versiones y configuración excepto algunos parámetros SOAP para PHP, en la máquina de producción tienen la caché de wsdl activada mientras que en los demás está desactivada.

soap.wsdl_cache_enabled = 1
soap.wsdl_cache_dir = /tmp/
soap.wsdl_cache_ttl = 7200
soap.wsdl_cache_limit = 50

No puede ser que el error sea el cacheo. Pues sí, lo es, en cuanto desactivamos la caché del servidor de producción todo comenzó a funcionar correctamente.

Perfecto, pero la caché tiene que estar activada, la aplicación hace uso de unos 35 webservices, si para cada instancia de cada uno de ellos hay que cargar previamente el wsdl, el rendimiento cae por los suelos, es imprescindible.

Pues nada, solución increíblemente cutre:

ini_set('soap.wsdl_cache_enabled', '0');
ini_set('soap.wsdl_cache_ttl', '0');
ini_set('soap.wsdl_cache', '0');

$client = new SoapClient("http://wcf.tudominio.com/webservice/ws.svc?wsdl");

ini_set('soap.wsdl_cache_enabled', '1');
ini_set('soap.wsdl_cache_ttl', '7200');
ini_set('soap.wsdl_cache', '3');

Así es, desactivamos la caché antes de instanciar sólo este webservice y la volvemos a activar después. No hemos encontrado otra manera de solucionarlo ni hemos encontrado ninguna referencia de alguien que haya sufrido el mismo problema. La solución es mala, muy mala, no deja de ser un apaño, pero funciona y nos permite salir del paso hasta que sepamos por qué ocurre.

Segmentation fault en WordPress al utilizar eAccelerator

Hacía casi un mes que no escribía nada :O, cosas del periodo navideño. Espero retomar mis hábitos de escritura con ganas con el año nuevo, tengo muchas cosas pendientes por contar.

Llevaba un par de meses preocupado por mi blog. Por alguna extraña razón, de vez en cuando comenzaba a devolverme pantallas en blanco al acceder a cualquier artículo e incluso al acceder al tablero de administración, la pantalla de login funcionaba bien pero una vez te autentificabas saltaba el error. Reiniciando el servidor web volvía a funcionar correctamente por un periodo indeterminado de tiempo, a veces unas horas a veces varios días, pero terminaba saltando de nuevo la pantalla en blanco.

Al comprobar los logs de errores de Apache lo único que aparecía era algo como:

[Mon Jan 08 09:34:14 2010] [notice] child pid 4899 exit signal Segmentation fault (11)

Es decir, no me aportaba nada, un misterio, y tampoco encontré ninguna referencia útil buscando “wordpress segmentation fault“. Me tocó poner sobre la mesa todas mis habilidades de depuración y a base de echo’s y exits ir siguiendo la pista hasta llegar al punto donde saltaba el error. Tras un par de horas llegué a la conclusión de que el problema estaba en la función wp_filter_kses del archivo:

wp-includes/kses.php

Faltaba saber por qué provocaba un Segmentation fault y no un error de PHP estándar. Descubrí además que reescribiendo el mismo archivo sin hacer cambios, es decir, cambiando la fecha de actualización, todo comenzaba a funcionar correctamente… hasta que volvía a fallar. Raro, raro, raro…

Teniendo ya una referencia clara del origen del problema, una sencilla búsqueda de “kses.php segmentation fault”  me condujo al origen del problema, y éste no era otro que el sistema de cacheo que se utiliza en el servidor, eAccelerator. Al parecer, y sin una causa lógica, la caché de ese archivo se corrompe periódicamente y al intentar leerlo provoca el “Segmentation fault“. Tiene sentido entonces que al cambiar la fecha del archivo volviese a funcionar, este cambio provocaba que se regenerase la caché del mismo y funcionase correctamente.

La solución es indicarle a eAccelerator que no cachee ese archivo concreto. Para ello añadimos al archivo de configuración:

/etc/php.d/eaccelerator.ini

eaccelerator.filter="!*kses.php"

Con esto se acabaron los pantallazos en blanco, espero recuperar algo de las visitas perdidas las últimas semanas ya que ha estado la mayor parte de las vacaciones caído por culpa de este error.

De artículo a proyecto explicado paso a paso: acorta URLs con IraUrl.me

Hace un par de semanas me encontré con la necesidad de utilizar uno de esos sistemas que hay por ahí para acortar URL‘s. Necesitaba enviar una dirección por SMS y tenía que ocupar la menor cantidad de caracteres posible por aquello de optimizar el texto del mensaje. Mientras lo utilizaba pensaba en lo ingenioso de utilizar un sistema de numeración base36 para reducir exponencialmente el número de caracteres de la redirección. Esto iba a ser, pues, un artículo sobre las ventajas de los sistemas de numeración distintos al decimal para determinados proyectos, pero se acabó convirtiendo en un proyecto completo. Cuando estaba comenzando la explicación teórica pensé, ¿por qué no hacerlo? ¿por qué no demostrar lo rápido y fácil que se puede montar algo en Internet hoy en día?

Así, tras unas 15 horas de trabajo os presento IraUrl.me, un acortador de URL’s al estilo de TinyUrl o Bit.ly. Me ha costado más escribir el artículo que hacerlo realidad, curioso ¿eh?. En realidad a medida que iba preparando la aplicación se me iban ocurriendo más cosas que sería interesante montar, por lo que las 8 horas iniciales, más o menos, se convirtieron en 15.

iraurl

La teoría

Para el que no lo sepa, un acortador de URL se basa en encontrar un dominio lo más corto posible y crear redirecciones HTTP 301 a otras URL‘s. El truco está en optimizar los parámetros que añadiremos a la URL para que sean lo más cortos posible, no queremos que éstos nos penalicen lo corto del dominio.

¿Cómo funcionan entonces estos acortadores de URL‘s? Mucho más fácil de lo que parece y seguramente como a ti se te habría ocurrido. Simplemente tenemos una base de datos donde vamos añadiendo registros a una tabla a medida que se van creando nuevas URL’s cortas.  Esta tabla tiene un campo autonumérico, la clave de la tabla, que para cada nueva URL nos devuelve un identificador único, con lo que cada dirección podría ser accesible de la manera habitual:

http://dominio.com/1

http://dominio.com/1000000

Esa es exactamente la idea, lo único que hacemos es cambiar el identificador en cuestión de base10 (la de nuestro sistema métrico decimal) a base36 o base62 en mi caso. Otros sistemas de numeración conocidos son el hexadecimal (base16) y base64.

Vale, ya has hablado en chino. ¿De qué va esto? Veamos.

Sobre bases de numeración

El sistema decimal utiliza diez dígitos (de ahí lo de decimal :P) para formar todas las combinaciones de números posibles. Lo que ya conocemos, vamos. El binario utiliza dos dígitos (0 y 1), el hexadecimal 16 (0..9ABCDE), base36, como su nombre indica, treinta y seis (0..9a..z) y base62 utiliza los 62 dígitos que comprenden los números del 0 al 9 y las letras de la A a la Z en mayúsculas y minúsculas (0..9a..zA..Z). Veamos unos ejemplos:

Binario Decimal Hexadecimal Base36 Base62
0 0 0 0 0
1 1 1 1 1
10 2 2 2 2
1010 10 A a a
1100100 100 64 2s 1c
1000000 F4240 lfls 4c92
10000000 989680 5yc1s FXsk

Se puede observar de un vistazo cómo a medida que aumenta el número, cuanto mayor sea la base que manejamos menos dígitos tendrá . Los números, a fin de cuentas, son combinaciones continuas entre todos los dígitos posibles.Así, en función de la base y del número de dígitos, el mayor número representable representable sería:

Num. dígitos
Decimal Base62
1 10 62
2 100 3844
3 1000 238328
4 10000 14776336
5 100000 916132832
6 1000000 56800235584
7 10000000 3521614606208
8 100000000 218340105584896
9 1000000000 13537086546263552

O lo que es lo mismo, base(número de dígitos), 629 contra 109.Espero que se entienda la teoría. Como curiosidad:

Decimal: 10000000000000000000000

Base62: 36aHo5IWaicak

La pregunta ahora sería, ¿Por qué Base62 y no Base64, por ejemplo, mucho más conocida? Sencillo, porque además de los 62 caracteres de Base62, Base64 utiliza dos adicionales, generalmente + y / además del =, lo que convierten la cadena en no web safe, es decir, los caracteres especiales debieran traducirse para que su transporte no diese problemas, con lo que estaríamos perdiendo las ventajas de nuestro cifrado corto. Los 62 caracteres utilizados en Base62 son totalmente seguros, sólo letras (mayúsculas y minúsculas) y números.

Sabiendo ya cómo funciona el sistema, veremos cómo crear nuestra aplicación. Obviamente no contaré todo paso a paso ya que sino tardaría mucho más en escribir el artículo que en hacer la aplicación, me meteré sólo en las cosas que considere más importantes.

Para codificar/decodificar de base10 a base62 utilizaré estas librerías:

function dec2base($dec, $base, $digits = FALSE) {
      if($base < 2 or $base > 256) {
            die("Invalid Base: .$basen");
      }
      bcscale(0);
      $value = '';
      if(!$digits) {
            $digits = digits($base);
      }
      while($dec > $base - 1) {
            $rest = bcmod($dec,$base);
            $dec = bcdiv($dec,$base);
            $value = $digits[$rest].$value;
      }
      $value=$digits[intval($dec)].$value;
      return (string) $value;
}

function base2dec($value, $base, $digits = FALSE) {
      if($base < 2 or $base > 256) {
            die("Invalid Base: .$basen");
      }
      bcscale(0);
      if($base < 37) {
            $value = strtolower($value);
      }
      if(!$digits) {
            $digits = digits($base);
      }
      $size = strlen($value);
      $dec = '0';
      for($loop=0; $loop < $size; $loop++) {
            $element = strpos($digits, $value[$loop]);
            $power = bcpow($base, $size-$loop-1);
            $dec = bcadd($dec, bcmul($element, $power));
      }
      return (string)$dec;
}

function digits($base) {
      if($base < 64) {
            return substr('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_', 0, $base);
      } else {
            return substr("x0x1x2x3x4x5x6x7x8x9xaxbxcxdxexfx10x11x12x13x14x15x16x17x18x19x1ax1bx1cx1dx1ex1f !x22#x24%&'()*+,-./0123456789:;<=>x3f@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~x7fx80x81x82x83x84x85x86x87x88x89x8ax8bx8cx8dx8ex8fx90x91x92x93x94x95x96x97x98x99x9ax9bx9cx9dx9ex9fxa0xa1xa2xa3xa4xa5xa6xa7xa8xa9xaaxabxacxadxaexafxb0xb1xb2xb3xb4xb5xb6xb7xb8xb9xbaxbbxbcxbdxbexbfxc0xc1xc2xc3xc4xc5xc6xc7xc8xc9xcaxcbxccxcdxcexcfxd0xd1xd2xd3xd4xd5xd6xd7xd8xd9xdaxdbxdcxddxdexdfxe0xe1xe2xe3xe4xe5xe6xe7xe8xe9xeaxebxecxedxeexefxf0xf1xf2xf3xf4xf5xf6xf7xf8xf9xfaxfbxfcxfdxfexff", 0, $base);
      }
}

function base_encode($value) {
      return dec2base(base2dec($value, 256), 62);
}

function base_decode($value) {
      return dec2base(base2dec($value, 62), 256);
}

Las dos últimas funciones son las que utilizaremos para las conversiones.

Paquetes y librerías utilizadas:

  • Free CSS Templates: Para tener una bonita plantilla xhtml para nuestro proyecto 🙂 .
  • Maxmind GeoLite Country: Para la geolocalización de un usuario a través de su IP.
  • Wurfl: Para identificar el navegador/terminal de un visitante por su User Agent. Yo lo complemento con Tera-Wurfl para mantener la información en una base de datos.
  • Fusion Charts Free: Para los gráficos de estadísticas.
  • Zero Clipboard: Para copiar al portapapeles la url corta generada sin que el usuario deba seleccionarla, solo con un click.
  • JqueryUI: Para el componente de navegación con pestañas.
  • Google Safebrowsing API: Para comprobar si una url es potencialmente peligrosa.
  • Adodb (opcional): Para abstraer el acceso a la base de datos. Yo suelo utilizarla en todos mis proyectos pero se pueden utilizar las funciones nativas de PHP.
  • PHPExcel: Para generar Excel y PDF.

Adicionalmente:

  • Un dominio y hosting donde alojarlo (6€).
  • PHP y MySQL (tampoco son obligatorios, puedes hacerlo con cualquier tecnología).
  • 15 horas de tu tiempo :P.

Estructura de la web

Cualquier proyecto web que se precie debe comenzarse describiendo qué queremos mostrar a nuestros visitantes, hay que recopilar todas las ideas, decidir las que interesan de verdad, estudiar cómo se van a disponer en el frontend y terminar con un mapa web que nos indique el flujo a seguir en el trabajo. Este será el nuestro:

mapaWebIraUrlCuando un usuario vaya a una de nuestras url’s cortas en realidad estaremos reenviando la petición http internamente a un script encargado de hacer todo el proceso, link.php en mi caso.

Para ver las estadísticas de una URL me ha gustado el sistema de bit.ly, así que se lo copiamos :P. Añadiendo un “+” al final de la URL corta, en vez de saltar a la dirección larga mostraremos las estadísticas. Esto lo haremos, como en el caso anterior, dirigiendo internamente a otro script, stats.php.

Si el identificador que pretendemos usar para saltar a la url larga o ver estadísticas no existe, reenviaremos a index.php para que muestre un mensaje de error tipo “La url no existe“.

El dominio

Obviamente tendremos que buscar un dominio lo más corto posible, la mayoría estarán ya ocupados, pero buscando y buscando en TLD‘s extraños puedes encontrar algo. Yo he escogido un .me porque tiene un carácter menos que un .com 🙂 y no cuesta lo que un .es :P.

La base de datos

Muy sencilla, dos tablas solamente, en una mantendremos las urls generadas y en otra las estadísticas de acceso a las mismas.

CREATE TABLE IF NOT EXISTS `urls` (
  `id` bigint(20) NOT NULL auto_increment,
  `url` varchar(500) NOT NULL,
  `titulo` varchar(255) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

CREATE TABLE IF NOT EXISTS `stats` (
`id` bigint(20) NOT NULL auto_increment,
`idurl` bigint(20) NOT NULL,
`codpais` varchar(255) NOT NULL,
`referer` varchar(255) NOT NULL,
`hostreferer` varchar(255) NOT NULL,
`ua` varchar(255) NOT NULL,
`hora` datetime NOT NULL,
`pais` varchar(255) NOT NULL,
`marca` varchar(255) NOT NULL,
`modelo` varchar(255) NOT NULL,
PRIMARY KEY  (`id`),
KEY `idurl` (`idurl`,`hora`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

La configuración de Apache

Como hemos comentado queremos que cuando alguien vaya a cualquier url de nuestro site del tipo

http://tudominio.com/prueba3

Se redirija internamente al script link.php que será el encargado de procesar la petición. De igual modo si alguien visita

http://tudominio.com/prueba3+

le mostraremos las estadísticas de esa URL (si existen). Configuramos Apache para que tenga en cuenta todas estas particularidades, mod_rewrite será nuestro amigo para conseguirlo. En mi caso he hecho que si la llamada no es un script php, ni una imagen ni un archivo javascript ni un css ni tiene el signo “+“, se vaya a link.php. Si tiene el signo “+” se irá a stats.php.

RewriteEngine on
RewriteCond %{REQUEST_URI} !^/(.*).php$
RewriteCond %{REQUEST_URI} !^/css/(.*)$
RewriteCond %{REQUEST_URI} !^/js/(.*)$
RewriteCond %{REQUEST_URI} !^/(.*)+$
RewriteCond %{REQUEST_URI} !^/images/(.*)$
RewriteRule ^(.+) /link.php?id=$1
RewriteCond %{REQUEST_URI} ^/(.*)+$
RewriteRule ^(.+) /stats.php?id=$1

Imagen y diseño

Para el diseño, o te lo haces tú mismo o si eres un negado creativo como yo te descargas una plantilla superchula de freecsstamplates.org y la adaptas a tus necesidades, no voy a entrar en más detalles.

Crear urls cortas

El primer script de nuestra aplicación. Un sencillo formulario donde el usuario introduce la URL que quiere acortar y al hacer submit… la acortamos integrando el sistema con la comprobación de malware que explicaba hace unos días ;).

$urlbase="";
if(isset($_POST['url'])){
        $url=$_POST['url'];

        try{
	        if(substr($url, 0, 4)!="http")
	        	$url="http://".$url;
			$class = new GoogleSafeBrowsing('ABQIAAAAQYvf-54bCBMuGY20SeONVxQ236Mc_IjryQBl-W_Repaw3fCykA', true);
			$nomalware=$class->lookupsFor($url);

			if($nomalware){
        			$htmltitle="";
        			$html=file_get_contents($url);
        			if($html!=""){
        				preg_match('/(.*)</title>/is', $html, $matches);
					if(is_array($matches) && count($matches>0))
						$htmltitle=trim($matches[1]);
        			}
                		$query="select * from urls where url='$url'";
				$rs=$conn->Execute($query);
				if($rs->recordcount()>0){
					$id=$rs->fields['id'];
				}else{
					$query="insert into urls (url, titulo) VALUES ('$url', '$htmltitle')";
					$rs=$conn->Execute($query);
					$id=$conn->insert_id();
				}
				$base=base_encode($id);
				$urlbase="http://iraurl.me/".$base;
			}else{
				$err=4;
			}
        }catch(exception $e){
        	$err=3;
        }
}

Hemos añadido una pequeña comprobación. Si la URL que se quiere añadir ya existe, devolvemos la misma URL corta, yo he tomado esa decisión, tú puedes hacer lo que quieras. Además obtenemos el título de la URL final para tener una referencia hacia la misma, cuestión de sencillez visual :P.

Reenvío a urls largas

Ya tenemos nuestra URL corta, vamos ahora a reenviar las solicitudes a ella a la larga. Recordemos que nuestro Apache nos va a redirigir esa petición a link.php?id=XXXX. Nuestro script actualiza, además, las estadísticas de visitas de la url.

if(isset($_GET['id'])){
	$idb=$_GET['id'];
	$id=base_decode($idb)+0;
	try{
		$query="select * from urls where id=$id";
		$rs=$conn->Execute($query);
		if($rs->recordcount()>0){
			$url=$rs->fields['url'];
			$referer=@$_SERVER['HTTP_REFERER'];
			$ua=@$_SERVER['HTTP_USER_AGENT'];
			$ip=@$_SERVER['REMOTE_ADDR'];

			$hostreferer="";
			if(preg_match('@^(?:http://)?([^/]+)@i', $referer, $matches)>0)
				$hostreferer = $matches[1];

			$terminal=getMarcaModelo($_SERVER['HTTP_USER_AGENT']);
			$marca=$terminal['marca'];
			$modelo=$terminal['modelo'];

			$temp=getGeoCodeAndPais($ip);
			$codpais=$temp['code'];
			$pais=$temp['pais'];

			$query="insert into stats (idurl, codpais, referer, ua, hora, pais, marca, modelo, hostreferer) VALUES
					($id, '$codpais', '$referer', '$ua', now(), '$pais', '$marca', '$modelo', '$hostreferer')";
			$rs2=$conn->Execute($query);

			header("HTTP/1.x 301 Moved");
			header("Location: $url");
			exit;
		}else{
			header("Location: http://iraurl.me/index.php?err=1");
			exit;
		}
	}catch(exception $e){
		header("Location: http://iraurl.me/index.php?err=2");
		exit;
	}
}
header("Location: http://iraurl.me/index.php?err=1");

Como veis, si la URL no existe redirigimos al usuario a index.php con un mensaje de error. Necesitaremos dos funciones adicionales, las que nos devuelven información del país de origen de una IP y los datos del terminal del usuario (móvi o web). No entraré en detalles sobre la instalación de Maxmind GeoLite Country o de Wurfl/Tera-Wurfl.

function getGeoCodeAndPais($ip){
	require_once(dirname(__FILE__)."/geoip/geoip.inc");
	$gi = geoip_open("/usr/share/GeoIP/GeoIP.dat",GEOIP_STANDARD);
	$codpais=geoip_country_code_by_addr($gi, $ip);
	$pais=geoip_country_name_by_addr($gi, $ip);
	geoip_close($gi);
	return array("pais"=>$pais, "code"=>$codpais);
}

function getCapabilities($ua){
	require_once(dirname(__FILE__)."/Tera-WURFL/TeraWurfl.php");
	$wurflObj = new TeraWurfl();
	$matched = $wurflObj->GetDeviceCapabilitiesFromAgent($ua);
	$movil = $wurflObj->capabilities;

	return $movil;
}

Estadísticas

La teoría es la misma. Si existe la URL cargamos los datos, si no redirigimos a la home. En nuestra caso utilizamos el componente de pestañas de JqueryUI para organizar los distintos tipos de datos que permitiremos ver y añadiremos los botones para exportar a Excel y PDF.

$idb=substr($_GET['id'], 0, strlen($_GET['id'])-1);

$id=base_decode($idb)+0;
$query="select * from urls where id=$id";
$rs=$conn->Execute($query);
if($rs->recordcount()>0){
	$urlbase="http://iraurl.me/".$idb;
	$url=$rs->fields['url'];
	$id=$rs->fields['id'];
	$htmltitulo=$rs->fields['titulo'];
	if($htmltitulo=="")
		$htmltitulo=$url;
	$query="select count(*) as nregs from stats where idurl=$id";
	$rs=$conn->Execute($query);
	$clicks=$rs->fields['nregs'];
}else{
	header("Location: http://iraurl.me/index.php?err=1");
	exit;
}

Muy sencillo.

stats

Lo complicado en este caso es mostrar las gráficas con FusionCharts. Para cada una debemos añadir algo de código html:

<div id="chartClicks"></div>
<script type="text/javascript">
 var myChart = new FusionCharts("images/Charts/FCF_Column3D.swf", "idChartClicks", "430", "400", "0", "1");
 myChart.setDataURL(escape("xml.php?t=cli&id='.$idb.'"));
 myChart.setTransparent(true);
 myChart.render("chartClicks");
</script>

El script xml.php será el que devuelva los datos en el formato adecuado para FusionCharts. Por ejemplo:

$query="select DAY(hora) as dia, MONTH(hora) as mes, YEAR(hora) as ano, count(*) as nclicks
 from stats
 where idurl=$id
 group by ano, mes, dia
 order by hora";
$rs=$conn->Execute($query);

$xml='<graph caption="Clicks" rotateNames="1" xAxisName="Día" yAxisName="Clicks" showNames="1" decimalPrecision="0" formatNumberScale="0" chartLeftMargin="5" chartRightMargin="5" chartTopMargin="0">';
while($r=$rs->fetchrow()){
 $xml.='<set name="'.$r['dia']."/".$r['mes']."/".$r['ano'].'" value="'.$r['nclicks'].'" color="#A1A1A1" />';
}
$xml.='</graph>';

Os doy sólo un ejemplo, el resto lo montáis por vuestra cuenta :).

Descifrar urls cortas

Todos los sistemas de acortar URL’s funcionan tal y como cuento en este artículo, haciendo un HTTP/301 redirect hacia la url original.

A partir de la URL corta podemos saber cual es la URL original simplemente siguiendo las redirecciones que hace. Muy sencillo con PHP y que, además nos sirve para, integrándola en nuestra API de malware, prevenir posibles problemas con la URL final.

function get_web_page( $url )
{
    $options = array( 'http' => array(
        'user_agent'    => 'spider',
        'max_redirects' => 10,
        'timeout'       => 120,
    ) );
    $context = stream_context_create( $options );
    $page    = @file_get_contents( $url, false, $context );

    $result  = array( );
    if ( $page != false )
        $result['content'] = $page;
    else if ( !isset( $http_response_header ) )
        return null;    // Bad url, timeout

    // Save the header
    $result['header'] = $http_response_header;

    // Get the *last* HTTP status code
    $nLines = count( $http_response_header );
    for ( $i = $nLines-1; $i >= 0; $i-- )
    {
        $line = $http_response_header[$i];
        if ( strncasecmp( "HTTP", $line, 4 ) == 0 )
        {
            $response = explode( ' ', $line );
            $result['http_code'] = $response[1];
            break;
        }
    }

    return $result;
}

$url="";
if(isset($_POST['url'])){
	$url=$_POST['url'];
	$datos=get_web_page( $url );
	if($datos){
		$headers=$datos['header'];
		$urls=array($url);
		foreach($headers as $head){
			$temp=explode(" ", $head);
			if(strtolower($temp[0])=="location:"){
				$urls[]=$temp[1];
			}
		}
		$htmltitle="";
	    	preg_match('/(.*)</title>/is', $datos['content'], $matches);
		if(is_array($matches) && count($matches>0))
			$htmltitle=trim($matches[1]);
	}
}

Ya está, en $urls tendremos la lista de urls que van saltando hasta llegar a la final.

descifrar

Api

Hoy en día todo tiene que tener Api. Para las estadísticas es muy sencillo, el propio XML que generamos para consumir con FusionCharts nos permite que clientes externos se alimenten del mismo. Para crear URL‘s cortas remotamente, simplemente creamos un archivo api.php:

if(isset($_GET['url'])){
	$url=urldecode($_GET['url']);
	try{
        $htmltitle="";
        if(substr($url, 0, 4)!="http")
        	$url="http://".$url;
        $html=file_get_contents($url);
        if($html!=""){
        	preg_match('/(.*)</title>/is', $html, $matches);
			if(is_array($matches) && count($matches>0))
				$htmltitle=trim($matches[1]);
        }
        $query="select * from urls where url='$url'";
        $rs=$conn->Execute($query);
        if($rs->recordcount()>0){
                $id=$rs->fields['id'];
        }else{
                $query="insert into urls (url, titulo) VALUES ('$url', '$htmltitle')";
                $rs=$conn->Execute($query);
                $id=$conn->insert_id();
        }
        $base=base_encode($id);
        $urlbase="http://iraurl.me/".$base;
        echo $urlbase;
    }catch(exception $e){
   		echo "ERROR";
    }
}

Eso es todo. No olvides integrarlo también con el sistema de malware.

Conclusiones

Bueno, y todo este rollo ¿para qué?. Pues muy sencillo, para que veais que hoy en día la tecnología está al alcance de todos, es sencillo y rápido crear un proyecto en Internet, hay de todo por todas las esquinas, la tecnología no es lo importante, lo que verdaderamente cuenta es cómo mueves ese producto tecnológico para rentabilizarlo y obtener un beneficio de él.

Ya tengo mi proyecto superchulo funcionando, sólo me ha costado unas 15 horas de trabajo. Le he puesto un poco de Adsense por aquí y por allí. ¿Y ahora qué? ¿A esperar a que la gente entre y me haga millonario? 😛 Es mucho más complicado que eso como todos sabéis, primero tienes que tener una masa de usuarios elevada que le dé movimiento al proyecto y después tienes que conseguir que la mayoría de ellos sean gente normal, no gente técnica, usuarios avanzados que no pagamos por nada ni pinchamos en publicidad :P.

Hoy en día, en Internet, como en cualquier negocio, las técnicas de marketing y venta son mucho más importantes que la tecnología en sí misma, es duro reconocerlo, pero es así. De nada sirve que tengas el mejor producto del mundo mundial si no consigues que la gente lo utilice y se deje dinero, así de claro. Si tienes los conocimientos adecuados para mover el negocio, no te preocupes, la tecnología te la aporta cualquier partner por un módico precio, pero poner en manos de otro toda la estrategia de ventas de tu negocio no está tan claro ¿no?.

Espero que os sirva de algo el artículo. He querido mostrar, fundamentalmente, cómo utilizando algunas librerías que puedes obtener sin coste puedes hacer algo realmente útil y funcional con muy poco esfuerzo. Seguro que sacáis alguna idea.

Perdón por el rollo :P, al final me ha costado mucho más escribir el artículo que implementarlo.

Google Safe Browsing API con PHP, filtra las urls potencialmente peligrosas

Preparando un pequeño nuevo proyecto del que os hablaré en mi siguiente artículo, tuve un problema que me llevó a escribir este otro. En un momento de la aplicación el usuario debe escribir una url (el resto es secreto aún :P). Pues bien, ya durante las pruebas aparecieron los típicos graciosos que utilizan url’s potencialmente peligros, de esas que no hacen cosas nada buenas. Buscando un poco dí con Google Safe Browsing API, el sistema que Google pone a nuestra disposición para comprobar contra sus sistemas si tienen una determinada url recogida dentro de su base de datos de urls peligrosas, la misma que utilizan ellos para lanzar a veces esos avisos de “¿seguro que quieres ver esto?”.

Esta API funciona de modo diferente a la mayoría de aplicaciones de Google que residen directamente en la nube y lanzamos consultas contra su API. En este caso nos provee de su base de datos de urls potencialmente peligrosas para que las guardemos localmente y hagamos las consultas directamente en nuestros sistemas, lo único que debemos hacer es actualizar periódicamente esos datos, eso sí, con algunas limitaciones que nos impone Google, como que no se pueden renovar en intervalos inferiores a 30 minutos. Actualmente esta base de datos tiene más de trescientos mil registros.

Vamos a explicar como utilizarla. Lo primero que debemos hacer es, como en todo lo que hace Google, crear una “API KEY” y una sencilla tabla en nuestro servidor MySQL donde alojaremos la información que nos envíe Google.

CREATE TABLE IF NOT EXISTS `malware` (
`malware_hash` varchar(32) NOT NULL,
PRIMARY KEY  (`malware_hash`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

Creamos ahora una tarea que se ejecutará periódicamente y que leerá, siguiendo las especificaciones adecuadas, los datos que devuelve Google y los insertará/eliminará de nuestra base de datos. Yo, como ya he comentado en otras ocasiones, utilizo Adodb para el acceso a datos.

include(dirname(dirname(__FILE__))."/includes/funciones.inc.php");

$api_key = "TU KEY";
$google_url = "http://sb.google.com/safebrowsing/update";

//open the remote URL
$target = "$google_url?client=api&apikey=$api_key&version=goog-malware-hash:1:-1,goog-black-hash:1:-1";

$handle = fopen("$target", 'r')
	or die("Couldn't open file handle " . $target);
if ($handle) {
    while (!feof($handle)) {
        $line = fgets($handle);
        if (substr($line,0,1) != '[') {
        	$operation = (substr($line,0,1)); //get the '+' or '-'
        	$hash = trim(substr($line,1)); //get the md5 hash
        	$hash = qstr($hash); //escape
        	if ($operation == '+'){
        		$sql = "insert ignore into malware (malware_hash) VALUES ('".$hash."')";
        		$conn->Execute($sql);
        	}else{
        		$sql = "delete from malware where malware_hash = '".$hash."'";
        		$conn->Execute($sql);
        	}
        }
    }
    fclose($handle);
}

Lo que hacemos simplemente es eliminar los hash que nos indica que debemos quitar y añadir los nuevos. Vamos a probar el sistema. Utilicé varias clases y sistemas que había por ahí para utilizar con PHP pero ninguno me parecía lo suficientemente completo hasta que encontré este. Su funcionamiento es muy sencillo. De nuevo está hecho usando Adodb, cámbialo según tus requerimientos.

$class = new GoogleSafeBrowsing('tu_key', true);
$safe=$class->lookupsFor("http://blog.osusnet.com");

Nos devolverá un booleano que indica si la url es segura o no. Si probamos con alguna de éstas obtendremos un bonito false :). Espero que os sirva.

Paradigma Reflection en PHP

Literalmente, el paradigma Reflection es un proceso mediante el cual el software puede observarse a sí mismo, aprender sobre cómo está desarrollado y modificarse automáticamente. ¿Ha quedado claro?. Ya 😛 .

Supongamos que tienes una clase con unos atributos y unos métodos. Supongamos ahora que desde otra clase necesitas averiguar por cualquier razón qué atributos y métodos tiene esa clase primera. Más aún, supongamos que necesitas saber los parámetros de llamada de cada método. ¡No se puede!. Pues te equivocas 😛 , sí que se puede, el paradigma Reflection es el que nos da la solución y viene implementado de serie en PHP con un conjunto de clases que lo dan todo hecho. Una especie de ingeniería inversa.

Estaréis pensando ¿qué utilidad tiene eso? ¿por qué he de necesitarlo?. Bueno, depende de lo que quieras hacer 😛 . Probablemente si sabes que existe y que se puede hacer, algún día recurras a ello. En mi caso necesitaba crear un sistema dinámico para listar un número indeterminado de clases, sus métodos y poder ejecutarlos con todos sus parámetros, es decir, listar todos los parámetros de un método en un formulario, que el usuario pueda rellenarlos y ejecutar ese método de la clase para obtener el resultado. Se podría hacer cargando en una base de datos todas las clases, sus métodos y los atributos de estos, pero habría que hacer un mantenimiento horrible y tenerlo en cuenta, además, cada vez que se modifique algo ya que el sistema está en desarrollo. Reflection nos aporta una solución más elegante.

Nuestro sistema consta de cuatro pasos:

  • Listar las clases disponibles
  • Una vez se selecciona una clase, listar sus métodos públicos.
  • Al escoger un método, listar sus parámetros en un formulario.
  • Ejecutar el método y devolver el resultado al usuario.

1) Lista de clases

Este paso es el sencillo y obvio, en mi caso simplemente listo los archivos que hay en la carpeta donde guardo todas las clases, no hay más truco. Sólo quedaría montar un combo en un formulario para que el usuario escoja la clase a probar.

$clases=array();
$d=dir("./clases");
while (false !== ($entry = $d->read())) {
 //los archivos de clases son del tipo class.nombreClase.php
 $temp=explode(".", $entry);
 $ext=$temp[count($temp)-1];
 if($ext=='php'){
   $clases[]=$temp[count($temp)-2];
 }
}
natcasesort($clases);
$clases=array_merge(array("Escoge Clase"), $clases);

2) Obtención de métodos de una clase

Hacemos ahora uso de la clase ReflectionClass para obtener los métodos públicos de una clase. Hay muchos más métodos dentro de esta clase para consultar muchísimas cosas.

$methods=array();
if(isset($_GET['clase']) && file_exists("class.".$_GET['clase'].".php")){
	require_once("clases/class.".$_GET['clase'].".php");

	$class = new ReflectionClass($_GET['clase']);
	$metodos=$class->getMethods();

	foreach($metodos as $m)
		$methods[]=$m->name;
	$methods=array_merge(array("Escoge Método"), $methods);
}

Como véis, obtenemos otro array con los métodos públicos de la clase seleccionada, listo para montar otro combo con los métodos disponibles.

3) Obtención de los parámetros de un método

Sabiendo ahora el método y la clase que queremos ejecutar sólo tenemos que averiguar sus parámetros de invocación. Parece imposible, pero de nuevo el API Reflection viene a nuestra ayuda.

$parametros=array();
if(isset($_GET['method'])){
 $class = new ReflectionClass($_GET['clase']);
 try{
   $asmth=$class->getMethod($method);
   foreach ($asmth->getParameters() as $i => $param)
     $parametros[]=$param->getName();
 }catch(ReflectionException $e){}
}

Tenemos de nuevo otro array con los parámetros del método. Sólo debemos montar un formulario con tantos campos de texto como parámetros del método para que el usuario pueda probarlo.

4) Ejecutar el método

Voy a complicarlo más. Todas mis clases tienen implementado un singleton, con lo que no se pueden instanciar tal cual 😛 .Podríamos haber hecho algo del tipo:

$class=$_GET['clase']::getInstancia();

Pero PHP no permite utilizar una variable en el nombre de clase al llamar a un método estático, devolvería un error:

Parse error: syntax error, unexpected T_PAAMAYIM_NEKUDOTAYIM

Lo que traducido del hebrero 😐 significaría “Unexpected double-colon“, es decir, en los dos puntos.

Si no tuviésemos el singleton y no fuese necesario hacer la llamada estática que devuelva la instancia, podríamos hacer algo del tipo:

$class=new $_GET['clase'];
$res=$class->metodo($parametros);

Pero seguiríamos teniendo un problema con los parámetros ya que no sabemos cuántos hay que pasar, no podemos montar algo dinámico en una llamada directa a un método.

Lo primero que debemos hacer es llamar al método getInstancia de mi clase para que nos devuelva una instancia a la misma. Para eso recurrimos a la función call_user_func indicándole la clase y el método a ejecutar.

$class=call_user_func(array($_GET['clase'], 'getInstancia'));

Ahora ejecutamos el método correspondiente sobre la clase ya instanciada pasándole los parámetros necesarios en un array, con lo que no importa la cantidad de ellos que haya, irán todos en una lista. Para ello usamos esta vez call_user_func_array.

$params=array();
foreach($_GET as $name=>$param){
 if($name!="clase" && $name!="method" && $name!="submit")
   $params[]=$param;
}
$res=call_user_func_array(array($class, $method), $params);

Eso es todo. Hemos conseguido averiguar los métodos de una clase, sus parámetros y ejecutarlos salvando todos los problemas que nos hemos ido encontrando.

Espero que os sirva de ayuda 🙂 .

Webservices: Tratando con cabeceras SOAP en PHP (2)

Tras el artículo anterior dónde explicaba cómo leer las cabeceras en una respuesta SOAP, he descubierto cómo hacer funcionar el método estándar, seguro que a más de uno le viene bien saberlo.

El problema era que con __soapCall no había manera de que me funcionase la llamada, independientemente de recibir las cabeceras de la respuesta. Ahora sé porqué. El webservice al que estaba llamando está hecho en .NET y parece ser que hay que llamarlo de distinta forma que si se hace invocando al método directamente 😐 .

Si lo llamamos directamente hacemos:

$result = $client->TuMetodo($parametros);

Si lo llamamos con __soapCall haremos:

$result = $client->__soapCall("TuMetodo", array("parameters"=>$parametros), NULL, $reqheaders, $resheaders);

¿Veis la diferencia?

De la segunda forma hay que pasar los parámetros de entrada del método cómo un sólo parámetro “parameters“, es decir, el mismo array que teníamos con la primera manera pero asignándolo a “parameters“.

Eso es todo. Ahora ya funciona y podemos recoger automáticamente los headers de la respuesta SOAP.

Sigo sin saber porqué de esta manera se tiene acceso a las cabeceras y con la invocación directa (la forma recomendada) no. Por el momento voy a seguir utilizando el desarrollo que hice en el primer artículo, la invocación directa me parece más elegante y ya que había conseguido recuperar las cabeceras, ¿por qué cambiarlo ahora? 😛 .

Webservices: Tratando con cabeceras SOAP en PHP

Llevo ya un tiempo bastante liado con webservices a los que debo llamar con PHP y hoy me ha tocado lidiar con cabeceras SOAP. La verdad es que es un mundo bastante oscuro y me he encontrado con muchas trabas. Os contaré cuales y cómo las he solucionado, pero veamos primero algo de teoría.

Los servicios web se han convertido en el principal modo de intercambio de  información entre aplicaciones independientemente de plataformas, sistemas operativos y lenguajes de programación. SOAP es uno de los protocolos sobre los que se realiza el intercambio de los datos y está basado en XML, de manera que la parte cliente interroga al servidor con un código XML en el formato adecuado y recibe la respuesta en otro XML. Para entender de qué estamos hablando veamos la estructura de una petición SOAP y su respuesta.

Llamada (request):

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="com.xplota.ws">
    <SOAP-ENV:Header>
        <ns1:entity>
            <Code>1</Code>
            <Desc></Desc>
        </ns1:entity>
        <ns1:language>
            <Code>1</Code>
            <Desc></Desc>
        </ns1:language>
        <ns1:userId>
            <Code>1</Code>
            <Desc></Desc>
        </ns1:userId>
    </SOAP-ENV:Header>
    <SOAP-ENV:Body>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Respuesta (response):

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <soap:Header>
        <Status xmlns="com.xplota.ws">
            <Code>0</Code>
            <Desc>Ok</Desc>
        </Status>
    </soap:Header>
    <soap:Body>

    </soap:Body>
</soap:Envelope>

Como se puede ver en los listados anteriores, tanto el request como el response constan de dos nodos XML, header y body. El que se utiliza habitualmente es el body (que yo he dejado en blanco pues no nos interesa en este momento) y es el que contendría tanto los parámetros que se envían al webservice en el request como los que devuelve en el response.

Enviando headers soap

En el caso que nos ocupa debía enviar determinados parámetros en el header y leer de allí los potenciales códigos de error si los hubiese habido. El envío, pese a ser una estructura en vez de un parámetro simple, fue sencillo, se define una clase con los parámetros adecuados y se le envía directamente. El motor de SOAP de PHP se encarga de la traducción. Veamos un caso práctico.

//definimos la clase para las cabeceras
class wsHeader
{
    public $Code = 0;
    public $Desc = '';

    public function __construct($code, $desc){
        $this->Code=$code;
        $this->Desc=$desc;
    }
}

//instanciamos el cliente soap
$par=array();
$client = new SoapClient("http://midominio.com/ws?wsdl", $par);

//añadimos las cabeceras a las peticiones
$headers=array();
$headers[] = new SoapHeader("com.xplota.ws", 'entity', new wsHeader(1, ''));
$headers[] = new SoapHeader("com.xplota.ws", 'language', new wsHeader(1, ''));
$headers[] = new SoapHeader("com.xplota.ws", 'userId', new wsHeader(1, ''));
$client->__setSoapHeaders($headers);

//lanzamos la llamada al metodo del ws
$result = $client->TuMetodo($parametros);

Como veis es bastante sencillo de entender. Al añadir una cabecera hay que indicarle el namespace al que pertenece para que el motor SOAP sepa como tratarla, se le da un nombre y el objeto que la contiene.

Con esto hemos solucionado la parte del envío de nuestras cabeceras SOAP y tendremos un request como indicábamos en el primer XML.

Recibiendo headers SOAP

Ahora resulta que el método de nuestro webservice nos responde con otras cabeceras que debemos saber interpretar según el XML de response del segundo listado. Pues tenemos un problema y muy gordo. No hay forma de obtener estas cabeceras, el motor SOAP de PHP sólo devuelve el body, nunca los headers.

Según el manual de PHP el método __soapCall del cliente SOAP permite definir un array en el que se devolverán estas cabeceras, pero no fui capaz de hacer funcionar la invocación de un método del webservice con esta sintaxis mientras que invocándolos directamente en el cliente (cómo la documentación indica que se puede hacer) sí que me funcionaba perfectamente. Es decir, la teoría dice que con el primer método puedo recibir las cabeceras pero no me funcionó mientras que el segundo me funcionaba pero no me devuelve las cabeceras ni hay ningún método para recuperarlas.

Tras pelearme mucho con las funciones SOAP e investigar todavía más no llegué a ninguna conclusión, es como si no le hubiese pasado a nadie, no encontré absolutamente nada útil. Sólo me quedaba una solución, hacer mi propia clase SOAP a partir de la original y procesar el XML del response a mano para obtener los datos que necesitaba. Dicho y hecho. Veamos la solución.

Primero creo mi propia clase de SOAP y compruebo si voy a poder hacer lo que quiero.

class XSoapClient extends SoapClient{
    public function __construct($wsdl, $options){
        parent::__construct($wsdl, $options);
    }

    public function __doRequest($request, $location, $action, $version){
        $response=parent::__doRequest($request, $location, $action, $version);
        return $response;
    }
}
$client = new XSoapClient("http://midominio.com/ws?wsdl", $par);

Parece que voy a tener suerte, si pruebo este nuevo cliente SOAP funciona perfectamente, pero además si compruebo el contenido de $response veo que contiene íntegramente el XML de la respuesta del webservice. Cómo veis lo único que cambia al instanciarlo es que le paso el nombre de la nueva clase. Buen comienzo, si juego bien mis cartas podré sacar las cabeceras en el método __doRequest 🙂 .

Tratemos pues ese XML para obtener lo que buscamos. Gracias a las funciones DOM y XPATH de PHP será muy sencillo. Este es el resultado final de mi cliente SOAP con recuperación de cabeceras:

class XSoapClient extends SoapClient
{
    private $responseHeaders = array();

    public function __construct($wsdl, $options){
        parent::__construct($wsdl, $options);
    }

    public function __doRequest($request, $location, $action, $version){
        $response=parent::__doRequest($request, $location, $action, $version);

        $dom = new DOMDocument;
        $dom->loadXML($response, LIBXML_NOWARNING);
        $path = new DOMXPath($dom);
        $path->registerNamespace('soap', 'http://schemas.xmlsoap.org/soap/envelope/');
        $xml = $path->query('//soap:Header/*');
        $this->responseHeaders=$this->headers2array($xml);

        return $response;
    }

    public function getResponseHeaders(){
        return $this->responseHeaders;
    }

    private function headers2array($response){
        $headers=array();
        foreach ($response as $node) {
            if($node->hasChildNodes()){
                $headers[$node->nodeName]=$this->headers2array($node->childNodes);
            }else{
                $headers[$node->nodeName]=$node->nodeValue;
            }
        }
        return $headers;
    }
}
$client = new XSoapClient("http://midominio.com/ws?wsdl", $par);
$result = $client->TuMetodo($parametros);
$soapheaders=$client->getResponseHeaders();

Problema solucionado y de manera bastante elegante. Si alguien sabe cómo conseguir las cabeceras sin montar todo este lio que me lo cuente por favor.

Acelera tus scripts PHP con eAccelerator

Como ya sabréis la mayoría, PHP es un lenguaje de programación de script, es decir, el código fuente no se compila generando un binario o un bytecode sino que el intérprete de PHP lo lee, compila y ejecuta cada vez que se llama a ese script. No hay que ser muy espabilado para darse cuenta de que este proceso de compilación permanente tiene que tener penalizaciones en cuanto a rendimiento, supongo que se compensa con la facilidad de desarrollo al no tener que compilar cada prueba que se quiere hacer.

Hace ya unos años descubrí eAccelerator, fork del conocido Turck MMCache. La primera vez que lo probé e hice tests de rendimiento no me creía la diferencia que había entre el antes y el después, poco a poco y a base de más pruebas me fui convenciendo y hoy en día es parte fundamental de todos los servidores a los que tengo que meter mano. eAccelerator trabaja compilando los scripts PHP y cacheando esta compilación de manera que la siguiente vez que se solicita está ya compilado y no hay que volver a hacerlo. Es importante hacer notar que maneja automáticamente los cambios de versiones a través de la fecha de modificacion (mtime). Si el script es más nuevo que el que tiene cacheado, lo vuelve a compilar, con lo que, de cara al desarrollador, el sistema sigue teniendo la ventaja de no necesitar compilados ni empaquetados.

La instalación es muy sencilla, descargamos el código fuente desde la web oficial y lo compilamos.

phpize
./configure
make
make install

Ya está el módulo instalado, ahora tendremos que activarlo. En mi CentOS es muy sencillo, simplemente debo añadir un nuevo archivo a /etc/php.d y será leído automáticamente en el siguiente reinicio de Apache. El contenido del archivo será algo así:

[osus@servidor ~]# cat /etc/php.d/eacclerator.ini
zend_extension="/usr/lib/php/modules/eaccelerator.so"
eaccelerator.shm_size="16"
eaccelerator.cache_dir="/tmp/eaccelerator"
eaccelerator.enable="1"
eaccelerator.optimizer="1"
eaccelerator.check_mtime="1"
eaccelerator.debug="0"
eaccelerator.filter=""
eaccelerator.shm_max="0"
eaccelerator.shm_ttl="0"
eaccelerator.shm_prune_period="0"
eaccelerator.shm_only="0"
eaccelerator.compress="1"
eaccelerator.compress_level="9"
eaccelerator.allowed_admin_path="/ruta/a/tu/servidor/de/control"

Todos los parámetros están bien explicados en la documentación oficial. El más importante es el último ya que, como veremos más adelante, indica una ruta dentro de tu servidor web desde donde podrás ver un pequeño panel de control de eAccelerator.

Ahora reinicia Apache y comprueba si funciona de verdad:

[osus@servidor ~]# php -v
PHP 5.1.6 (cli) (built: Nov 12 2008 11:22:34)
Copyright (c) 1997-2006 The PHP Group
Zend Engine v2.1.0, Copyright (c) 1998-2006 Zend Technologies
    with eAccelerator v0.9.5.2, Copyright (c) 2004-2006 eAccelerator, by eAccelerator

Parece que sí. Aprovecharemos para configurar la interfaz web que comentaba que nos permite controlar algunas funcionalidades. Para ello copiamos desde la carpeta con el código fuente el archivo control.php a la ruta indicada anteriormente y configuramos los parámetros de autenticación que nos pedirá, usuario y clave, al principio de este archivo.

if (!function_exists('eaccelerator_info')) {
    die('eAccelerator isn't installed or isn't compiled with info support!');
}

/** config **/
$user = "osus";
$pw = "bombona";
/** /config **/

Ya está, sólo queda acceder a esta ruta web para ver algo semejante a esta imagen: información del estado de la caché, posibilidad de desactivar el cacheo, limpiar caché…

Eaccelerator admin

Analizando el rendimiento

Todo esto está muy bien y suena muy bonito, pero ¿mejora de verdad el rendimiento? ¿qué beneficios reales obtenemos? La pregunta del millón.

Utilizaremos Apache Benchmark para hacer pequeñas pruebas de carga sobre el servidor. Esta utilidad viene siempre con Apache, con lo que no tendrás que instalar ningún software adicional. La sintaxis es muy sencilla:

ab -n 100 -c 5 http://dominioentuservidor.com/script.php

Con esto estaremos diciendo a “ab” que lance 100 peticiones contra la url indicada con una concurrencia máxima de 5 solicitudes. El resultado seria algo similar a esto:

[osus@servidor ~]# ab -n 100 -c 5 http://tudominio.com/
This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright 2006 The Apache Software Foundation, http://www.apache.org/

Benchmarking tudominio.com (be patient).....done

Server Software:        Apache
Server Hostname:        tudominio.com
Server Port:            80

Document Path:          /
Document Length:        125397 bytes

Concurrency Level:      5
Time taken for tests:   52.549041 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      12559900 bytes
HTML transferred:       12539700 bytes
Requests per second:    1.90 [#/sec] (mean)
Time per request:       2627.452 [ms] (mean)
Time per request:       525.490 [ms] (mean, across all concurrent requests)
Transfer rate:          233.40 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      195  232  45.7    211     417
Processing:  1687 2356 401.0   2257    3609
Waiting:      342  620 240.6    546    1756
Total:       1933 2589 400.7   2487    3815

Percentage of the requests served within a certain time (ms)
  50%   2487
  66%   2611
  75%   2798
  80%   2903
  90%   3090
  95%   3475
  98%   3791
  99%   3815
 100%   3815 (longest request)

Bien, pues a partir de esta información he hecho un pequeño estudio estadístico para valorar el cambio de rendimiento.  Se hicieron dos baterías de pruebas, primero con eAccelerator desactivado y después activado. En cada grupo se lanzaron diez tests utilizando “ab” con 100 peticiones. Vale, sí, la muestra es pequeña, pero veréis que suficiente para ver el cambio. Estos fueron los resultados.

eaceleratorstats.gif

La última fila establece los porcentajes de las pruebas con eAcclerator respecto de no utilizarlo. Interpretemos los resultados:

  • Tiempo total: utilizando el acelerador las pruebas se han ejecutado en el 73% del tiempo empleado al no utilizarlo. Eso supone un 27% menos de tiempo, una cifra nada despreciable.
  • Peticiones por segundo: con eAccelerator se ejecuta un 36% más de peticiones por segundo que sin él. Sobran más comentarios.
  • Tiempo mínimo: El tiempo mínimo empleado para atender una petición desciende un 22% al utilizar eAccelerator.
  • Tiempo máximo: este valor es sumamente llamativo, con eAccelerator se necesita un 42% menos de tiempo en el peor de los casos.

¿Necesitas más argumentos? 😛 . Otro día hablaremos de memcached.