askeet – jour 17 – web services

Précedemment dans symfony

L’appication askeet a juste été mise en ligne hier, et nous avons déjà beaucoup de retour sur des améliorations. L’entrée utilisateur est fondamentale dans le design d’une application web 2.0, et même si ce concept d’application est nouveau, il doit être pris en compte le plus vite possible.
Mais nous implémenterons des fonctionnalités non prévues pour le jour 21. Avant cela, nous avons planifié de vous montrer quelques techniques de développement web avancées sur askeet, et la première présentée aujourd’hui est la programmation d’un API externe requierant une authentification HTTP.
Comme nous avons fait pas mal de petites modificaitons hier, vous avez tout intérêt à commencer le tutoriel d’aujourd’hui avec une version vierge de askeet d u jour 16 téléchargée dans le dépot de askeet.
L’API

un Application Programming Interface, ou API, est une interface du développeur pour un service de votre application en particulier, elle peut donc être utilisée à partir d’autres sites. Pansez à Google Maps ou Flickr, qui sont utilisés par de nombreux sites via leur API.
Askeet ne fait pas exception, et nous pensons que dans le but de développer la popularité des services, ils doivent être disponible pour d’autres sites. Le filet RSS développé pendant le jour 11 était une première approche de ce cahier des charges, mais nous devons pouvoir faire mieux.
Askeet mettra à disposition une API de réponse au questions posées par l’utilisateur. L’accès à cette API sera restreint aux seul utlisateurs de Askeet, via une authentification HTTP. Le format de réponse de l’API choisi est Representational State Transfer, ou REST – qui signifie que la réponse est un simple block XML similaire à la plupart des sorties de toutes les API sur le web:

<?xml version="1.0" encoding="utf-8" ?> <rsp stat="ok" version="1.0">
  <question href="http://www.askeet.com/question/what-shall-i-do-tonight-with-my-girlfriend" time="2005-11-21T21:19:18Z" >
    <title>What shall I do tonight with my girlfriend?</title>
    <tags>
      <tag>activities</tag>
      <tag>relatives</tag>
      <tag>girl</tag>
    <tags>
    <answers>
      <answer relevancy="50" time="2005-11-22T12:21:53Z">You can try to read her poetry. Chicks love that kind of things.</answer>
      <answer relevancy="0" time="2005-11-22T15:45:03Z">Don't bring her to a doughnuts shop. Ever. Girls don't like to be seen eating with their fingers - although  it's nice.</answer>
    </answers>
  </question>
</rsp>

Nous implémenterons l’API dans un nouveau module de l’application frontend, donc nous utilisons la ligne de commande pour générer le squelette du module:
symfony init-module frontend api
Authentification HTTP

Nous choisissons de llimiter l’accès à l’API au xseuls utilisateurs de Askeet. Pour cela, nous allons utiliser uneauthentificaiton HTTP, qui sera basé sur le mécanisme d’authentification du protocole HTTP. Ce processus est différent de l’authentification que nous avons mis en place précédemment puisqu’il ne fait pas intervenir de page web – tout est dans les en-têtes HTTP.
Nous aurons besoin de la méthode inclue dans le validateur lors du jour 6, donc dans un premier temps on fait un peu de factorisation et déplacement de code dans le modèle de la classe de UserPeer:

public static function getAuthenticatedUser($login, $password)
{
  $c = new Criteria();
  $c->add(UserPeer::NICKNAME, $login);
  $user = UserPeer::doSelectOne($c);

  // nickname exists?
  if ($user)
  {
    // password is OK?
    if (sha1($user->getSalt().$password) == $user->getSha1Password())
    {
      return $user;
    }
  }
  return null;
}

La nouvelle méthode UserPeer::getAtenticatedUser() peut maintenant être utilisée dans myLoginValidator.class.php (nous vous laissons le soin de le faire) et dans la nouvelle api/index du web service:

<?php

class apiActions extends sfActions
{
  public function preExecute()
  {
    sfConfig::set('sf_web_debug', false);
  }

  public function executeIndex()
  {
    $user = $this->authenticateUser();
    if (!$user)
   {
     $this->error_code    = 1;
     $this->error_message = 'login failed';

     $this->forward('api', 'error');
   }
   // do some stuff
  }

