Domina rápidamente Adobe Air

Leo en el blog de Mario Casario esta entrada donde anuncia la disponibilidad gratuita y bajo licencia Creative Commons de la guía Adobe AIR 1 for JavaScript Developer en formato PDF.

Para los que no lo sepáis, Air es la tecnología de Adobe para desarrollar rápidamente aplicaciones de escritorio utilizando otras tecnologías ya existentes: HTML, Javascript y Flash. Así de sencillo, aplicando lo que ya sabes puedes generar aplicaciones de escritorio multiplataforma (Windows y Mac ahora mismo, Linux en camino).

Air está de moda y más desde la compra de Twhirl por parte de Seesmic, de hecho Twitter es de lo más utilizado para crear aplicaciones Air.

Con Air puedes crear rápidamente pequeñas aplicaciones con acceso al sistema de archivos local, bases de datos locales (SQLlite), arrastrar y soltar… todo lo que necesitas para crear tus aplicaciones.

Os aseguro que es increíble lo que se puede hacer con poquísimas líneas de código.

Movilizando Joomla

Hace unos días me llegó desde la lista de correo de dev.mobi un interesante artículo sobre la creación de sitios para móviles con Joomla.

No es que me encante Joomla, de hecho probablemente haga un artículo crítico al respecto, pero creo que es muy interesante para determinados websites por su aparente sencillez. El problema que tenía es que quería una solución que me permitiese, a partir de un portal web, tener acceso desde el móvil a una versión adaptada. Gracias a este artículo encontré este plugin que prácticamente te lo da todo hecho.

Hay algo, sin embargo, que no tiene en cuenta el plugin: las imágenes. Las imágenes que insertas en tus artículos serán, normalmente, de un tamaño bastante elevado, sobre todo si hablamos de terminales móviles donde la media es de 174px de ancho de pantalla. La solución pasa por toquetear un poco el plugin haciendo que las imágenes de los articulos pasen a través de un redimensionador automático que crearemos nosotros. Os recomiendo los artículos que escribí acerca del escalado de gifs transparentes y animados (aquí y aquí) para tener una idea de como hacerlo. La solución llega en dos pasos

1) Parcheando el plugin

Este es el cambio que debemos hacer en el archivo pdabot.php del plugin. Busca al final de todo la función onAfterRenderer y haz que quede asi:

function onAfterRender()
{
	global $mainframe;
	//DESDE AQUI
	if($GLOBALS['ispda']==true){
		$body = JResponse::getBody();
		$body = preg_replace( '/<img src="(.*)"(.*)>/i', '<img src="/redimensionar.phtml?imagen=\1">', $body );
		$body = preg_replace( '/<img(.*)width="(.*)"(.*)>/i', '<img\1\3>', $body );
		$body = preg_replace( '/<img(.*)height="(.*)"(.*)>/i', '<img\1\3>', $body );
		JResponse::setBody($body);
	}
	//HASTA AQUI

Como ves, sustituimos el atributo src de todas las imágenes por nuestro script de autoescalado redimensionar.php al que le pasamos la url original de la imagen. Además aprovechamos para quitar los atributos de width y height porque en este punto no sabemos cuales serán los resultantes, si los dejásemos se vería a su tamaño original. Hemos terminado con el plugin.

2) Escalando las imágenes

Si has entendido todo el proceso hasta aquí estarás pensando, vale, pero nos falta un dato: ¿a qué tamaño escalamos las imágenes?. En efecto, no lo sabemos… todavía.

Y aquí viene W urfl en nuestra ayuda. Wurfl es una base de datos de características de terminales móviles que te permiten conocer datos como el ancho de pantalla simplemente pasándole el UserAgent del mismo. No voy a explicar el funcionamiento de Wurfl puesto que se sale del alcance de este artículo, pero en su web tienes todo lo necesario.

Crearemos entonces nuestro redimensionar.php donde simplemente buscamos mediante Wurfl el ancho de pantalla del terminal cliente y reescalamos la imagen al tamaño adecuado. Es recomendable no hacerlo al 100% para evitar que los scrolls verticales reduzcan el espacio útil y nos aparezca también el scroll horizontal (suele pasar en los Nokia). Yo suelo descontar 10px al ancho. Para imágenes de artículos puedes aplicar un escalado porcentual, pueden no quedar bien imágenes muy grandes, déjalas al 70% por ejemplo.

Conclusiones

Rápidamente y de un modo sencillo hemos adaptado para terminales móviles tu portal en Joomla, no se puede pedir más. No olvides diseñar la plantilla pda a tu gusto y necesidades.

En webs móviles es habitual utilizar una imagen como cabecera del portal. Puedes utilizar también tu redimensionador para adaptarla automáticamente al ancho de pantalla de los clientes.

Se podrían hacer muchas más cosas si integramos Wurfl directamente en el plugin, incluso podríamos hacer que el código generado estuviese adaptado a las capacidades del terminal del cliente (xHTML, iMode, WML) creando plantillas para cada lenguaje distinto. ¿Te atreves a hacerlo tu?

Flash Player, sockets y políticas de seguridad

El pasado miércoles 9 de abril Adobe liberó una actualización de su Flash Player numerada como 9.0.124. Esta noticia pasaría completamente desapercibida si no fuese por las implicaciones que tiene. De hecho no entiendo como una actualización con los cambios que conlleva se ha numerado como una actualización menor y no se le ha dado más importancia.

El cambio fundamental de esta nueva versión radica en el modelo seguridad de la máquina virtual Flash al de acceder a sockets remotos y al utilizar headers HTTP. Recordemos que la opción de abrir sockets binarios y conectarse a ellos permitiendo aplicaciones bajo cualquier protocolo apareció en julio de 2006 junto a la versión 9 de Flash Player. Hasta ahora el modelo de seguridad de sockets era el mismo que para cualquier llamada remota bajo Flash, el famoso crossdomain.xml. Según esto, para acceder a cualquier archivo que no resida bajo el mismo host que sirve el swf que hace la llamada, el host servidor debe autorizar a la maquina virtual Flash la carga de ese archivo a través de ese crossdomain.xml, si no existe o no se autoriza el acceso, no se podrá acceder. Este es, por tanto, un archivo de políticas de seguridad y decide quién puede conectarse con Flash a tu servidor. A mi, personalmente, siempre me ha parecido algo ridículo, es como si para cargar un archivo de cualquier host desde tu navegador tuviesen que autorizártelo, pero siempre habrá quien encuentre ventajas en la seguridad. Si el archivo está publicado en Internet se supone que ya estas autorizado a leerlo, ¿para qué complicar más todo el proceso requiriendo más políticas de seguridad?. Y vaya si se complica…

