Introducción
Para un proyecto reciente, tenía que implementar en una aplicación una caja de búsqueda que se comunicara con el AppStore. La idea era que el usuario introdujera algunas palabras clave y devolverle mediante AJAX una lista de aplicaciones disponibles que cumplieran con el criterio seleccionado.
Como parece algo trivial, me puse manos a la obra. Sin embargo, como no podía ser menos tratantándose de los chicos de la manzana, la cosa no es como cabría esperar.
Como no encontré mucha documentación o ejemplos funcionales, he decidido escribir este post para que así sirva de pequeña guía. Además, expondré el por qué no estoy de acuerdo con la forma en que Apple sirve el contenido.
El resultado final podéis verlo en la siguiente demo. El código fuente lo tenéis en descarga directa para modificarlo a vuestro antojo.
Dónde pregunto y qué me contestan
Tras 5 minutos buscando en Google, llego a la primera conclusión: la APPStore, no tiene un API como tal. En su lugar, para obtener resultados, hay que recurrir al servicio de búsqueda de iTunes o tirar directamente del canal de noticias (RSS).
La segunda opción no me convence porque implica pocas opciones de configuración y parsear muchos datos. Además, desde la documentación oficial de Apple, invitan a usar solo el primer método.
La URL que propone iTunes tiene la siguiente estructura:
http://itunes.apple.com/search?params |
Donde params se correspondería con los parámetros que podemos añadir para afinar las búsquedas.
Si probamos esta URL en el navegador, nos devuelve un JSON: perfecto para poder manejarlo directamente con Javascript mediante AJAX (o eso puede parecer en este primer contacto).
Si no introducimos ningún parámetro, obtenemos un objeto vacío:
{ "resultCount":0, "results": [] } |
Añadiendo parámetros
Aunque de forma interna se manejan más, la URL anterior admite solo 5 parámetros interesantes según la documentación:
- term: esto sería la palabra o término a buscar.
- country: país para el que se desea obtener el resultado.
- entity: tipo del resultado que se desea obtener (libro, música, aplicación, …)
- limit: número de resultados que se desea obtener.
- callback: nombre de la función que se desea ejecutar tras obtener respuesta (este elemento, el más polémico a mi entender, lo veremos en detalle un poco más abajo).
Con estos datos sobre la mesa, podemos montar consultas con cierto criterio:
Dame 5 aplicaciones que contengan la palabra ‘Birds’ para España:
http://itunes.apple.com/search?term=birds&country=es&entity=software&limit=5 |
Dame 10 recursos de audio (música) que contengan la palabra ‘Beattles’ para España también:
http://itunes.apple.com/search?term=beattles&country=es&entity=musicTrack&limit=10 |
Etcétera… Toda la documentación sobre los parámetros la tenemos aquí.
Si nos quedamos con la última consulta, la de los Beattles, el JSON que obtenemos tiene esta pinta:
{ "resultCount":3, "results":[ { "wrapperType":"track", "kind":"song", "artistId":136975, "collectionId":400833614, "trackId":400833650, "artistName":"The Beatles", "collectionName":"The Beatles 1962\u20131966 (The Red Album)", "trackName":"From Me to You", "collectionCensoredName":"The Beatles 1962\u20131966 (The Red Album)", "trackCensoredName":"From Me to You (Mono Version)", "artistViewUrl":"http://itunes.apple.com/es/artist/the-beatles/id136975?uo=4", "collectionViewUrl":"http://itunes.apple.com/es/album/from-me-to-you-mono-version/id400833614?i=400833650&uo=4", "trackViewUrl":"http://itunes.apple.com/es/album/from-me-to-you-mono-version/id400833614?i=400833650&uo=4", "previewUrl":"http://a2.mzstatic.com/us/r1000/025/Music/31/29/47/mzm.bbwipwbe.aac.p.m4a", "artworkUrl30":"http://a5.mzstatic.com/us/r1000/014/Music/d7/8c/65/mzi.urqxhfun.30x30-50.jpg", "artworkUrl60":"http://a4.mzstatic.com/us/r1000/014/Music/d7/8c/65/mzi.urqxhfun.60x60-50.jpg", "artworkUrl100":"http://a4.mzstatic.com/us/r1000/014/Music/d7/8c/65/mzi.urqxhfun.100x100-75.jpg", "collectionPrice":19.99, "trackPrice":1.29, "releaseDate":"1973-04-19T08:00:00Z", "collectionExplicitness":"notExplicit", "trackExplicitness":"notExplicit", "discCount":2, "discNumber":1, "trackCount":13, "trackNumber":3, "trackTimeMillis":117907, "country":"ESP", "currency":"EUR", "primaryGenreName":"Rock", "contentAdvisoryRating":null, "shortDescription":null, "longDescription":null }, // ... ] } |
Como vemos, obtenemos muchos datos interesantes. Sin embargo, algo a tener en cuenta es que cada entidad tiene a su vez sus propios pares de clave-valor, lo que es un inconveniente a la hora de pintar los resultados si no sabemos exactamente qué se ha buscado. Pero bueno; nos ceñiremos a las aplicaciones para este ejemplo.
NOTA: En la demo se manejan de forma básica todos los campos.
Tomemos el JSON obtenido en la primera consulta, la que buscaba aplicaciones con la palabra ‘birds‘:
{ "resultCount":1, "results":[ { "kind":"software", "artistId":356333537, "artistName":"PLH Software", "price":0.00, "version":"1.8", "description":"ALL 18 EASTER/18 ST PATRICKS/45 HALLOWEEN/25 XMAS/18 VALENTINE LEVELS + GOLDEN EGGS WITH TEXT/SCREENSHOT/VIDEO WALKTHROUGHS WITH IAP ** ALL 258 ANGRY BIRDS I, 105 AB RIO LEVELS, & 200 CUT THE ROPE LEVELS **\n\nYou can try my Angry Birds Seasons guide with 15 free levels, my Angry Birds I guide with 42 free levels, my AB Rio guide with 15 free levels, and my Cut the Rope guide with 25 free levels all in one app. I will be adding all my Angry Birds series guides to this guide as I release them. You no longer need a guide for Angry Birds I, Angry Birds Seasons, Angry Birds Rio, Golden Eggs, etc. I just keep adding new guides!!\n\n(This guide is unofficial)\n\nMy Angry Birds Seasons guide contains exactly what you need to beat Angry Birds Seasons (Halloween/Christmas/Valentine/St Patricks/Easter):\n\n- 3-star video walkthroughs for all 124 levels of Angry Birds Seasons (Halloween/Christmas/Valentine/St Patricks/Easter) IAP\n- all Golden Eggs IAP\n- convenient button to submit your better solutions for inclusion in the guide!\n- iPhone 4 retina display enhanced graphics\n- fast and free web updates\n\nMy Angry Birds Seasons guide is more than just a simple reference \u2013 it contains step-by-step walkthroughs filled with exactly the information used by the pros everyday to capture 3-star ratings in Angry Birds Seasons (Halloween/Christmas/Valentine/St Patricks/Easter). Don't care about 3-stars? No problem. The walkthroughs will still get you through those tough levels for less than the cost of a cup of joe at Starbucks. \n\nYour in-app purchase also gets you my Golden Egg walkthrough as well. \n\nI undertook this effort to create this Angry Birds Seasons (Halloween/Christmas/Valentine/St Patricks/Easter) walkthrough because there was simply no current reference containing all the information I needed such as:\n\n- how many and what type of birds are provided in each level\n- the minimum number and type of birds to achieve 3 stars for each level\n- convenient 3-star YouTube videos\n\nIf Rovio adds levels, I update the guide. If better solutions are found, I will update the guide.\n\n\nDisclaimer: \n\nThis guide is an unofficial guide created by PLH Software. This guide, nor PLH Software, are affiliated with Rovio Mobile, nor Zeptolab. This guide was created under Fair Use doctrine of United States copyright law and associated and equal doctrine and laws in other legal jurisdictions. Any inquiries regarding this guide should be addressed to peilei@pobox.com.\n\nAll trademarks and copyrights referenced are the sole property of their respective owners.\n\nAll icon and panel art is original.", "genreIds":[ "6006", "6016" ], "releaseDate":"2010-11-12T07:30:24Z", "sellerName":"Pei Lei Holdings, Incorporated", "currency":"EUR", "genres":[ "Referencia", "Entretenimiento" ], "trackId":400147971, "trackName":"Guide for Angry Birds Seasons (Halloween, Christmas, Valentine, St. Patricks) / Angry Birds / Angry Birds Rio / Cut the Rope / Cut the Rope Holiday Gift & Golden Eggs", "supportedDevices":[ "all" ], "releaseNotes":"1) web updater enhancements for bad network connections \n2) feedback form enhancements \n3) added 15 free Rio levels in binary", "primaryGenreName":"Reference", "primaryGenreId":6006, "isGameCenterEnabled":false, "wrapperType":"software", "artworkUrl60":"http://a3.mzstatic.com/us/r1000/016/Purple/b0/8c/71/mzi.bpsblmin.png", "artworkUrl100":"http://a1.mzstatic.com/us/r1000/021/Purple/bd/e2/8a/mzl.tkpjzkdl.png", "artistViewUrl":"http://itunes.apple.com/es/artist/plh-software/id356333537?uo=4", "contentAdvisoryRating":"4+", "trackCensoredName":"Guide for Angry Birds Seasons (Halloween, Christmas, Valentine, St. Patricks) / Angry Birds / Angry Birds Rio / Cut the Rope / Cut the Rope Holiday Gift & Golden Eggs", "trackViewUrl":"http://itunes.apple.com/es/app/guide-for-angry-birds-seasons/id400147971?mt=8&uo=4", "languageCodesISO2A":[ "EN" ], "fileSizeBytes":"10010832", "screenshotUrls":[ "http://a1.mzstatic.com/us/r1000/034/Purple/27/35/f8/mzl.rspdjhju.png", "http://a3.mzstatic.com/us/r1000/030/Purple/0a/06/1e/mzl.mtqayflp.png", "http://a2.mzstatic.com/us/r1000/042/Purple/db/95/03/mzl.aysokuuo.png", "http://a1.mzstatic.com/us/r1000/002/Purple/42/d6/65/mzl.rswqixxe.png", "http://a2.mzstatic.com/us/r1000/041/Purple/39/10/18/mzl.vyddgwvn.png" ], "ipadScreenshotUrls":[ "http://a5.mzstatic.com/us/r1000/044/Purple/e3/a9/f2/mzl.qxkkfoqk.1024x1024-65.jpg", "http://a5.mzstatic.com/us/r1000/023/Purple/ef/ee/4b/mzl.mpxsfsyi.1024x1024-65.jpg", "http://a3.mzstatic.com/us/r1000/023/Purple/67/58/23/mzl.gfsibelr.1024x1024-65.jpg", "http://a4.mzstatic.com/us/r1000/025/Purple/d1/b5/f0/mzl.xdjblgfv.1024x1024-65.jpg" ], "sellerUrl":null, "averageUserRatingForCurrentVersion":2.5, "userRatingCountForCurrentVersion":33, "artworkUrl512":"http://a1.mzstatic.com/us/r1000/021/Purple/bd/e2/8a/mzl.tkpjzkdl.png", "trackContentRating":"4+", "averageUserRating":2.5, "userRatingCount":41 } ] } |
Hemos cogido solo un elemento para ver su estructura y simplificar el código.
De entre todos los datos, los principalmente relevantes son:
- trackName: el nombre de la aplicación.
- description: descripción larga de la aplicación.
- trackViewUrl: URL en la APPStore de la aplicación.
- screenshotUrls: array con capturas de pantalla de la aplicación (puede incluir la portada).
- price: precio de la aplicación
Con estos datos podemos montar una página de resultados decentes.
Como parece que tenemos todo listo, vamos a lanzar nuestra consulta AJAX para ir pintando los resultados.
Para no detenernos demasiado en esto, usamos una petición con jQuery de las de toda la vida:
$.getJSON('http://itunes.apple.com/search', { 'term' : 'birds', 'country' : 'es', 'entity' : 'software', 'limit' : '1' }, function(data){ console.log( data ); }); |
Lanzamos la consulta, obtenemos un OK pero.. la respuesta está vacía. No hay datos de retorno. ¿Hemos escrito algo mal?
Si miramos en nuestra consola del navegador y copiamos la URL que ha generado jQuery,
http://itunes.apple.com/search?term=birds&country=es&entity=software&limit=1 |
todo parece normal. Es más, si la copiamos y pegamos en la barra del navegador para resolverla, si que obtenemos datos. Sin embargo, mediante AJAX la respuesta llega vacía: raro.
Buscamos un poco en Google, volvemos a la documentación y, de repente, encontramos el error…
Nada de AJAX!
Efectivamente; Apple no permite peticiones vía AJAX a su servicio de búsqueda (pseudo API). En lugar de eso, nos obliga a que creemos una tag script con el src apuntando a la URL que hemos compuesto. Es decir, que quieren que para el caso anterior, generemos lo siguiente:
var urlToSend = 'http://itunes.apple.com/search?term=birds&country=es&entity=software&limit=1'; $('<script />', { 'src' : urlToSend }).appendTo('body'); |
Vaya, vaya… ¿y cómo se entera el navegador de que se han devuelto resultados con este script? Utilizando un último parámetro en la URL anterior que mencionamos más arriba: un callback.
La idea de Apple es que tengamos una función independiente que es ejecutada cuando llegan los datos y a la que se le pasa como argumento el objeto JSON.
Así, para pintar los resultados, necesitamos un print_results:
function print_results( data ){ console.log( data ); } |
Y añadir esta función al callback del script anterior:
var urlToSend = 'http://itunes.apple.com/search?term=birds&country=es&entity=software&limit=1'; urlToSend += '&callback=print_results' $('<script />', { 'src' : urlToSend }).appendTo('body'); |
Si ejecutamos el código, ahora si que obtenemos resultados.
Cuidado, cuidado
Hasta al desarrollador más novato, cuando ve el código anterior, una alarma interior se dispara con una misma sospecha: ‘¿Pero esto es seguro?’.
La respuesta es fácil: No.
En términos de seguridad, este método es un agujero, una puerta abierta de par en par a lo que Apple quiera devolver tras la petición. Estamos permitiendo voluntariamente que terceros inyecten código Javascript en nuestra aplicación, algo que no puede dejarnos indiferentes por muchas manzanas que la emprese lleve en su logo. Estamos comprometiendo todo el sitio.
Imaginemos que Apple, además de devolver su JSON, decide incluir eventualmente código para monitorizar eventos y enviar dichos datos por AJAX a sus servidores: qué teclas pulsa el usuario, por donde mueve el ratón, etc… Es cierto que quizá sería un ejemplo extremo pero la posibilidad está ahí. Y por eso no me gusta nada este sistema de petición-respuesta.
Conclusión
Pues la conclusión es simple: si necesitamos que nuestra aplicación se conecte con la APPStore para poder ofrecer a los usuario la posibilidad de buscar aplicaciones, no disponemos de un API tradicional para ello. En su lugar, debemos permitir que nos inyecten código Javascript en el que esperamos los datos. Con esto, estamos comprometiendo la seguridad de nuestro sitio y confiando ciegamente en el valor y calidad de la respuesta obtenida.
Por lo tanto, no aconsejaría utilizar este sistema en entornos críticos donde la seguridad prima sobre el contenido. En aquellos otros en los que quizá tampoco se manejen datos sensibles pues podemos usarlo sin problemas.
Muy buen tutorial. Gracias
Cuidado, no todos los navegadores tienen soporte nativo del depurador console.
Si; es cierto: las versiones anteriores a IE8 no soportan ese comando.
Sin embargo, en el artículo se ha utilizado únicamente con fines didácticos: doy por hecho que el público que consulte esta guía tiene el suficiente conocimiento Javascript como para entender el código sin mayor problema y que es capaz de modificarlo según sus necesidades. Es ‘norma de la casa’ llenar los códigos de ‘consoles’ para que nos indiquen en todo momento por dónde vamos 😉
Gracias por el aviso!
Hombre, siempre se ha sabido que no se debe usar JSONP con fuentes que no sean «de confianza», no?
¿Y usuando un lenguaje de servidor como paso intermedio? es decir, en vez de pedir directamten el json a http://itunes.apple.com/search?parametros ,haces una peticion ajax a , por ejemplo, itunes.php ( o lo que sea) y que sea este itunes.php quien pida el json y te lo devuelva a cliente.
Sería una solución interesante para tener un mínimo de control sobre el objeto devuelto. Le echaré un vistazo.
Gracias!
Buenas Carlos. Llevo unos dias leyendo tu blog y queria felicitarte por ello, interesante a la par que util.
Respecto a lo que comentas en la entrada, dices que usando el metodo que obliga apple mediante la creacion de un script en el cuerpo del dom y recogiendo el resultado json mediante un callback, se compromete la seguridad de la pagina. Pero esto no ocurriria igual si obtuvieses los datos mediante una llamada ajax? En que se diferenciarian?
Y cuando dices que se compromete la seguridad, te refieres al uso de eval en el resultado obtenido por apple, el cual puede disparar un js malintencionado?
Gracias por adelantado.
Hola Raúl;
cuando realizamos una llamada AJAX tradicional podemos manejar la respuesta del servidor antes de ejecutarla o evaluarla. Para entendernos, es como una entrada de usuario en un campo de un formulario: podemos limpiar y parsear aquello que el servidor nos envíe.
Sin embargo, en este tipo otro tipo de llamadas, estas a las que nos referimos como JSONP, estamos permitiendo al código entrante ejecutarse a ciegas. Es decir, el servidor aquí no envía una respuesta que evaluamos y luego manipulamos, sino que se trata en realidad de una inyección de código que tenemos que asumir como fiable. Esto conlleva que, si por ejemplo tratáramos con un tercero malitencionado, éste pueda enviarnos scripts que se harían con el control total de la página: por esta vía, es relativamente fácil ‘capturar’ datos de usuario, monitorizar la actividad de la página, etc. Por eso no hace falta ni siquiera un eval para disparar ese tipo de código ya que el código devuelto puede ser autoejecutable.
Lo preocupante de este tipo de peticiones a ciegas es que no tienen que enviarse siempre esos paquetes en cada petición, sino que pueden hacerlo de forma ocasional, siendo así casi indetectable por parte de los responsables. Pensemos en un tercero que solo envía ese código malintencionado cada 500 peticiones; o cuando sospecha que su API no está siendo usada correctamente, o cuando detecta que están entrando muchas peticiones desde el mismo server, etc…
Como podemos ver, el uso de este tipo de servicios conlleva un riesgo de seguridad con el que tenemos que contar. Algo desde mi punto de vista innecesario ya que Apple podría simplemente ofrecer su API de un modo accesible a una petición normal AJAX igual que hace, por ejemplo, Twitter.
Saludos.
Muchas gracias por responder! Me has aclarado las dudas ^_^
Aaamm creo que estas mal entendiendo un poco las negras intenciones de Apple… :-/ yo no diría que Apple no acepta peticiones por XHR, ya que una de las limitantes del objeto XHR es que los recursos deben de estar en el mismo dominio:puerto desde donde son invocados al intentar hacer AJAX desde midominio.com.mx a http://itunes.apple.com/ no funcionará y no por que Apple no quiera o no acepte peticiones XHR si no porque esta es una limitante de seguridad de JavaScript.
Lo que se tiene que hacer entonces son dos cosas en «AJAX» crossdomine… o hacer JSONP o hacer un proxy alojando en tu dominio. más bien lo que documentas y mencionas aquí son las desventajas de usar JSONP que no es más que incluir dinámicamente un js en nuestro sitio que invoque un callback que se le envía para evitar la restricción de dominio del XHR.
El recurso termina siendo un API no-oficial por lo tanto una API no confiable ni segura y pudiese dar el caso de lo que mencionas
Lo otro es hacer el proxy, crear un script PHP que consuma el dominio itunes.apple.com y nos imprima el resultado en un recurso dentro de nuestro dominio y así ya podemos hacer AJAX con XHR…
Soy anti-Apple… pero creo que este post tacha de malo a Apple, muchas veces lo es… Pero en este caso no lo es… esta vez Apple es inocente
Si; es cierto el tema de los Ajax crossdomain. Sin embargo, hoy día no cuesta nada implementar este tipo de soluciones. El propio método AJAX de jQuery ya facilita el crossdomain para que no resulte complejo de interpretar.
El uso de JSONP en si mismo tampoco conlleva demasiado problema ya que, por lo general, se puede implementar de forma que controlemos la entrada de datos que emite el servidor. Es por ejemplo lo que utilicé en el API de OpenLibra; sin embargo, Apple obliga a que inyectemos un código completamente a ciegas. Es con esa parte con la que no estoy de acuerdo: hay varias formas de ofrecer un API de un modo transparente al desarrollador y ésta no lo es.
Gracias por tu aportación.
Saludos!!
No sé cómo funciona exactamente la implementación de jQuery.getJSON, pero creo que el error va por parte de la implementación de jQuery.
Porque según yo getJSON hace exactamente lo mismo que como remediaste tu problema, ya que según yo en peticiones remota se envía el callback igualado a “?” y jQuery le asigna un nombre aleatorio al callback que le enviaste a getJSON y antes de enviarse se remplaza el ultimo “?” por el nombre temporal del callback.
Por lo que observe el problema se da cuando se pasan datos de envió; si se avía el callback:’?’ En el objeto de datos de envió no funciona correctamente por que “?” se pasa a su encode de URL por lo que la URL al final tiene un %3F y jQuery ya no remplaza el “?” por el nombre temporal del callback.
No revise ni hice ingeniería en inversa, pero según yo si el segundo paramento se pasa un JSON; getJSON intenta hacer una petición por XHR por lo cual no se realiza al ver que es un dominio distinto esto lo observe en la consola de firebug en Console aparece la petición pero si se hace un JSONP no debería de aparecer en la concola por que aquí solo aparecen las peticiones XHR pero si aparece en la lista de recursos en la pestaña de Red.
Y cuando se envía una sola url escrita “manualmente” o completa y el segundo parámetro es el callback no hay ningún problema el único problema es rederear las variables que se enviarían al servidor un paso que se supone getJSON haría por nosotros pero podemos usar jQuery.param para obtener esto, al final ponemos el callback igualado a nada, porque param() «encodea» y necesitamos que callback sea enviado como «?» y no su códec para que getJSON lo replace por el nombre del callback temporal
Así es como pude solucionar usando jQuery.getJSON
Al final según yo realiza exactamente lo mismo que tú con:
Y al intente realizar peticiones desde fuera a tu API y tengo exactamente las mismas situaciones, de hecho no pude acezar haciendo $.getJSON(url,{},callback), sería buena idea que implementarás el parámetro de callback para JSONP porque igual en tu dominio si funciona como si nada pero en externos tendríamos que crear nuestro proxy para poder consumir la API…
PD: oye como le hago para que el codigo quede bonito en los comentarios? en algunos pos vi el codigo formateado….