  private function authenticateUser()
  {
    if (isset($_SERVER['PHP_AUTH_USER']))
    {
      if ($user = UserPeer::getAuthenticatedUser($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']))
      {
        $this->getContext()->getUser()->signIn($user);

        return $user;
      }
    }

    header('WWW-Authenticate: Basic realm="askeet API"');
    header('HTTP/1.0 401 Unauthorized');
  }

  public function executeError()
  {
  }
}
?>

Dans un premier temps, avant d’exécuter quoique ce soit comme action du module API (celles dans la méthode preExecute()), nous allons désactiver la barre d’outils de débogage. La vue étant du XML pure l’insertion de la barre d’outils de débogage entrainerait une réponse non valide.
La première des choses que l’action index aura à faire sera de vérifier que le login et le mot de passe fourni, et s’ils correspondent à un compte existant dans l’application askeet. SI ce n’est pas le ca, la méthode authenticateUser() renverra une en-tête 401. cela déclenchera l’affichage d’un prompt d’authetification dans le navigateur de l’utilisateur; l’utilisateur aura alors à resoumettre une requête avec son login mot de passe.

// first request to the API, without authentication
GET /api/index HTTP/1.1
Host: mysite.example.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8) Gecko/20051111 Firefox/1.5
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
...

// the API returns a 401 header with no content
HTTP/1.x 401 Authorization Required
Date: Thu, 15 Dec 2005 10:32:44 GMT
Server: Apache
WWW-Authenticate: Basic realm="Order Answers Feed"
Content-Length: 401
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=iso-8859-1

// a login box will then appear on the user's window.
// Once the user enters his login/password, a new GET is sent to the server
GET /api/index HTTP/1.1
Host: mysite.example.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8) Gecko/20051111 Firefox/1.5
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
...
Authorization: Basic ZmFicG90OnN5bWZvbnk=

Un attribut d’autorisation est ajouté à la requête HTTP, qui est renvoyé une nouvelle fois. Elle contient le ‘login:mot de passe’ encrypté en base 64. c’est ce que représentent les variables $_SERVER['PHP_AUTH_USER'] et $_SERVER['PHP_AUTH_PW'] dans notre méthode authenticateUser().
Base64 ne renvoie pas une version encryptée de sa sortie. Decodé une chaîne de caractère encodée en Base64 est trés simple, et cela révèle le mot de passe en clair. Par exmple, décoder la chaîne de carctère ZmFicG90OnN5bWZvbnk= donne fabpot:symfony. Donc vous devez considérer que les mots de passe tranistent en clair sur Internet ( comme si’ls étaient entrés dans un formulaire) et peuvent être intercéptés. L’authentification HTTP doit être réservé aux contenus et service non critique pour cette raison. Une protection supplémentaire peut être mùise en place en utilisant le protocol HTTPS pour les appels à l’API.

<?php echo '<?' ?>xml version="1.0" encoding="utf-8" ?>
  <rsp stat="fail" version="1.0">
  <err code="<?php echo $error_code ?>" msg="<?php echo $error_message ?>" />
</rsp>

Bien sûr, vous devez définir toutes les vues du module api à un content-type XML, et désactiver le décorateur. Cela ce fait en ajoutant le fichier view.yml suivant dans le répertoire askeet/apps/frontend/modules/api/config/ directory:

all:
  has_layout: off
  http_metas:
    content-type: text/xml

réponse de l’API