Hasta ahora los sockets se trataban como archivos en lo que a políticas de seguridad se refiere, es decir, el crossdomain.xml del host al que conectábamos nos autorizaba el acceso igual que con cualquier llamada http. Si tienes una aplicación que utiliza sockets y actualizas a Flash Player 9.0.124 verás que ahora no te funciona, te salta una excepción de seguridad. Los chicos de Adobe se han inventado un sistema de políticas de seguridad exclusivo para sockets e independiente de las llamadas http.

Desde Adobe se han currado una ayuda/explicación de 7 páginas. Ninguna pega a no ser porque llegas al final de la lectura con la pregunta, vale, pero ¿qué tengo que hacer para que funcione? Es decir, mucha lectura pero poca, muy poca ayuda, da la impresión de que repiten lo mismo en todas las páginas sin aclarar nada.

La solución es sencilla a la par que curiosa y complicada para muchas aplicaciones. Debes crear una pequeña aplicación que, bajo el puerto 843 por defecto (aunque puedes usar otros puertos), debe responder a las peticiones de acceso con el crossdomain.xml. La mayoría de la gente ha entendido que debes tener tu servidor web a la escucha en el puerto 843 y devolver el crossomain.xml desde allí (como veis no está clara la documentación), pero NO es eso, no es una llamada HTTP la que te va a hacer el swf para pedir autorización. Veámoslo con un ejemplo práctico. Supongamos que fuese una solicitud HTTP, a grosso modo haríamos:

telnet www.tuhost.com 843
GET /crossdomain.xml HTP/1.0
HTTP/1.x 200 OK
Date: Tue, 15 Apr 2008 17:24:26 GMT
Server: Apache
Last-Modified: Thu, 28 Oct 2004 21:57:25 GMT
Content-Length: 128
Content-Type: text/xml

<xml version="1.0" encoding="UTF-8">.....</xml>

El xml final sería nuestro crossdomain.xml. Esto sería lo mismo que si cargamos el crossdomain.xml desde el puerto 80. Vale, pues esto es lo que NO va a hacer el swf, por tanto, no sirve. Esto es lo que va a hacer:

telnet www.tuhost.com 843
<policy-file-request/>
<xml version="1.0" encoding="UTF-8">.....</xml>

La diferencia es evidente, en el segundo caso, al contectar con el servidor simplemente le decimos <policy-file-request/> y nos debe devolver el xml del crossdomain.xml. No es el protocolo HTTP, es uno inventado que sólo responde a una petición.

El puerto no tiene porqué ser el 843, puedes modificarlo, pero Adobe pretende que 843 sea el estándar y, de hecho, está solicitando su homologación con el IANA.

Desde Adobe incluso se han puesto cachondos y te dicen que lo ideal es modificar tu aplicación en el servidor para que acepte la llamada <policy-file-request/> y responda con el XML, así no necesitarás una aplicación adicional. Pues nada chicos, a modificar todos los servidores smtp, pop, irc (nuestro caso), ftp… para que acepten la llamada. ¡Qué ridículo!

Como soy un tío enrollado os paso la solución completa. En esta URL tenéis un ejemplo de servidor en TCL/TK que funciona a la perfección y no consume recursos. Configura el puerto y la ruta al crossdomain.xml y a funcionar. Recuerda que si el puerto es inferior al 1024 deberá ser un usuario privilegiado quién ejecute la aplicación, deberías, entonces, enjaularla (chroot) para evitar posibles puertas. En realidad lo que hacemos es responder con el crossdomain.xml a cualquier petición que nos hagan, sea la correcta o no, ¿qué mas da? No vamos a hacer absolutamente nada más con ese puerto. Tendréis que preocuparos también de que se quede residente como demonio en vuestro servidor y, además, habrá que comprobar periódicamente que sigue a la escucha. Para esto recomiendo hacer un script que haga llamadas reales y espere el código XML necesario, comprobar simplemente con un netstat que el servidor ocupa el puerto no nos salvará de que el servidor se quede zombie. Por supuesto no olvides abrir tu firewall a ese puerto.

Actualización.
Acabo de terminar el artículo y a través de flexcoders me llega este enlace, mucho más claro y sencillo que la ayuda inicial. Ah! y con ejemplos de servidores en Perl y Python. En líneas generals viene a contar lo mismo que acabo de contar yo :P.

Cobrando por servicios a través de PayPal con PHP

Hace un tiempo necesitábamos crear un sistema de compras a través de PayPal con la particularidad de que teníamos recibir confirmación instantánea del pago para reflejar esas compras al usuario. Esta era la diferencia principal respecto de un carro de compra de una web donde vendes algo físicamente, en este caso no necesitas saber el estado de una compra, cuando el equipo de almacén prepare el pedido ya comprobará si se ha realizado el pago antes de servirlo. Nuestro caso es más complicado. Vendemos servicios que pueden ser suscripciones, créditos de uso en la web, servicios premium, acceso a zonas privadas… todo este tipo de opciones donde el usuario, después de pagar, vuelve a tu web para disfrutar de los servicios que ha comprado.

Al querer cobrar a través de PayPal tienes dos opciones, o creas botones estáticos (Comprar Ahora) desde la web de PayPal o generas los tuyos propios. El problema de crearlos estáticos es que debes crear uno por cada usuario y para cada servicio que necesites vender, algo absurdo para el caso que tratamos ya que estarías ante miles de botones. Al generar los tuyos propios puedes hacerlos normales o seguros, para que no se puedan modificar por el camino. Este es el método que deberías utilizar normalmente ya que, en los normales, un usuario experimentado puede modificar el importe a cobrar, algo que no deseas que ocurra.

Para que el sistema funcionase bien deberíamos recibir en una URL las compras realizadas de alguna manera que nos permitiese identificar al usuario que habia hecho la compra para añadirle los servicios en cuestión. La solución es crear botones firmados online utilizando como identificador de producto de compra el idUsuario de tu base de datos, de esta forma las confirmaciones de compra te indican el usuario al que pertenecen y ya puedas darle los servicios por los que ha pagado. Puedes utilizar combinaciones más elaboradas para reutilizar el sistema para distintos productos, por ejemplo idUsuario-idServicio (12345-3). Al recibirlo, con dividir la cadena por el guión ya tienes todo lo necesario. En nuestro caso había varios tipos de servicio pero se indentificaban por el coste del mismo, con lo cual con el identificador de usuario teníamos suficiente.

