AJAX fra andre domener
Hvis jeg nå ønsker å bruke noen av tjenestene på mine vevsider, er det selvsagt fristende å bruke AJAX. De fleste tjenestene er slik at de gir svaret enten i JSON- eller XML-format. Problemet er at tjenesten ligger i et annet domene enn min vevside, og jeg kan ikke uten videre sende en XMLHttpRequest. Vi skal se på to måter å løse dette på.
IndirekteVi skriver et program på tjeneren, geoaccess, som henter data fra geoNames. Vi kommuniserer fra vevsiden til dette programmet med AJAX. Dette innbærer at AJAX-kommunikasjonen foregår innen samme domene. |
DirekteVi bruker en litt tvilsom strategi som gjør det mulig å hente materialet direkt via AJAX. Dette innbærer at AJAX-kommunikasjonen foregår på tvers av domener. |
Som eksempel bruker vi for enkelhets skyld tjenesten findNearbyPlaceNameJSON. Vi vil altså finne vilke steder som ligger innenfor en bestemt radius fra de koordinatene som er registrert for et beestemt postnummer. Vi holder oss til norske postnummere.
Indirekte
Vi begynner med et skript som skal gå på tjeneren. Skriptet er laget slik at det videreformidler en forespørsel, tar mot svaret og sender det direkte tilbake til vevsiden. Skriptet er laget så generelt som mulig og er ikke begrenset til den oppgaven vi har satt oss.
#! /usr/bin/python2 import urllib import cgi import cgitb; cgitb.enable() # Attempt to convey an AJAX-request to # geonames as general and smooth as possible form=cgi.FieldStorage() print 'Content-type: text/html charset=iso-8859-1\n' #expect to find: function and any set of parameters function='postalCodeSearch' if form.has_key('function'): function=form['function'].value lookUp='http://ws.geonames.org/'+function ks=form.keys() params='' for k in ks: if k=='function': continue params+=k+'='+form[k].value+'&' if len(ks) > 1: params=params[:-1] lookUp+='?'+params try: f=urllib.urlopen(lookUp) S=f.read() f.close() S=S.decode('utf') S=S.encode('iso-8859-1') print S except: print 'cannot load'
Fra vevsiden bruker vi skriptet slik:
// relies on jquey.js function findNearbyPostalCodesJSON(postnummer,radius) { var params='function=findNearbyPostalCodesJSON&country=NO&postalcode=' +postnummer+'&radius='+radius; $.ajax({ url:'http://www.it.hiof.no/~borres/cgi-bin/ajax/geonames/geoaccess.py', data:params, success:function(data) { processNearbyPlaceNameJSON(data); }, error:function(data) { $('#result').html("Could not access content"); } }); } function processNearbyPlaceNameJSON(jData) { try{ p=eval('('+jData+')'); var pcodes= p.postalCodes; T='' for(ix=0;ix<pcodes.length;ix++) { var navn=pcodes[ix].placeName+'<br/>'; if(T.indexOf(navn)==-1) T+=navn; } document.getElementById('result').innerHTML=T; } catch(e) { document.getElementById('result').innerHTML= 'Finner ikke noe'; } }
Direkte
Nøkkelen er følgende script av Jason(!) Levitt [2] . Sikkerhetsadvarselen er lagt til i den versjonen som anbefales brukes av geonames.
// JSONscriptRequest -- a simple class for accessing Yahoo! Web Services // using dynamically generated script tags and JSON // // Author: Jason Levitt // Date: December 7th, 2005 // // A SECURITY WARNING FROM DOUGLAS CROCKFORD: // "The dynamic <script> tag hack suffers from a problem. It allows a page // to access data from any server in the web, which is really useful. // Unfortunately, the data is returned in the form of a script. That script // can deliver the data, but it runs with the same authority as scripts on // the base page, so it is able to steal cookies or misuse the authorization // of the user with the server. A rogue script can do destructive things to // the relationship between the user and the base server." // // So, be extremely cautious in your use of this script. // // Constructor -- pass a REST request URL to the constructor // function JSONscriptRequest(fullUrl) { // REST request path this.fullUrl = fullUrl; // Keep IE from caching requests this.noCacheIE = '&noCacheIE=' + (new Date()).getTime(); // Get the DOM location to put the script tag this.headLoc = document.getElementsByTagName("head").item(0); // Generate a unique script tag id this.scriptId = 'YJscriptId' + JSONscriptRequest.scriptCounter++; } // Static script ID counter JSONscriptRequest.scriptCounter = 1; // buildScriptTag method // JSONscriptRequest.prototype.buildScriptTag = function () { // Create the script tag this.scriptObj = document.createElement("script"); // Add script object attributes this.scriptObj.setAttribute("type", "text/javascript"); this.scriptObj.setAttribute("src", this.fullUrl + this.noCacheIE); this.scriptObj.setAttribute("id", this.scriptId); } // removeScriptTag method // JSONscriptRequest.prototype.removeScriptTag = function () { // Destroy the script tag this.headLoc.removeChild(this.scriptObj); } // addScriptTag method // JSONscriptRequest.prototype.addScriptTag = function () { // Create the script tag this.headLoc.appendChild(this.scriptObj); }
Vi bruker dette biblioteket fra vår egen javascriptkode slik:
// this function will be called by our JSON callback // the parameter jData will contain an array with postalcode objects function processNearbyPlaceNameJSON(jData) { if (jData == null) { document.getElementById('result').innerHTML='ikke noe svar'; return; } try{ var pcodes = jData.postalCodes; if (pcodes.length > 0) { T='' for(ix=0;ix<pcodes.length;ix++) { var navn=pcodes[ix].placeName+'<br/>'; if(T.indexOf(navn)==-1) T+=navn; } document.getElementById('result').innerHTML=T; } else document.getElementById('result').innerHTML='fant ikke noe'; } catch(E) { document.getElementById('result').innerHTML='feil spørsmål'; } } function findNearbyPostalCodesJSON(pcode,radius) { request = 'http://ws.geonames.org/findNearbyPostalCodesJSON?postalcode=' + pcode + '&country=NO&radius='+radius + '&callback=processNearbyPlaceNameJSON'; // Create a new script object aObj = new JSONscriptRequest(request); // Build the script tag and execute it aObj.buildScriptTag(); aObj.addScriptTag(); }
Forklaring
Dersom vi skal oppsummere kort hvordan denne koden virker så må det bli omtrent slik:
- Vi etablerer et scriptelement som dersom vi skulle skrive det i
klartekst blir slik( jeg har lagt inn linjeskift av praktiske årsaker):
<script type="text/javascript" src="http://ws.geonames.org/findNearbyPostalCodesJSON? postalcode=1850& country=NO& radius=2& callback=processNearbyPlaceNameJSON& noCacheIE=1193327617484" id="YJscriptId1"> </script>
Dette bygges opp ved hjelp av funksjonene: JSONscriptRequest(request) og buildScriptTag(). Merk at navnet på den funksjonen som skal ta mot resultatet inngår i parameter lista: callback=processNearbyPlaceNameJSON. -
I det øyeblikk vi setter inn dette script-elementet, addScriptTag(), som barn til head-elementet
blir det evaluert. Denne evalueringen innebærer at nettleseren forsøker å
hente skriptet. Nøkkelen til å forstå hva som skjer er en analyse av hva som kommer
tilbake. Med parametrene ovenfor vil vi få følgende tilbake (jeg har lagt inn linjeskift av praktiske årsaker):
processNearbyPlaceNameJSON( {"postalCodes":[ {"adminName2":"EIDSBERG","adminCode2":"0125", "adminCode1":"01","postalCode":"1850", "countryCode":"NO","lng":11.3333333,"placeName":"Mysen","lat":59.55, "adminName1":"Oestfold"},{"adminName2":"EIDSBERG","adminCode2":"0125", "adminCode1":"01","postalCode":"1851", "countryCode":"NO","lng":11.3333333,"placeName":"Mysen","lat":59.55, "adminName1":"Oestfold"}, {"adminName2":"EIDSBERG","adminCode2":"0125", "adminCode1":"01","postalCode":"1859", "countryCode":"NO","lng":11.3333333,"placeName":"Slitu","lat":59.55, "adminName1":"Oestfold"} ]});
Det vil si at vi får tilbake et funksjonskall med et JSON-objekt som parameter. GeoNames-serveren produserer dette formatet, og kan gjøre det fordi vi har sendt over navnet på vår mottagerfunksjon som parameter. - Funksjonskallet effektueres siden det oppfattes om en JavaSciptlinje, som skal utføres på linje med alle JavaScript-setninger som parses i scriptelementet. Og vår funksjon, processNearbyPlaceNameJSON, utføres. Merk at parameteren som er et JSON-objekt som også evalueres.
Vi ser at det ikke er noe XMLHttpRequest involvert. Alt er basert på at vi "sniker" oss utenfor kontrollmekanismene ved å manipulere dom-treet ved å sette in et scriptelemment som evalueres.
Eksperiment (lokalt)
Vi forsøker å anvende denne teknikken mot en serverfunksjon vi skriver selv. Vi gjør dette på samme domene som web-siden, altså ikke fremmed domene.
Vi skriver følgende script på tjeneren:
#! /usr/bin/python import cgi print 'Content-type: text/plain\n' form=cgi.FieldStorage() callback='error' if form.has_key('callback'): callback=form['callback'].value print callback+'({"melding":"Halleluja"});'
Vi bruker biblioteket fra javascript koden over, og skriver funksjonene:
function takeIt(melding) { alert('Im back, '+melding.melding); } function doRequest(theURL) { request = theURL+'?callback=takeIt'; // Create a new script object aObj = new JSONscriptRequest(request); // Build the script tag and execute it aObj.buildScriptTag(); aObj.addScriptTag(); }
En enda enklere løsning
#! /usr/bin/python import cgi print 'Content-type: text/plain\n' form=cgi.FieldStorage() callback='error' if form.has_key('callback'): callback=form['callback'].value print callback+'("Halleluja");'
function doRequest(theURL) { request = theURL+'?callback=alert'; // Create a new script object aObj = new JSONscriptRequest(request); // Build the script tag and execute it aObj.buildScriptTag(); aObj.addScriptTag(); }
Tja
Hvilken av teknikkene skal vi bruke ? Det er fristende å bruke den direkte metoden. Den krever lite kode og er åpenbart rask.
Det er imidlertid noen negative momenter/bergrensninger som det er verdt å merke seg:
- teknikken omgår de sikkerhetselementene som er innebygd i domenegrensene for AJAX.
- Kan ikke motta responsekoder, fungerer bare når forespørselene returnerer kode: 200.
- Fungerer bare med get, og bare asynkront.
- vi ser bort fra de mulighetene vi har for datakontroll og formatering som vi lett kan realisere i et tjenerskript.