La raison pour laquelle l’action index renvoie un forward(‘api’,'error’) au lieu d’un sfView::ERROR en cas d’erreur est que toutes les actions du module api utilisent la même vue. Imganiez que notre action index et une autre, par exemple popular, se termine par sfView::ERROR : nous aurions à renvoyer deux vues d’erreur identique (indexError.php te popularError.php) avec le même contenu. Le choix du forward() limite la répétition du code. De toute façon, il faut exécuter une autre action.
API response
construire une réponse XML est exactement comme construire une page XHTML. donc rien de ce uqi suit ne vous surprendra, maintenant que vous avez le jour 16 d’askeet derrière vous.

api/index action

public function executeQuestion()
{
  $user = $this->authenticateUser();
  if (!$user)
  {
    $this->error_code    = 1;
    $this->error_message = 'login failed';

    $this->forward('api', 'error');
  }

  if (!$this->getRequestParameter('stripped_title'))
  {
    $this->error_code    = 2;
    $this->error_message = 'The API returns answers to a specific question. Please provide a stripped_title parameter';

    $this->forward('api', 'error');
  }
  else
  {
    // get the question
    $question = QuestionPeer::getQuestionFromTitle($this->getRequestParameter('stripped_title'));

   if ($question->getUserId() != $user->getId())
   {
     $this->error_code    = 3;
     $this->error_message = 'You can only use the API for the questions you asked';

     $this->forward('api', 'error');
   }
   else
   {
     // get the answers
     $this->answers  = $question->getAnswers();
     $this->question = $question;
   }
  }
}

le template questionSuccess.php

<?php echo '<?' ?>xml version="1.0" encoding="utf-8" ?>
<rsp stat="ok" version="1.0">
  <question href="<?php echo url_for('@question?stripped_title='.$question->getStrippedTitle(), true) ?>" time="<?php echo strftime('%Y-%m-  %dT%H:%M:%SZ', $question->getCreatedAt('U')) ?>">
    <title><?php echo $question->getTitle() ?></title>
    <tags>
    <?php foreach ($sf_user->getSubscriber()->getTagsFor($question) as $tag): ?>
      <tag><?php echo $tag ?></tag>
    <?php endforeach ?>
    </tags>
    <answers>
    <?php foreach ($answers as $answer): ?>
      <answer relevancy="<?php echo $answer->getRelevancyUpPercent() ?>" time="<?php echo strftime('%Y-%m-%dT%H:%M:%SZ', $answer-   >getCreatedAt('U')) ?>"><?php echo $answer->getBody() ?></answer>
    <?php endforeach ?>
    </answers>
  </question>
</rsp>

Ajouter une nouvelle règle de routage pour l’appel à l’API

api_question:
  url:   /api/question/:stripped_title
  param: { module: api, action: question }

Test

Comme la réponse d’une API REST est simplement du XML, vous pouvez tester avec un navigateur en demandant:

http://askeet/api/question/what-shall-i-do-tonight-with-my-girlfriend

Intégration d’une API externe
intégrer une API externe n’est pas plus complexe que de lire un fichier XML en PHP. Comme il n’y a pas d’intérêt immédiat à intégrer une API externe dans askeet, nous allons décrire en quelques mots comment intégrer l’API askeet dans un autre site web – qu’il soit basé sur symfony ou non.
PHP5 intègre SimpleXML, quelques outils faciles d’utilisation pour interpréter et boucler sur un document XML. avec SimpleXML tous les noms d’éléments sont automatiquement mappés en propriétés d’un objet. et ce de manière récursive. Les attributs eux sont mappés comme des index.
Pour reconstruire une liste de réponses à une question donnée par l’API dans une simple page, tout ce qu’il y a besoin c’est de ces quelques lignes de PHP.

<?php $xml = simplexml_load_file(dirname(__FILE__).'/question.xml') ?>

<h1><?php echo $xml->question->title ?></h1>
<p>Published on <?php echo $xml->question['time'] ?></p>

<h2>Tags</h2>
<ul>
<?php foreach ($xml->question->tags->tag as $tag): ?>
<li><?php echo $tag ?></li>
<?php endforeach ?>
</ul>

<h2>Answers to this question from askeet users</h2>
<ul>
<?php foreach ($xml->question->answers->answer as $answer): ?>
<li>
<?php echo $answer ?>
<br />
Relevancy: <?php echo $answer['relevancy'] ?>% - Pulished on <?php echo $answer['time'] ?>
</li>
<?php endforeach ?>
</ul>

donation paypal
Tant que nous parlons des APIs externes certaines sont très simple à intégrer et peuvent apporter beaucoup à votre site. L’API de donation paypal est un simple bloque d’HTML dans lequel l’email du receveur doit être inclus.
Est ce que ce ne serait pas une bonne motivation pour les utilisateurs d’askeet qui répondent généreusemen aux question que de pouvoir recevoir une petite donation de tout utilisateur stisfait par la qualité de leur réponse? le bouton « donate » pourrait apparître sur la page de profil de l’utilisateur, et être liée à sa page de donation Paypal.
Premièrement, ajoutez la colonne add_paypal à la table User dans schema.xml:

<column name="has_paypal" type="boolean" default="0" />

Reconstruisez le modèle, et ajoutez au template user/show le code suivant

<?php if ($subscriber->getHasPaypal()): ?>
<p>If you appreciated this user's contributions, you can grant him a small donation.</p>
<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
<input type="hidden" name="cmd" value="_xclick">
<input type="hidden" name="business" value="<?php echo $subscriber->getEmail() ?>">
<input type="hidden" name="item_name" value="askeet">
<input type="hidden" name="return" value="http://www.askeet.com">
<input type="hidden" name="no_shipping" value="1">
<input type="hidden" name="no_note" value="1">
<input type="hidden" name="tax" value="0">
<input type="hidden" name="bn" value="PP-DonationsBF">
<input type="image" src="http://images.paypal.com/images/x-click-but04.gif" border="0" name="submit" alt="Donate to this user">
</form>
<?php endif ?>

Désormais un tuilisateur a la possibilité de déclarer un compte paypal lié à son adresse amil. Ce seriat une bonne occasion pour autoriser l’utilisateur à pouvoir modifier son profil, un bouton ‘edit profile’ devrait apparaître. Il sera lié à l’action user/edit, utilisée à la fois pour afficher le formulaire et pour effectuer le traitement lié à la soumission. Le formulaire de modification autorisera la modification du mot de passe et de l’adresse email. le surnom comme il est utilisé comme clé, ne peut pas être modifié. Maintenant que vous êtes à l’aise avec symfony, le code ne sera pas décrit ici mais simplement inclu dans le SVN.

A demain
Développer un webservice ou en intégrer un externe ne devrait pas vous poser de problème avec symfony.
Demain sera l’occasion d’aborder les filtres, et de diviser askeet.com en plusieurs sous projets comme php.askeet.com et symfony.askeet.com avec seulement quelques lignes de code. Si vous n’êtes pas convaincu par la vitesse de développement et la puissance de symfony, vous devriez changer d’avis.

Comme d’habitud, le code d’aujourd’hui a été soumis sur le depot SVN d’askeet; sous /tags/release_day_17_tag. Les quesiton et suggestions à propos de askeet et les tutoriels du calendrier de l’avent sont les bienvenues dans le forum askeet. A demain!

Posted by: mazenovi on 20 octobre 2008 @ 16 h 43 min
Filed under: dev

Pas de commentaire »

Pas encore de commentaire.

Flux RSS des commentaires de cet article. TrackBack URL

Laisser un commentaire