Aparentemente es sencillo el proceso, pero se complica ya que la documentación, a pesar de ser extensa, no esta del todo claro. Por un lado no está nada claro cómo configurar tu cuenta de PayPal para tener el sistema completo y por otro es bastante lioso el modo de crear los botones y recibir las confirmaciones. Vamos a explicarlo detalladamente ya que es un interesante ejercicio de programación en PHP.

Configurando la cuenta de PayPal

Lo primero que tienes que hacer es crear un certificado público X.509, único sistema que acepta PayPal.

Las instrucciones detallas las tienes aquí. básicamente necesitas tener openssl funcionando, cualquier sistema Linux lo tendrá instalado y sino tienes versiones para Windows.

openssl genrsa -out my-prvkey.pem 1024
openssl req -new -key my-prvkey.pem -x509 -days 3650 -out my-pubcert.pem

Con esto generamos primero la clave privada y a continuación el certificado público X.509 de esa clave. Es importante el parámetro -days, lo hemos puesto a 10 años para no tener problemas. En mi caso, la primera vez puse 365 días tal como viene en el ejemplo y al año dejó de funcionar sin que te dieses cuenta, fueron los usuarios los que nos avisaron. Guarda los dos archivos, my-prvkey.pem y my-pubcert.pem pues los necesitaremos a continuación.

Ya tenemos nuestro certificado preparado. Ahora debemos subirlo a PayPal para que sepa desencriptar nuestros botones. En tu cuenta de PayPal debes ir a:

Perfil->Configuración de pago codificado

Desde ahí, por un lado descargas el certificado público de PayPal, lo necesitaremos para codificar nuestros botones, y por otro subes tu certificado público que hemos llamado my-pubcert.pem. Obtendrás un ID de certificado, apúntatelo.

Si todo ha ido bien, ahora debemos configurar la cuenta para que PayPal sólamente acepte botones firmados, de manera que nadie pueda suplantar nuestra identidad con botones a otros precios, un detalle muy importante. Vamos entonces a:

Perfil->Preferencias de pago en el sitio Web

Primero activas Transferencia de datos de pago y te copias el Código Personal de Identidad que te indica, lo necesitaremos más adelante. A continuación, en la sección Pagos en el sitio Web codificado activamos la opción Bloquear pago en el sitio Web no codificado. Como véis no estaba tan claro el proceso.

Este paso no es obligatorio, pero haciéndolo conseguimos que el usuario vuelva a tu web una vez haya realizado el pago y, si ha sido correcto, tenga ya sus servicios disponibles. Activa la opción Retroceso automático e indica la URL de devolución, es decir, la URL donde será devuelto tu cliente, por ejemplo: http://www.tudominio.com/index.php?accion=creditos_paypalok

Finalmente activaremos la opción que nos permitirá recibir notificaciones de pagos online. Para ello vamos a:

Perfil->Preferencias de Notificación de pago instantánea

Y activaremos la notificación indicando la URL donde las vamos a recibir, por ejemplo, http://www.tudominio.com/secure/paypal/ipn.php.

Si has llegado a este punto, ya tienes todo lo necesario para comenzar con el código. Como recordatorio, necesitas:

  • Tu clave privada, my-prvkey.pem.
  • Tu certificado público, my-pubcert.pem.
  • Tu ID de certificado en PayPal
  • Tu Código Personal de Identidad
  • Certificado público de Paypal, paypal_cert_pem.txt.

Nos quedan, entonces, tres tareas pendientes:

  • Crear botones de compra
  • Crear el script de recepción de cobros realizados
  • Crear el script de vuelta después de una compra

Creando los botones

Comenzamos con el código. El único requerimiento es que tu instalación de PHP debe tener configurada la extensión openssl imprescindible para trabajar con los certificados. Con esta clase que encontré en su momento (me costó bastante localizar algo sencillo) tienes todo el proceso automatizado, sólo debes preocuparte por indicarle los datos que hemos ido guardando, los certificados y el ID del tu certificado en PayPal, simple ¿no?.

include("Class.PayPalEWP.php");
$paypal = &new PayPalEWP();
$paypal->setTempFileDirectory("/tmp");
$paypal->setCertificate("my-pubcert.pem", "my-prvkey.pem");
$paypal->setCertificateID("XXXXXXXXXX");
$paypal->setPayPalCertificate("paypal_cert_pem.txt");

$paypalParam = array(
    'cmd' => '_xclick',
    'business' => '[email protected]',
    'item_name' => 'Comprar Servicio X,
    'item_number' => $_SESSION['idUsuario'],
    'amount' => '5',
    'no_shipping' => '1',
    'currency_code' => 'EUR',
    'lc' => 'ES',
);
$form5="<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
            <input type="hidden" name="cmd" value="_s-xclick"/>
            <input type="hidden" name="encrypted" value="-----BEGIN PKCS7-----n".$paypal->encryptButton($paypalParam)."n-----END PKCS7-----"/>
            <input type="image" src="imagenes/comprar_paypal.gif" border="0" name="submit" alt="Realice pagos con PayPal: es rápido, gratis y seguro." style="border:0;">
        </form>";

El código es bastante claro.
Ya lo tenemos, $form5 contiene el código de tu botón.
Los parámetros importantes son:

  • item_name: informativo, para que tu cliente sepa lo que compra. Por ejemplo: compra de suscripción a noticias.
  • item_number: Aquí configuramos el idUsuario de tu cliente en tu base de datos, así sabes quién compra.
  • amount: Precio que le cobras, en este caso 5 euros.

Modifica estos y los demás parámetros para reflejar tus opciones y adecuarlos a tu aplicación. Al sacar el código de $form5 en tu página tienes listo el sistema de compra.

Recibiendo las notificaciones

La recepción de notificaciones es la piedra angular del sistema para estar seguros de que un cliente ha pagado por un servicio. PayPal ha pensado que puedes llegar a tener problamas temporales de conectividad que te impidan reconocer una compra, con lo cual obliga a que le confirmes que has recibido la confirmación reenviándole los mismos parámetros que te ha enviado. Un sistema curioso pero efectivo, no puedes suplantarlo puesto que no sabes los identificadores de operación que te va a enviar, así que sólo el receptor de la confirmación podrá confirmar la recepción. Lo que hacemos es crear una solicitud HTTP POST con todos los parámetros que nos ha enviado y se la devolvemos a PayPal. Si todo ha ido bien recibiremos un VERIFIED y podremos aceptar esa transaccion como válida y hacer el procesado que estimemos oportuno, comenzando por comprobar la duplicidad de la transacción ya que PayPal podría estar reenviándola y terminando por otorgar al usuario el servicio por el que ha pagado.

// read the post from PayPal system and add 'cmd'
$req = 'cmd=_notify-validate';
foreach ($_POST as $key => $value) {
    $value = urlencode(stripslashes($value));
    $req .= "&$key=$value";
}

// post back to PayPal system to validate
$header .= "POST /cgi-bin/webscr HTTP/1.0rn";
$header .= "Content-Type: application/x-www-form-urlencodedrn";
$header .= "Content-Length: " . strlen($req) . "rnrn";
$fp = fsockopen ('www.paypal.com', 80, $errno, $errstr, 30);

// assign posted variables to local variables
$item_name = $_POST['item_name'];
$item_number = $_POST['item_number'];
$payment_status = $_POST['payment_status'];
$payment_amount = $_POST['mc_gross'];
$payment_currency = $_POST['mc_currency'];
$txn_id = $_POST['txn_id'];
$receiver_email = $_POST['receiver_email'];
$payer_email = $_POST['payer_email'];
$transid=$_POST['txn_id'];
$idUsuario=$_POST['item_number'];
$cantidad=$_POST['mc_gross'];
$creditos=100;

if (!$fp) {
    //CONTROL DE ERRORES; NO SE PUEDE CONECTAR CON PAYPAL
    //NO ES GRAVE, COMO NO LE CONFIRMAMOS LA TRANSACCION
    //ELLOS MISMOS LA REINTENTARÁN MÁS ADELANTE
}else{
    fputs ($fp, $header . $req);
    while (!feof($fp)) {
        $res = fgets ($fp, 1024);
        if (strcmp ($res, "VERIFIED") == 0) {
            //compruebo que no se haya procesado ya la transaccion
            $query="select * from paypal where transid='$transid' and estado=1";
            $rs=$conn->Execute($query);
            $sumar=$rs->recordcount();
            if($sumar==0){
                //LOGEAMOS TODA LA TRANSACCION
                $vars="GET: ".serialize($_GET)."rnPOST: ".serialize($_POST)."";
                $query="insert into paypal (transid, fecha, estado, variables)
                    VALUES ('$transid', now(), 1, '$vars')";
                $rs=$conn->Execute($query);

                //aquí debes hacer ahora tus operaciones
                //para conceder el servicio al usuario: $idUsuario
                //incluso comprobar que idUsuario es válido
            }else{
              //TRANSACCION DUPLICADA, NO HACEMOS NADA
            }
        }else if (strcmp ($res, "INVALID") == 0) {
            //CONTROL DE ERRORES
        }
    }
    fclose ($fp);
}

Ya hemos recibido la confirmación de pago de PayPal, la hemos guardado en nuestra base de datos y le hemos dado a nuestro cliente su servicio, este proceso será distinto para cada aplicación así que no lo explicaremos, haz el tuyo como creas oportuno. Lo que sí te recomiendo es guardar una tabla con todas las transacciones recibidas a modo de log, te servirá para buscar errores o reclamaciones de usuarios.

Cabe señalar que PayPal sólamente lanza este proceso con las transacciones correctas, aquellas se que se han cobrado correctamente, nunca con las erróneas (falta de saldo, tarjeta incorrecta, etc.).

Devolviendo al usuario a nuestra web

Una vez que el usuario ha terminado su transacción en PayPal deberíamos enviarlo de nuevo a nuestra web para que comience a utilizar el servicio por el que ha pagado. Para ello PayPal nos provee del método Retroceso automático que mandará al usuario a la URL que le hayamos especificado indicando los parámetros de la transacción, entre ellos si ha sido válida o no. PayPal, además, ha pensado en todo: no puedo devolver a un usuario a la web de origen sin antes haberle comunicado el éxito de la transacción (el paso anterior). Así el mecanismo de PayPal retrasa el reenvío del usuario unos segundos para intentar que tu servidor ya esté informado de esa transacción. Simplemente genial. Ahora utilizaremos tu código personal de identidad que hemos obtenido al configurar la cuenta en PayPal. Es el parámetro que en el código llamamos $auth_token.

El proceso es parecido a la confirmación de transacciones. Debemos comunicar a PayPal que hemos recibido al petición de vuelta del usuario y que somos nosotros quién lo hacemos, sólo así nos dará los datos de la transacción. De nuevo un método curioso. Para hacerlo, nos indica por GET el identificador de la transacción y debemos devolvérselo junto a nuestro código personal de identidad mediante otra llamada HTTP POST, de este modo nos aseguramos de que somos nosotros quienes solicitamos la información. Si todo ha sido correcto PayPal nos devuelve los datos de la transacción como respuesta a esta llamada. Otra vez genial el sistema.

¿Por qué no hacerlo como en el paso anterior? Sencillo, el método IPN es transparence al usuario, es una llamada interna que hacen los sistemas de PayPal a los tuyos, nadie va a saber que datos se envían, con lo cual puedes utilizar esos datos para confirmar la transacción. En el método de retroceso, esta URL llega al usuario, con lo que podría hacer cosas que no deseamos con los datos, con lo que no nos envían nada en la solicitud, simplemente el identificador de la transacción para que internamente nosotros pidamos los datos validándonos con el código personal.

//read the post from PayPal system and add 'cmd'
$tx_token = $_GET['tx'];
$auth_token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
$req = 'cmd=_notify-synch';
$req .= "&tx=$tx_token&at=$auth_token";
$header .= "POST /cgi-bin/webscr HTTP/1.0rn";
$header .= "Content-Type: application/x-www-form-urlencodedrn";
$header .= "Content-Length: " . strlen($req) . "rnrn";

$fp = fsockopen ('www.paypal.com', 80, $errno, $errstr, 30);
$isError=0;
if(!$fp) {
    $isError=2;//error HTTP
}else{
    fputs ($fp, $header . $req);
    // read the body data
    $res = '';
    $headerdone = false;
    while (!feof($fp)) {
        $line = fgets ($fp, 1024);
        if (strcmp($line, "rn") == 0) {
            // read the header
            $headerdone = true;
        }else if ($headerdone){
            // header has been read. now read the contents
            $res .= $line;
        }
    }    // parse the data
    $lines = explode("n", $res);
    $keyarray = array();
    if (strcmp ($lines[0], "SUCCESS") == 0) {
        for ($i=1; $i<count($lines);$i++){
            list($key,$val) = explode("=", $lines[$i]);
            $keyarray[urldecode($key)] = urldecode($val);
        }

        $isError=0;//no error
        $nombre = $keyarray['first_name']." ".$keyarray['last_name'];
        $producto = $keyarray['item_name'];
        $amount = $keyarray['payment_gross'];
        $idUsuario=$keyarray['item_number'];
        $cantidad=0+$keyarray['mc_gross'];
        $estado=$keyarray['payment_status'];
        $transid=$keyarray['txn_id'];

        //ahora ya puedes evaluar lo que necesites de tu transacción
        //y termina informando al usuario de que todo ha ido bien y ya tiene su servicio
    }else if (strcmp ($lines[0], "FAIL") == 0) {
        $isError=1; //error de transaccion
    }
}
fclose ($fp);

La respuesta que recibimos es una respuesta HTTP estandar, con lo cual debemos ser conscientes de que vamos a recibir primero todas las cabeceras HTTP de la respuesta, después dos saltos de línea y a continuación la respuesta propiamente dicha. En una petición normal esta respuesta sería el código HTML de tu página, pero en este caso recibimos la lista de parámetros de la transacción, uno por línea y del tipo:

parámetro1=valor1
parámetro2=valor2
...

El código tiene esto en cuenta y, al recoger la respuesta, regenera la lista de parámetros/valores recibidos con lo que tenemos los datos necesarios.
La primera línea va a ser SUCESS ó FAIL, está claro el dato, una indica que la transacción ha sido válida y la otra que no. Obviamente debes informar al usuario de que la operación ha sido correcta y que ya tiene su servicio disponible.

Si has entendido bien todo lo explicado hasta ahora, verás que los sistemas de recepción de notificaciones y retroceso automático son muy similares, de hecho puedes utilizar este último para validar las transacciones de igual modo que con el primero y no necesitarías éste. Pero ¿qué ocurriría si sólo implementas el segundo y el usuario cierra la ventana del pago mientras está en esos segundos de espera antes de reenviarlo a tu web? Simplemente que el usuario no tendría su servicio disponible ya que nunca ha llegado a realizar el proceso del Retroceso automático. Para solucionarlo implementamos los dos. Con el primero aseguramos que la mayoría de transacciones válidas son procesadas y los servicios otorgados al cliente, con la segunda, además de informar al cliente del éxito o fracaso se su operación, tenemos un sistema de redundancia por si el primer procedimiento hubiese fallado. Como en ámbos tenemos el identificador de transacción, simplemente debemos comprobar que esa transacción ya existe y no hacer nada o procesarla si no existe.

Y eso es todo amigos. Con esto hemos aprendido a tener un sistema de compras de servicios seguro y dinámico en PayPal. Cómo habéis podido observar, el procedimiento es bastante complejo en cuanto a configuración y desarrollo, pero es muy interesante el estudio del resultado para tener una idea más amplia de como implementar sistemas seguros.

Excursiones de fin de semana: Morella y Sant Mateu

Tenía una deuda pendiente con Morella. Llevaba mucho tiempo con ganas de ir y al final lo hemos conseguido.

Morella está situada al norte de la provincia de Castellón, a la altura de Vinaroz pero al interior, casi ya en Teruel, en plena zona del Maestrazgo en la comarca de Les Ports. Fue declarada conjunto Histórico-Artístico en 1963 y actualmente trabaja para su reconocimiento por la UNESCO como Patrimonio de la Humanidad.

Fijáos en la impresionante panorámica que te recibe nada mas llegar a Morella. Se puede apreciar la muralla medieval que rodea la ciudad coronada con el castillo.

Vista panorámica de Morella

Morella es de esos lugares en los que te sientes transportado a épocas pasadas. Cabe decir que esta ciudad ha sufrido todas las invasiones y guerras habidas y por haber: celtas, griegos, romanos, bereberes, reconquista, Guerra de Sucesión, independencia contra las tropas napoleónicas, guerras Carlistas… No se ha librado de nada. Imaginaos la historia que encierran sus rincones.

Acueducto medieval de MorellaPortal de Sant Miquel, MorellaVista rasera del castillo desde el exterior de la murallaPortal de Sant Miquel desde el interior, MorellaPintorescas calles de MorellaBasílica de Santa Maria la Mayor, MorellaCuadro pintoresco de Morella con cruceroConvento de San Francisco, MorellaClaustro del Convento de San Francisco, Morella

Si a algo le he de poner pegas es al acceso al Convento de San Francisco y al Castillo. Se realiza desde el propio Convento, pero apenas hay un pequeño cartel colgado de un caballete de pintor en la puerta. Si no es porque vimos salir gente, no nos hubiésemos acercado. Por 2€ tienes acceso a los dos edificios. Eso sí, prepárate para caminar y caminar, la subida al castillo, pronunciada y más larga de lo que parece, se hace pesada. El paseo completo llevará alrededor de 45 minutos y cuando ya crees que estás cerca de la Plaza de Armas te encuentras con un largo camino de escaleras que te hace llegar derrotado.

Vista del Castillo de Morella desde el Claustro del Convento de San FranciascoCañon en el interior del Castillo de MorellaAcceso a la Plaza de Armas del Castillo de MorellaPlaza de Armas del Castillo de MorellaMorella desde el Castillo con la Porta de Sant Miquel y las murallas al fondoPrisión del Castillo de MorellaRuínas de las salas del Convento de San Francisco desde la subida al Castillo de MorellaCamino de subida al Castillo de MorellaVista del Castillo de Morella desde el Claustro del Convento de San Franciasco

Del castillo cabe decir que fue construido aprovechando la propia montaña, con varias dependencias excavadas en la propia roca de la montaña, una de ellas los calabozos.


Entrada al Monasterio de San Francisco y Castillo de MorellaCalle pintoresca de MorellaCalle pintoresca de Morella

Sant Mateu

Aprovechando el viaje de vuelta y por consejo de Álvaro, alias Rachandras, hicimos una parada en Sant Mateu, declarado Monumento Nacional. La verdad, he de decirlo, esperábamos más, y, si tienes en cuenta que bajábamos de Morella, te parece menos aún de lo que en realidad es. Aún así, es un buen lugar para hacer una parada en el camino.

Murallas de Sant MateuCalabozos, Presión del DemonioFont de la Mare de Deu, Sant Mateu

Lo mejor, sin duda, las murallas medievales que caen hacia el lado del río y fueron restauradas en 1996.

Y eso es todo por hoy, de vuelta a casa a descansar un poco que al día siguiente tocaba trabajar.

Modelando bases de datos con MySQL Workbench

Si hay una herramienta de diseño de bases de datos conocida dentro del software libre, ésta es, sin duda, DBDesigner. Si a estas alturas no la conoces es que no diseñas bases de datos o lo haces sobre la marcha, tu sabrás lo que haces :).

Hace algunos años MySQL compró Fabforce, la empresa que desarrollaba aquél software. No he podido encontrar la fecha exacta, pero ya en agosto de 2005 se anunciaba el lanzamiento de la primera alpha de su sucesor, MySQL Workbench. Obviamente algo tiene que haber trastocado los planes iniciales de lanzamiento puesto que en abril de 2008 estamos todavía con versiones beta, eso sí, en estado ya Release Candidate. Cabe señalar que la última versión de DBDesigner es nada más y nada menos que de 2003, y aún así se sigue utilizando.

Lo interesante de este nuevo producto es que está basado completamente en su antecesor, el equipo de desarrollo es exactamente el mismo e incluso han dejado una opción para importar diseños creados con DBDesigner.

Tal como indican en el antiguo foro del programa, fabForce mantendrá el sitio web de DBDesigner hasta que se publique la primera versión de MySQL Workbench, a partir de ese momento desaparecerá. Parece que el momento está cerca.

Mi impresión es que, a pesar de los planes que tenían para el producto después de la compra por MySQL, se antepusieron productos nuevos. Nada más ni nada menos que el conjunto de nuevas utilidades bajo entorno gráfico que aparecieron hace un par de años: MySQL Administrator, QueryBrowser y Migration Toolkit, todas ellas desarrolladas por el equipo de DBDesigner, fantásticas utilidades por cierto. Recordad que hasta entonces no había ninguna herramienta visual oficial para trabajar con MySQL, las que había eran todas de terceros, una clara desventaja dentro del sector de pequeña y mediana empresa en el que pretendían entrar después del gran éxito en entornos web.

¿Qué hacía de especial a DBDesigner?

Lo primero y fundamental, una herramienta visual para modelar bases de datos y crear diagramas e/r al más puro estilo de Oracle DBDesigner o los Diagramas de SQL Server. A esto hay que añadir las increíbles funciones de sincronización e ingeniería inversa. La primera permite reflejar automáticamente en tu diseño los cambios realizados sobre tu base de datos en el servidor y viceversa, no es necesario crear toda la base de datos de nuevo. La segunda permite leer una base de datos completa desde tu servidor y crear automáticamente el esquema. Si no los has probado no sabes lo que te pierdes.

Otra de las funcionalidades era la posibilidad de crear plugins para extender sus utilidades. Traía algunos de serie, uno de ellos, SimpleWebFront, permitía generar una aplicación CRUD completa de tu diseño en PHP sin tocar una sóla línea de código.

Finalmente debemos señalar que se podía trabajar ya en aquel momento con otros gestores de bases de datos, Oracle, MSSQL, SQLLite, ODBC…

¿Qué hay de nuevo?

Han tenido que pasar cinco años para que veamos una nueva versión de una de las mejores herramientas que han existido para MySQL. Algo nuevo tiene que aportar. Básicamente se ha mantenido la esencia de la antigua aplicación modernizando la interfaz y añadiendo algunas opciones nuevas.

La mala noticia es que saldrán dos versiones, la de comunidad, gratuita, y la comercial. La diferencia fundamental es que las opciones de sincronización e ingeniería inversa contra el servidor se han dejado para la versión de pago, pero aparece una opción en la libre que permite hacer lo mismo con archivos SQL en vez de en vivo. La mayoría de nuevas funciones se incluirán en la versión comercial, pero os aseguro que no son indispensables, hablamos distintas opciones de debug y notación.

Entre los planes de MySQL figuran crear una comunidad de desarrolladores de extensiones y plugins que permitan nuevas opciones, con lo cual no sería de extrañar que la primera en aparecer fuese una versión libre de las opciones en vivo antes comentadas.

Lo mejor es que la pruebes y obtengas tu propias conclusiones. Para mi siempre es el primer paso al preparar una nueva aplicación. Si tienes un buen modelo visual es muy sencillo preparar tus clases del modelo o decirle a alguien lo que tiene que hacer sin tener que estar revisando continuamente la estructura desde el servidor.

La versión final está al caer. Mientras tanto puedes descargar la última beta desde aquí.

Enlaces interesantes de los últimos días

Excursiones de fin de semana: Requena y Gandía

Aprovechando que el lunes era fiesta en Valencia, decidimos hacernos alguna excursioncita para aprovechar el largo fin de semana. Pretendo con este post inaugurar una serie de artículos de escapadas de fin de semana a pueblos de la Comunidad Valenciana y alrededores, Cuenca y Teruel sobre todo, porque no todo va a ser trabajo.

Requena

El domingo por la tarde nos pegamos una vuelta por esta bonita ciudad de la provincia de Valencia ya casi en Cuenca. Había estado en bastantes ocasiones por Requena, pero nunca se me había ocurrido hacer algo de turismo, no sabía lo que me perdía.

Situada a unos 60km de la capital de la provincia, Requena cuenta con una zona antigua declarada en 1966 Conjunto Histórico Artístico.

Con un pasado íbero, romano y musulmán, Requena pasa a formar parte del Reino de Castilla durante la Reconquista y, de hecho, fue originariamente parte de la provincia de Cuenca, aunque finalmente prevalecieron intereses geográficos y comerciales y pasó a formar parte de Valencia.

Conocido su casco histórico como el Barrio de la Villa, la zona conserva reminiscencias de sus épocas pasadas con mucha mezcla gótica y barroca. Lástima que no pudimos ver las cuevas subterráneas de origen árabe que siembran el subsuelo de la Plaza de la Villa. Llegamos fuera de horario :(. Volveremos para verlas, sin duda. En las fotos podéis ver algunos puntos destacados.

Su pasado castellano se observa claramente en su gastronomía, más alejada de la cocina valenciana. El producto por excelencia de Requena son los embutidos de cerdo y así tienen su propia Feria del Embutido Artesano y de Calidad.

Plaza del Castillo, RequenaTorre del Homenaje, RequenaIglesia de Santa María, RequenaPlaza de la Villa, RequenaIglesia del Salvador, RequenaPuerta del Ángel, Requena

Gandía

El lunes tocaba desplazarnos a Gandía. Comida con la familia y tarde de paseo por la playa, qué mejor plan. Aunque mis primeros recuerdos en Valencia son en la playa de Cullera, no puedo hacer ascos a un apartamento en primera línea de playa como podéis ver en las dos primeras fotos tomadas desde el balcón. Para mi ésta es la mejor época del año en el Mediterráneo y la Playa de Gandía un lujazo.

Hoy tocaba… arroz al horno, comida valenciana donde las haya, posiblemente más común que la conocida paella ya que no se necesitan utensilios especiales para prepararlo. De rechupete, cuando fuí a hacer la foto de la olla de barro ¡ya estaba vacía!.

Aprovecho para daros algo de envidia con algunas fotos de la Playa de Gandía por estas fechas, sin turistas ni bañistas, impresionante. Buen tiempo, playa y buena comida, ¿qué más se puede pedir?.

Playa de Gandía desde el apartamentoPlaya de Gandía desde el apartamento con parte del jardínVistas del paseo y Playa de GandíaEl Chiringuito, Plata de Gandía, no cualquier chiringuito, el nuestro, el de los quintos fresquitos :)Playa de Gandía desierta, no hay nadie!

Por cierto, el chiringuito no es cualquiera, es el que solemos frecuentar. Ahora que viene el veranito ya se echan de menos esos quintos fresquitos de aperitivo. Mientras tanto aprovechamos la tarde con unas horchatas fresquitas o unos granizados.

Nueva API de Wurfl

Tenía pendiente desde hace unos días comentar los cambios producidos en la API de Wurfl.

La teoría

Para los que no sepan de qué estamos hablando, Wurlf es una base de datos de características de dispositivos móviles. A la hora de desarrollar sites para terminales ligeros, uno de los problemas principales es la diversidad de características distintas: tamaños de pantalla, formatos multimedia soportados, lenguajes de programación… Para solucionarlo, los proveedores necesitamos mantener bases de datos de modelos con sus características principales. Una de las opciones es mantener esa base de datos manualmente, de hecho se debe hacer para responder rápidamente a nuevos terminales. Para ello registramos los UserAgents de nuevos modelos que acceden a nuestras aplicaciones para, posteriormente, buscar sus características y añadirlos a nuestra base de datos.

Wurfl es un intento de solucionar este problema desde el software libre y colaborativo. Wurfl es, fundamentalmente, un archivo XML con la información relativa a más de 10.000 terminales distintos. Para consultar este XML la gente del proyecto ha desarrollado API’s en la mayoría de lenguajes utilizados (Java, PHP, Python, Perl…). Lo más curioso es la forma de acceder a los datos. A partir del XML se crean varios miles de archivos de manera que a la hora de buscar las características de un modelo sólo hay que buscar en un archivo. A mi personalmente no me gusta mucho el modelo. Un proyecto paralelo muy interesante es Tera-Wurfl, basándose en el XML de Wurfl crea una base de datos MySQL y modifica la API PHP para consultar los datos en ella en vez de en los miles de archivos. Nosotros hicimos en su día modificaciones sobre este proyecto para utilizarlo con Sql Server.

Wurfl lo mantiene la gente, gente como nosotros, añadiendo nuevos UserAgents. Obviamente esto no es perfecto, lleva a incongruencias y datos erróneos. Recientemente han publicado también una aplicación web para enviar nuevos UserAgents y nuevos modelos. Hemos llegado a la conclusión de que no hay un modelo único válido, sino que debe ser una combinación de Wurfl y de tu base de datos propia.

¿Existen alternavitas a Wurfl? Sí, por supuesto, alguna hay, aunque en general son de pago para uso en producción.

De DetectRight, cuando lo probamos, no me gustaba el modelo, las consultas se hacen online, añadiendo un retardo y tráfico no necesario. Device Atlas me ha parecido más interesante. Lo probamos el día que lo publicaron y no nos convenció, tenia muchas carencias de características, pero estos días lo hemos vuelto a probar y ha mejorado considerablemente, aunque al no ser gratuito, aunque sea poco, no me convence, habrá que seguirle la pista de todos modos..

¿Qúe es lo que ha cambiado entonces en la API?

El proceso por el cual Wurfl averigua a qué modelo pertenece un UserAgent es muy curioso. No se basa en UserAgents o modelos concretos, sino en grupos de UserAgents, de manera que sin llegar a tener un UserAgent ni su modelo registrado puede devolvernos las posibles caracteríscias comparándolo con un modelo semejante. Por ejemplo, es posible que las característias de un Nokia N73 que no tengamos registrado sean iguales o superiores a las de un Nokia N70, luego asumimos las características de este último. Esto es extremadamente útil a la hora de tratar con modelos nuevos ya que oficialmente Wurfl libera actualizaciones un par de veces al año, aunque puedes descargar las versiones diarias del cvs.

Originariamente, el procedimiento para obtener las características de un UserAgent se basaba en que, si no se encontraba un UserAgent idéntico al del cliente, se iba reduciendo la cadena del mismo un caracter hasta encontrar un UserAgent que conincidiese con esa cadena reducida. Obviamente muy eficiente no es el método, pero funcionaba hasta ahora. La introducción de navegadores avanzados en dispositivos de gama alta, derivados de Safari, Opera, etc. llevó a la aparición de UserAgents móviles derivados de Mozilla, como los navegadores web tradicionales. Nokia95 (y la mayoría de nuevos modelos de Nokia), iPhone, Blackberry… casi todos tienen cadenas del tipo:

Mozilla/5.0 (iPhone; U; CPU like Mac OS X; de-de) AppleWebKit/420.1
(KHTML, like Gecko) Version/3.0 Mobile/3B48b Safari/419.3

Mozilla/5.0 (SymbianOS/9.2; U; Series60/3.1 NokiaN95/11.0.026;
Profile MIDP-2.0 Configuration/CLDC-1.1) AppleWebKit/413
(KHTML, like Gecko) Safari/413

El método de reducción es poco eficiente con UserAgents de este tipo. Esto es, pues, lo que han cambiado. El nuevo sistema establece un análisis en dos pasos:

  1. Primero se averigua qué tipo de UserAgent estamos tratando (basado en Mozilla, Microsoft, etc.).
  2. Según el tipo del primer paso, se procesa el UserAgent con el manejador adecuado para encontrar la información requerida.

Por ejemplo, en el primer paso, si la cadena contiene la palara Blackberry sabemos que es un navegador de este tipo de dispositivos. Un algoritmo concreto para éstos nos devolverá el modelo.

Era una modificación imprescindible hoy en día. No por el iPhone como puedan pensar muchos, sino porque la mayoría de nuevos terminales de Nokia ya tienen derivados de Safari, navegadores que por cierto nos han dado algunos problemas desarrollando sites 100% correctos y estandard pero que no se visualizaban bien (a pesar de que en los navegadores de escritorio Safari sí que funcionaba bien).

Luca Passani, alma mater de Wurfl junto a Andrea Trasatti, lo explica mejor que yo aquí.

Internet Móvil: Adaptar o no adaptar

Importante pregunta ésta. Apple y su iPhone quieren que Internet sea universal, independientemente del dispositivo que utilices siempre verás lo mismo. En un mundo ideal sería perfecto. Incluso en un mundo iPhone podría ser aceptable. La realidad es bien distinta y dista mucho de estar cerca ese idealismo.

No tengo un iPhone con lo cual no he podido probar la experiencia de usuario navegando con él, pero sí lo he hecho con la mayoría de terminales normales del mercado, desde modelos de gama baja hasta los últimos Nokia N95 o HTC Touch y similares incluyendo distintos PDA’s y smartphones. Todos estos teléfonos de gama alta tienen algo en común: utilizan navegadores avanzados, versiones móviles de sus homónimos para web en PC’s. Nokia utiliza uno basado en Safari (sí, igual que el iPhone) y HTC, al utilizar Windows Mobile, tiene Internet Explorer. Otros utilizan Opera Mini. Con todos ellos puedes hacer lo mismo, desde ver sitios web desarrollados expresamente para tecnologías móviles hasta sitios web normales. ¿Cuál es el problema entonces? La usabilidad y la navegabilidad. Y esto hablando de los de gama alta, de los demás mejor nos olvidamos en este artículo.

Cuando ves en un móvil una web, estás viendo en una pantalla de un máximo de 320×240 un sitio diseñado para resoluciones de al menos 800×600, y digo al menos cuando la mayoría de sitios hoy en día se hacen para resoluciones superiores .

¿Qué te parecería ver una web diseñada a 1024px en una pantalla de 800×600? Pensarías de todo y cargarías contra los diseñadores, sin duda, ¡y eso que tienes un ratón para desplazarte lateralmente!.

Esta teoría se puede aplicar tal cual en los dispositivos móviles. Pantallas pequeñas (comparadas a las de los PC’s) para ver sites realizados a resoluciones mayores. Además, por mucho que la pantalla sea táctil, la navegavilidad no es la misma que con un ratón. Tendrías que estar desplazándote horizontalmente contínuamente.

Utilizo desde hace casi cuatro años una PDA, principalmente cuando estoy de viaje. La utilizo sobre todo para leer el correo y planificar rutas y visitas o buscar hoteles y restaurantes, con lo que hago uso de Internet. A veces con Wifi, si la encuentro (muy pocas veces), y otras a través del GPRS del móvil. La PDA tiene pantalla táctil y puedes moverte arrastrando el dedo para desplazarte por la página, como véis no es ninguna novedad lo del iPhone. Os puedo asegurar que es insufrible, el scroll horizontal es de lo peor.

A esto hay que sumarle otro problema muy importante cuando hablamos de navegación con móviles: el coste del tráfico de datos. Vale que se puede utilizar Wifi, pero hoy en día no es fácil encontrar puntos de acceso abiertos. Si cuando nos conectamos a través del móvil vemos un sitio web normal, no adaptado, estaremos viendo cientos de menús y banners supérfluos para lo que realmente queremos ver pero que nos ralentizan la navegación, la velocidad de carga (los procesadores móviles están muy limitados) y, sobre todo, el coste del tráfico que es lo que, a fin de cuentas, más nos duele.

Ahora nos quieren vender que la navegación con móviles ha aumentado exponencialmente el último año gracias al iPhone pero no nos aclaran si es navegación Wifi o GPRS. No es lo mismo. Hasta que apareció el iPhone nadie habría pensado en gastarse 500$ y dos años de contrato a razón de una buena cantidad mensual para tener un dispositivo que te permita navegar. Antes del iPhone ya existían dispositivos móviles con Wifi, no es nada nuevo. Encima el iPhone viene sin 3G, es un simple GPRS, con lo que me niego a creer que ese aumento de tráfico de Internet se deba al iPhone propiamente dicho sino a que una gran cantidad de gente ha pasado a tener un dispositivo móvil con Wifi, de otro modo se estarían arruinando pagando a su operadora por ese tráfico GPRS, además de aburrirse de esperar, no nos engañemos, GPRS es lo que es, y si vas a cargar webs ya sabes lo que te espera.

Mi opinión, después de varios años desarrollando portales para dispositivos móviles, es que tardaremos mucho en ver móviles con la usabilidad y navegabilidad necesaria como para ver webs normales. Aquí será fundamental la reducción de precios en la navegación, nos guste o no es el mayor problema. Aunque todos los operadores se pusiesen de acuerdo y ofreciesen tarifas del estilo de Yoigo o Simyo, alrededor de 1 euro diario, si lo utilizásemos todos los días estaríamos pagando más de 30 euros mensuales por Internet en el móvil a lo que habría que sumar los 30 o 40 que ya pagas por el de casa. Yo no lo veo. El segundo problema es el que comentábamos al principio, la gente que tiene un terminal de gama alta de este tipo es un porcentaje diminuto respecto al grueso de usuarios que tienen teléfonos de gamas media y baja y que son, a la larga, los que van a reventar el mercado de Internet para móviles, la gente normal.

Yo, mientras tanto, seguiré adaptando. Hay mucha diversidad de modelos y WML, xHTML Mobile e iMode estarán aquí por muchos años todavía.