Article publié dans Linux Magazine 77, novembre 2005.
Copyright © 2005 - Philippe Bruhat
Après avoir décrit notre boîte à outils et quelques robots simples dans Linux Magazine 75, nous allons dans cette deuxième partie automatiser des sites plus complexes. Ceux-ci vont simuler une navigation plus longue et manipuler en profondeur les formulaires envoyés par les serveurs ou les requêtes que nous leur renverrons.
Un des mongueurs de Lyon est un habitué d'eBay : il vend et achète régulièrement des objets sur ce site de vente aux enchères.
Dans le monde des ventes aux enchères, il y a deux types d'enchérisseurs :
ceux qui qui achètent effectivement les objets pour lesquels ils enchérissent, et qui ont un profil positif ;
ceux qui ont tendance à se rétracter peu avant la fin d'une enchère ou même juste après. Ils sont peu fiables et pénibles pour le vendeur, car ils conduisent à l'échec de la vente. Ces derniers ont un profil négatif ou nul. (Les nouveaux venus sur eBay ont aussi un profil nul, ce qui joue un peu en leur défaveur au début.)
Lorsqu'il est dans le rôle du vendeur, notre mongueur ne souhaite donc pas voir participer ces enchérisseurs au profil négatif ou nul. Pour cela, il peut :
ajouter dans la description de l'objet en vente un message demandant aux profils nuls de contacter le vendeur avant d'enchérir, mais en pratique ces messages sont ignorés ;
aller consulter régulièrement les profils des enchérisseurs et annuler les enchères faites par ceux qui ont un profil négatif ou nul ;
déléguer à un robot cette tâche de consultation systématique et de filtrage des « profils nuls ».
Nous allons bien sûr construire le robot.
Pour accéder à ses enchères sur eBay, il faut tout d'abord s'identifier.
mech-dump nous montre les formulaires de la page d'ouverture de session :
$ mech-dump https://signin.ebay.fr/ws/eBayISAPI.dll?SignIn POST https://scgi.ebay.fr/ws/eBayISAPI.dll?RegisterEnterInfo&siteid=71&co_partnerid=2&UsingSSL=1 [RegisterEnterInfo] MfcISAPICommand=RegisterEnterInfo (hidden readonly) co_partnerId=2 (hidden readonly) siteid=71 (hidden readonly) ru= (hidden readonly) bin=-1 (hidden readonly) <NONAME>=S'inscrire > (submit) POST https://signin.ebay.fr/ws/eBayISAPI.dll?co_partnerid=2&siteid=71&UsingSSL=1 [SignInForm] MfcISAPICommand=SignInWelcome (hidden readonly) siteid=71 (hidden readonly) co_partnerId=2 (hidden readonly) UsingSSL=1 (hidden readonly) ru= (hidden readonly) pp= (hidden readonly) pa1= (hidden readonly) pa2= (hidden readonly) pa3= (hidden readonly) i1=-1 (hidden readonly) pageType=-1 (hidden readonly) rtmData= (hidden readonly) userid= (text) pass= (password) <NONAME>=Ouvrir une session en mode sécurisé > (submit) keepMeSignInOption=<UNDEF> (checkbox) [*<UNDEF>/off|1/Je souhaite rester connecté sur cet ordinateur ; je fermerai moi-même la session. Astuces sur la sécurité de votre compte Assurez-vous que l'URL affichée ci-dessus commence par https://signin.ebay.fr/]
Le premier formulaire concerne l'inscription initiale, avec ouverture de
compte chez eBay. Celui qui nous intéresse est le second qui correspond
à l'ouverture d'une session (ainsi que l'atteste la présence des champs
userid
et pass
).
# récupération du formulaire $m->get('https://signin.ebay.fr/ws/eBayISAPI.dll?SignIn'); die 'Échec de connexion : ' . $m->res->status_line() unless $m->success(); # remplissage et validation du formulaire $m->form_number(2); $m->set_fields( userid => $userid, pass => $pass, ); $m->submit(); # connexion réussie ? die 'Échec de validation du formulaire : ' . $m->res()->status_line() unless $m->success();
Nous allons utiliser la technique utilisée avec Skype (dans GLMF 75) pour nous assurer que l'identification est correcte. La ligne suivante nous affichera la liste des cookies reçus par notre robot :
$m->cookie_jar()->scan( sub { print "$_[1]\n" } );
En cas d'identification réussie, le robot reçoit les cookies dp1
, ebay
,
nonsession
, ns1
, s
et secure_ticket
. En cas d'échec,
on constate que le cookie secure_ticket
n'est
pas envoyé. On peut remarquer de plus que c'est le seul des cinq cookies
qui est marqué comme secure, c'est-à-dire qu'il ne peut être
envoyé au serveur qu'à travers une connexion sécurisée (i.e. en SSL).
Comme pour Skype dans l'article précédent, la vérification de la réussite de notre identification se fait de la façon suivante, en inspectant les cookies :
{ my $ok = 0; $m->cookie_jar()->scan( sub { $ok++ if $_[1] eq 'secure_ticket' } ); die "Échec d'identification : informations d'ouverture de session invalides" unless $ok; }
Notre script sait désormais se connecter et vérifier que l'identification est réussie.
Nous allons maintenant récupérer la liste des objets que nous avons mis en vente, afin de pouvoir ensuite trouver les enchérisseurs dont le profil ne nous revient pas.
Notre robot possédant les bons cookies, nous pouvons nous connecter directement à la page qui nous intéresse :
# récupération de "mes ventes" $m->get( 'http://my.ebay.fr/ws/eBayISAPI.dll?MyeBay&CurrentPage=MyeBaySelling'); die 'Échec de connexion à "mes ventes" : ' . $m->res()->status_line() unless $m->success();
Les liens vers les pages des objets mis en vente sont reconnaissables
à l'URL du lien associé : chacun contient la chaîne ViewItem
et
l'identifiant de l'objet sous la forme item=numero
.
WWW::Mechanize
fournit la méthode find_all_links()
, qui renvoie
la liste des liens correspondant aux paramètres de recherche fournis.
Ces paramètres sont les mêmes pour les trois méthodes follow_link()
(équivalent de get()
à partir d'un lien dans la page), find_link()
(qui renvoie un seul lien correspondant aux critères de recherche)
et find_all_links()
(qui renvoie tous les liens correspondants).
Les critères de recherche décrits ci-après ont tous (sauf le dernier, n
)
un équivalent de la forme xxx_regex
qui accepte une expression
régulière comme argument, afin de faire une recherche plus souple. Les
paramètres sans le suffixe _regex
ne correspondront qu'en cas
d'identité stricte.
Ces critères sont :
text
le texte du lien (contenu entre <a>
et </a>
) ;
url
l'URL du lien (telle que codée dans la page, relative ou absolue) ;
url_abs
l'URL de lien (convertie en URL absolue, même si elle est relative dans le fichier HTML) ;
name
le nom du lien (attribut name
) ;
tag
la balise du lien (plus utile sous la forme tag_regex
pour obtenir
des liens extraits de plusieurs balises).
Les liens sont extraits des balises <a href=...>
,
<area href=...>
, <frame src=...>
, <iframe src=...>
et <meta content=...>
.
n
Le numéro d'ordre du lien dans la page (WWW::Mechanize
compte
à partir de 1). Pour les méthodes follow_link()
et find_link()
(qui ne doivent trouver qu'un lien au maximum), si n
n'est pas fourni,
il prend la valeur 1.
Nous allons ici utiliser la méthode find_all_links()
avec le paramètre
url_regex
pour trouver les pages associées à tous les objets mis
en vente :
my @ventes = $m->find_all_links( url_regex => qr/\?ViewItem.*&item=\d+/ );
Les objets renvoyés par find_all_links()
sont de type
WWW::Mechanize::Link
. Ils disposent de plusieurs accesseurs comme
url()
, text()
, tag()
, etc. qui fournissent tous les informations
concernant le lien tel qu'il était dans la page web. Ils peuvent être
passés directement à la méthode get()
de WWW::Mechanize
qui saura
les utiliser pour récuperer les pages concernées.
En accédant à la page dédiée à chaque objet mis en vente, nous obtenons la liste des enchérisseurs.
Soit $item
l'élément de @ventes
que nous
analysons. Nous allons tout d'abord récupérer l'historique des enchères
sur cet objet :
# boucle sur le contenu de @ventes my ($id) = ( $item->url() =~ /item=(\d+)/g ); print "Objet : ", $item->text(), " ($id)\n"; # pour info # connexion directe à l'historique des enchères sur cet objet $m->get( 'http://offer.ebay.fr/ws/eBayISAPI.dll?ViewBids&item=$id' ); do { warn "Échec de connexion aux enchères de l'objet $id : " . $m->res()->status_line(); $m->back(); next; } unless $m->success();
Note :
La méthode back()
réalise la même opération que le bouton « Précédent »
de votre navigateur web. WWW::Mechanize
conserve en mémoire l'historique
des pages visitées (et s'en sert par exemple pour renseigner le champ
d'en-tête Referer:
). Il est intéressant de suivre autant que possible
la logique de la navigation et donc d'utiliser back()
quand votre
programme retourne en arrière (par exemple en fin de boucle). Ceci permet
d'utiliser les méthodes usuelles de WWW::Mechanize
dans presque
tous les cas, et d'avoir des en-têtes Referer:
cohérents avec
l'organisation du site.
Les enchérisseurs sont listés sur cette page, avec un lien pour chaque
profil. find_all_links()
et une requête un peu plus élaborée que
précédemment, vont nous renvoyer la liste des liens vers des profils
négatifs ou nuls.
# détection des enchérisseurs avec profil négatif ou nul my @negs = $m->find_all_links( url_regex => qr/\?ViewFeedBack/, # lien vers les détails du profil text_regex => qr/^[-0]/ # profil négatif ou nul );
Il nous faut maintenant extraire le pseudonyme de ces enchérisseurs.
Le lien ViewFeedBack
contient un paramètre userid
, ce qui va
nous faciliter la chose. Nous allons stocker les identifiants des
utilisateurs dans une table de hachage, qui nous garantit par construction
que chaque pseudonyme n'apparaîtra qu'une seule fois.
Note : L'utilisation d'un hash pour gérer des listes non ordonnées dont les éléments n'apparaissent qu'une seule fois est un idiome Perl.
Normalement, l'expression régulière /userid=(.+?)(?:&|$)/
devrait
suffire (on s'arrête en fin de chaîne ou au premier &
rencontré).
my %negs; foreach my $link (@negs) { $link->url() =~ /(?:userid|ReturnUserEmail&requested)=(.+?)(?:&|$)/; # ajoute la clé correspondant au pseudonyme dans le hash $negs{$1} = ''; }
Lors de nos essais, nous avons constaté que l'identifiant de l'utilisateur
apparaît parfois après la chaîne ReturnUserEmail&requested=
pour une
raison obscure. C'est pourquoi nous utilisons une expression un peu
plus compliquée, afin de gérer tous les cas connus. Celle-ci pourra
d'ailleurs être étendue si d'autres formes venaient à se présenter.
Nous obtenons finalement un hash dont les clés sont les pseudos de tous les enchérisseurs importuns. Il ne reste plus qu'à accéder à la page d'annulation des enchères pour chacun d'entre eux et à soumettre le formulaire adéquat complété avec le numéro d'objet, le pseudo de l'acheteur et un petit message mentionnant la raison de l'éviction. Nos manières expéditives ne doivent pas nous empêcher de rester polis.
# suppression des enchères non souhaitées foreach my $pseudo (keys %negs) { $m->get('http://offer.ebay.fr/ws/eBayISAPI.dll?CancelBidShow'); $m->submit_form( # le formulaire 1 correspond à la boîte de recherche # c'est donc le formulaire 2 que nous devons remplir form_number => 2, fields => { item => $item, buyeruserid => $pseudo, info => "Profil <= 0 ; n'a pas pris contact avant d'enchérir." } ); }
Tous les bouts de code présentés précédemment ont été mis ensemble pour
produire un script complet qui accepte des options de ligne de commande
(--userid
, --pass
, --verbose
, etc.). Il ne reste plus qu'à ajouter la
lecture d'un fichier $HOME/.ebayrc et il sera parfait (passer un mot
de passe sur la ligne de commande, ce n'est pas beaucoup mieux que de le
mettre dans le source du script : l'idéal est bien d'avoir un fichier
de configuration à part).
Le script remanié reste d'une longueur raisonnable :
#!/usr/bin/perl use strict; use warnings; use Getopt::Long; use WWW::Mechanize; # options par défaut my %CONF = ( verbose => 0, base => 'ebay.fr', userid => '', # À compléter avec vos identifiant pass => '', # et mot de passe ); # récupération des paramètres de ligne de commande GetOptions( \%CONF, "verbose!", "base=s", "userid=s", "pass=s" ) or die << 'USAGE'; Options disponibles : --base <site> : base site (ebay.fr, ebay.com, etc.) --userid <userid> --pass <password> --verbose USAGE # calcul des URL spécifiques my %url = ( signin => "http://signin.$CONF{base}/ws/eBayISAPI.dll?SignIn", sales => "http://my.$CONF{base}/ws/eBayISAPI.dll?MyeBay&CurrentPage=MyeBaySelling", bids => "http://offer.$CONF{base}/ws/eBayISAPI.dll?ViewBids&item=", cancelbid => "http://offer.$CONF{base}/ws/eBayISAPI.dll?CancelBidShow" ); # création du robot my $m = WWW::Mechanize->new(); # récupération du formulaire $m->get( $url{signin} ); die "Échec de connexion à la page de login : " . $m->res()->status_line() unless $m->success(); # remplissage et validation $m->form_number(2); $m->set_fields( userid => $CONF{userid}, pass => $CONF{pass}, ); $m->submit(); die "Échec d'envoi du formulaire : " . $m->res()->status_line() unless $m->success(); # identification réussie ? { my $ok = 0; $m->cookie_jar()->scan( sub { $ok++ if $_[1] eq 'secure_ticket' } ); die "Échec du login : informations d'ouverture de session invalides" unless $ok; print "Connecté à $CONF{base} en tant que $CONF{userid}\n" if $CONF{verbose}; } # récupération de "mes ventes" $m->get( $url{sales} ); die 'Échec de connexion à "mes ventes" : ' . $m->res()->status_line() unless $m->success(); my @ventes = $m->find_all_links( url_regex => qr/\?ViewItem.*&item=\d+/ ); # traitement de chaque objet en vente for my $item (@ventes) { my ($id) = ( $item->url() =~ /item=(\d+)/g ); print "Objet : ", $item->text(), " ($id)\n" if $CONF{verbose}; # connexion directe à l'historique des enchères sur cet objet $m->get( $url{bids} . $id ); do { warn "Échec de connexion aux enchères de l'objet $id : " . $m->res()->status_line(); $m->back(); next; } unless $m->success(); # détection des enchérisseurs avec profil négatif ou nul my @negs = $m->find_all_links( url_regex => qr/\?ViewFeedBack/, text_regex => qr/^[-0]/, ); my %negs; foreach my $link (@negs) { $link->url() =~ /(?:userid|ReturnUserEmail&requested)=(.+?)(?:&|$)/; # ajoute la clé correspondant au pseudonyme dans le hash $negs{$1} = ''; } # suppression des enchères non souhaitées foreach my $pseudo ( keys %negs ) { $m->get( $url{cancelbid} ); $m->submit_form( form_number => 2, fields => { item => $id, buyeruserid => $pseudo, info => "Profil <= 0 ; n'a pas pris contact avant d'enchérir." } ); $m->back(); } $m->back(); }
Un point important à noter est que, puisque la base d'utilisateurs eBay est
mondiale et que tous les sites eBay utilisent le même moteur de gestion
de comptes et d'enchères, les divers sites eBay sont complètement
interchangeables du point de vue de notre navigation (à la langue
du site près, d'où l'intérêt de s'intéresser aux éléments invariants
entre les langues). Ainsi, si l'on remplace ebay.fr
par ebay.com
,
ebay.ca
, ebay.de
, ebay.at
, ebay.ie
, ebay.it
, ebay.nl
,
ebay.es
, ebay.ch
, ebay.co.uk
, ebay.com.au
ou ebay.com.cn
,
l'identification (et donc très probablement le reste du script) fonctionne
parfaitement.
Les sites ebay.se
, ebay.com.hk
, ebay.ph
, ebay.com.sg
semblent
utiliser des moteurs différents (mais identiques entre eux).
Une exception notable est le site ebay.be
qui semble partagé entre
flamands et wallons : il y a un niveau d'indirection supplémentaire
pour le choix de la langue qui empêche le script d'identification de
fonctionner.
Quelques autres sites eBay ne fonctionnent pas si on utilise ce script
tel quel. Je n'ai pas cherché à gérer cet aspect dans cet article, me
contentant d'une option --base
pour éventullement changer de site
eBay.
Depuis que j'ai un appareil photo numérique, je fais trop de photos. Le format JPEG n'étant pas toujours adapté pour montrer les photos à la famille, il faut parfois sortir les photos sur papier.
Un certain nombre d'entreprises proposent de développer vos photos
numériques très facilement : il suffit d'aller sur leur site web,
d'y déposer les photos et d'indiquer dans quel magasin de l'enseigne
on veut aller les chercher. Ça marche bien pour une dizaine de photos ;
mais c'est moins pratique quand je veux télécharger des centaines de
photos de la plus belle nièce du monde... Je préférerais donc lister
les photos dans un fichier que je passerais à un script qui se chargerait
du reste.
C'est ce que nous allons faire.
J'ai choisi pour mes photos l'entreprise Photo Station (http://www.photostation.fr). Comme d'habitude, nous allons d'abord naviguer un peu sur leur site, afin de nous faire une idée de l'interface du site web et des formulaires que nous allons rencontrer.
Première bonne surprise, le site propose une interface HTML en plus de l'interface ActiveX (réservée à Internet Explorer). C'est évidemment sur celle-ci que nous allons nous appuyer.
La navigation sur le site par l'interface HTML se fait selon la progression suivante :
Sélection de l'interface (nous choisirons toujours l'interface HTML)
Sélection du type de travail (« Formats classiques », « Agrandissements », « Produits Photo Fun »).
Sélection de l'interface (ActiveX ou HTML) ;
Sélection et envoi des fichiers images ;
Sélection du format des photos, ou sélection du format de chaque image (optionnel) ;
Si l'on ne s'était pas identifié jusqu'à présent (le formulaire apparait sur toutes les pages), identification avec son compte utilisateur ;
Sélection du magasin et acceptation des conditions ;
Validation de la commande
À l'issue de cette visite, nous avons déjà constaté (rien qu'en regardant
les URL dans le navigateur) qu'hormis la page d'accueil, l'intégralité de
l'application d'envoi des photos est gérée par le site photoprintit.de
.
Les « Produits Photo Fun » étant très spécifiques, nous allons nous intéresser uniquement aux « Formats classiques » et tirer toutes les photos au même format. Étant donnée une liste de photos, le rôle de notre script sera donc de les envoyer sur le site, de sélectionner le format des tirages, de sélectionner un magasin et enfin de valider la commande. Et bien sûr, ne pas oublier de nous identifier à un moment ou un autre.
Commençons par l'identification justement, ça ne doit pas être bien compliqué.
Comme tout bon client, nous allons créer manuellement un compte utilisateur avec notre adresse e-mail et le mot de passe idoine. Ceux-ci nous serviront à passer la commande à la fin.
Le formulaire d'identification est présent dès la page d'accueil de http://www.photostation.fr/. Pourtant, bien que visible sur cette la d'accueil chargée par notre navigateur, le formulaire d'identification en question n'est pas détecté par mech-dump :
$ mech-dump http://www.photostation.fr/index.php POST http://www.photostation.fr/entreprise/newsletter.php [newsletter] E_MAIL=Votre e-mail (text) imageField=<UNDEF> (image) POST http://www.photostation.fr/magasin/viamichelin.php [list3] intMapType=1 (hidden readonly) productId=50739 (hidden readonly) from=1234 (hidden readonly) withCriteria=false (hidden readonly) strCountry=000001424 (hidden readonly) strCP= (text) <NONAME>=<UNDEF> (image)
Note : en période promotionnelle, on trouve parfois une animation Flash au bout de http://www.photostation.fr/. La « vraie » page principale se trouve alors à http://www.photostation.fr/index.php.
En fait, le formulaire de connexion est fourni dans une balise iframe
.
Ceci confirme d'ailleurs que PhotoStation utilise les services
d'un fournisseur spécialisé pour la récupération des photos :
photoprintit.de
.
WWW::Mechanize
nous permet de suivre des liens
à partir de l'indication de la balise qui les contient. Nous allons ainsi
récupérer le contenu du cadre en question, et afficher le formulaire :
$m->get('http://www.photostation.fr/index.php'); $m->follow_link( tag => "iframe" ); print $m->current_form()->dump();
Ce qui nous affichera :
POST http://asp07.photoprintit.de/microsite/customers/1156/live/templates/login_iframe.php [external_login_form] PHPSESSID=184c6840f65e07d7057c0171dcbfe886 (hidden readonly) external_login_user=Email (text) external_login_pwd=Mot de passe (password) external_login[]=<UNDEF> (image)
Remplir le formulaire est trivial :
$m->set_fields( external_login_user => $email, external_login_pwd => $password, ); $m->submit();
Normalement, la page reçue en retour contient l'adresse email utilisée
pour se connecter, confirmant que nous nous sommes correctement identifiés.
On peut le vérifier en inspectant le contenu de $m->content()
, comme
ceci :
print $m->content =~ /\Q$email\E/ ? "ok" : "nok";
Note : L'utilisation de \Q
et \E
, qui émulent quotemeta()
à
l'intérieur de l'expression régulière, permet d'empêcher l'interprétation
par l'expression régulière des points et autres métacaractères que
l'adresse email pourrait contenir.
Le seul problème, c'est que notre petit script de test affiche nok
.
Pour comprendre ce qui se passe, nous allons utiliser notre proxy et tenter de nous identifier avec Firefox :
POST http://asp09.photoprintit.de/microsite/customers/1156/live/templates/login_iframe.php Cookie: PHPSESSID=5b56e6e73c7a90624c0a2755248f250a Referer: http://asp07.photoprintit.de/microsite/1156/customers/1156/live/templates/login_iframe.php PHPSESSID => 5b56e6e73c7a90624c0a2755248f250a external_login_user => ******* external_login_pwd => ******* external_login[].x => 0 external_login[].y => 0 200 OK Content-Type: text/html; charset=utf-8
Et comparer avec notre robot :
POST http://asp06.photoprintit.de/microsite/customers/1156/live/templates/login_iframe.php Cookie: PHPSESSID=bbf50583177dc9c43cc02dc39af4f397 Cookie2: $Version="1" Referer: http://asp.photoprintit.de/1156/customers/1156/live/templates/login_iframe.php PHPSESSID => bbf50583177dc9c43cc02dc39af4f397 external_login_user => ******* external_login_pwd => ******* 200 OK Content-Type: text/html; charset=utf-8
Outre l'en-tête Cookie2
, la différence principale c'est qu'avec le
navigateur, nous avons cliqué sur l'image contenue dans le formulaire.
Notre robot n'a pas renvoyé les champs external_login[].x
et
external_login[].y
.
Heureusement, WWW::Mechanize
permet également de simuler le clic sur
une image :
$m->click('external_login[]');
En utilisant cette commande au lieu de submit()
, le robot envoie les
champs attendus et notre script de test affiche ok
, confirmant que
nous avons réussi à nous authentifier sur le site !
Maintenant que nous sommes identifiés, nous allons déposer nos photos. Pour cela, il suffit de sélectionner l'interface « Netscape/Autre » sur la page principale.
Avec la méthode back()
, nous allons revenir à la page principale
(le robot est actuellement sur la page de réponse au formulaire
d'identification).
# après identification $m->back(); # retour au formulaire d'identification $m->back(); # retour à la page principale # on suit le lien vers l'interface HTML $m->follow_link( url_regex => qr/htmlclient/ ); # affiche tous les formulaires de la page print $_->dump(), "\n" for $m->forms();
Voici les formulaires visibles :
POST http://asp05.photoprintit.de/microsite/htmlclient.php?customerid=1156 [external_login_form] PHPSESSID=4a58ab74f9342a384a60e57c591e581d (hidden readonly) accountmode=1 (hidden readonly) external_login_user=Email (text) external_login_pwd=Mot de passe (password) external_login[]=<UNDEF> (image) POST http://asp05.photoprintit.de/microsite/1156/htmlclient.php PHPSESSID=4a58ab74f9342a384a60e57c591e581d (hidden readonly) quantity=10 (option) [*10|20|30|40|50] <NONAME>=<UNDEF> (image) POST http://asp05.photoprintit.de/microsite/1156/receive.php (multipart/form-data) [upload_form] PHPSESSID=4a58ab74f9342a384a60e57c591e581d (hidden readonly) UPLOAD_METER_ID=85582e6a2b60e80cc5339f7d809ff308 (hidden readonly) file0= (file) file1= (file) file2= (file) file3= (file) file4= (file) file5= (file) file6= (file) file7= (file) file8= (file) file9= (file) upload=<UNDEF> (image)
Avant de nous occuper du dernier formulaire, il nous faut remarquer un petit problème : le premier formulaire de la page est un formulaire d'identification, alors que nous sommes censés nous être identifiés précédemment. Que s'est-il passé ?
La réponse à cette question apparaît quand on demande au robot de nous afficher ses cookies :
print $m->cookie_jar()->as_string();
Ce qui donne le résultat suivant :
Set-Cookie3: PHPSESSID=4a58ab74f9342a384a60e57c591e581d; path="/"; domain=asp05.photoprintit.de; path_spec; discard; version=0 Set-Cookie3: PHPSESSID=bbf50583177dc9c43cc02dc39af4f397; path="/"; domain=asp06.photoprintit.de; path_spec; discard; version=0
Nous avons deux cookies, l'un pour asp05.photoprintit.de
et l'autre
pour asp06.photoprintit.de
.
En y regardant d'un peu plus près, on constate qu'effectivement, le
formulaire d'identification a été récupéré sur asp06
et le formulaire
d'envoi des photos sur asp05
. Selon toute vraisemblance, il y a un
système d'équilibrage de charge qui nous a redirigé une première fois vers
asp06
et la seconde vers asp05
. Seul le cookie associé à asp06
nous identifie.
Après vérification, ce problème se pose également avec Firefox quand on
s'identifie dans le <iframe>
affiché sur la page principale. Quand
ensuite on choisit une commande de photos, la boîte d'identification
attend encore notre e-mail et notre mot de passe.
La raison est très simple : le site vers lequel pointe la première
page a une URL en asp.photoprintit.de
. Quand on clique effectivement
sur le lien, le site fait une redirection automatique vers l'un des
sites asp01
à asp10
. Notre proxy espion nous montre comment cela
se passe :
GET http://asp.photoprintit.de/ 302 Found Content-Type: text/html; charset=iso-8859-1 Location: http://asp03.photoprintit.de/
Une fois connecté sur le site, tous les liens sont relatifs et on reste donc sur le même serveur.
Or l'iframe
qui se trouve dans la page d'accueil du site
PhotoStation pointe également vers le nom générique asp
(vers
http://asp.photoprintit.de/1156/customers/1156/live/templates/login_iframe.php,
pour être précis). Le jeu des redirections fait donc qu'il est fort peu
probable qu'une identification réalisée dans le formulaire de l'iframe
du début soit valable sur le serveur qui traitera notre demande (1 chance
sur 10 très exactement).
Heureusement, dès que l'on commence à aller chercher les pages sur
photoprintit.de
, le formulaire d'identification fait partie intégrante
de page (ce n'est plus un iframe
). Il nous suffit donc de remettre
les lignes de notre script dans le bon ordre et d'aller d'abord sur le
site du prestataire, puis de nous identifier.
my $m = WWW::Mechanize->new; $m->get('http://www.photostation.fr/'); # d'abord on va chez le fournisseur $m->follow_link( url_regex => qr/htmlclient/ ); # puis on s'identifie $m->set_fields( external_login_user => $email, external_login_pwd => $password, ); $m->click( 'external_login[]' ); print $_->dump(), "\n" for $m->forms(); print $m->cookie_jar()->as_string();
Ceci a l'avantage de simplifier notre script : comme le formulaire
d'identification fait partie de la page, il n'y a plus besoin
de faire des back()
pour revenir à la page qui nous intéresse.
Voici le résultat :
POST http://asp07.photoprintit.de/microsite/htmlclient.php external_logout[]=<UNDEF> (image) POST http://asp07.photoprintit.de/microsite/htmlclient.php quantity=10 (option) [*10|20|30|40|50] <NONAME>=<UNDEF> (image) POST http://asp01.photoprintit.de/microsite/receive.php (multipart/form-data) [upload_form] UPLOAD_METER_ID=22a21519eca86d05620e69e709f7945d (hidden readonly) file0= (file) file1= (file) file2= (file) file3= (file) file4= (file) file5= (file) file6= (file) file7= (file) file8= (file) file9= (file) upload=<UNDEF> (image) Set-Cookie3: PHPSESSID=9d08c5f4bf02846ae8580c6642937e35; path="/"; domain=asp07.photoprintit.de; path_spec; discard; version=0
Le formulaire d'identification nous propose cette fois de nous déconnecter (prouvant ainsi que nous sommes identifiés), un formulaire permet de requérir plus de champs d'ajout de photo, et le dernier formulaire est celui qui permet de déposer les photos. Enfin, nous n'avons cette fois-ci qu'un seul cookie, associé au serveur qui va gérer toute notre transaction.
Il faut noter que le formulaire d'identification/déconnexion sera toujours le premier formulaire des pages à venir. Les formulaires que nous validerons désormais seront toujours le deuxième ou le troisième de la page.
Par défaut, le formulaire envoyé par le site propose 10 entrées pour des fichiers de photo, et un formulaire permet de sélectionner 20, 30, 40 ou 50 entrées. Si on veut envoyer plus de 10 photos, il faudra donc faire une requête supplémentaire pour charger le formulaire adéquat.
Ce formulaire est le deuxième de ceux renvoyés par la requête précédente :
POST http://asp07.photoprintit.de/microsite/htmlclient.php quantity=10 (option) [*10|20|30|40|50] <NONAME>=<UNDEF> (image)
En le regardant, on imagine sans peine que si on change la valeur
du champ quantity
pour y mettre une valeur quelconque, le serveur
nous renverra une page avec le nombre de champs file
demandé.
$m->form_number(2); # il s'agit du deuxième formulaire de la page $m->field( quantity => 7 ); # valeur non standard
On constate que HTML::Form
respecte scrupuleusement les spécifications
du formulaire et n'accepte pas de valeur non prévue. Notre script meurt
en affichant le message suivant :
Illegal value '7' for field 'quantity' at /usr/share/perl5/WWW/Mechanize.pm line 1030
Cela vient du fait que le champ quantity
est défini comme étant de type
<select>
dans le source HTML du formulaire. Dans l'objet HTML::Form
construit par WWW::Mechanize
à partir du source HTML, cela se traduit
par un objet de type HTML::Form::ListInput
. C'est cet objet qui fait
les vérifications et provoque l'erreur ci-dessus.
Qu'à cela ne tienne, nous n'avons qu'à décider qu'il s'agit en fait
d'un champ de type text
normal ! Pour cela, il suffit de changer
l'idée que Perl se fait de cet objet, en changeant sa classe :
bless $m->current_form()->find_input('quantity'), 'HTML::Form::TextInput';
Le champ quantity
acceptera désormais n'importe quelle valeur et l'enverra
au serveur lors de la validation du formulaire.
Soit @photos
le tableau contenant la liste des fichiers à envoyer.
Nous pourrons donc étendre le formulaire si le besoin s'en fait sentir :
if ( @photos > 10 ) { $m->form_number(2); bless $m->current_form()->find_input('quantity'), 'HTML::Form::TextInput'; $m->field( quantity => scalar @photos ); $m->click; }
Notre script pourra donc envoyer autant d'images que nous le souhaitons via un seul formulaire.
Nous pouvons maintenant remplir ce formulaire sans difficulté :
# sélectionne le 3ème formulaire de la page $m->form_number(3); # ajoute les photos au formulaire $m->set_fields( "file$_" => $photos[$_] ) for 0 .. @photos - 1;
Une photo d'assez bonne qualité pour être imprimée pèse entre 1,5 et 2 mégaoctets. Même pour une dizaine de photos, ça veut dire envoyer pas loin 16 Mo en remontant à travers la liaison ADSL.
Pendant le téléchargement des photos, le site envoie une popup (associée à un identifiant unique) rafraîchie régulièrement qui donne l'état d'avancement du transfert.
Dans notre script, si nous appellons directement la méthode submit()
,
le script ne récupèrera
la main qu'à la fin du téléchargement c'est-à-dire au bout d'une dizaine
de minutes sur une ligne à 30 ko/s. Il nous faut donc donner la
possibilité au script de nous indiquer son avancement, sans quoi il nous
sera impossible de savoir ce qui se passe et on risque de l'interrompre
à tort. De plus, nous ne voulons pas charger 16 Mo de photos en mémoire
juste pour les envoyer sur le réseau !
Heureusement, la libraire LWP sait justement aussi produire des requêtes
HTTP::Request
qui envoient beaucoup de données au serveur sans
tout charger en mémoire (merci Gisles !). Et WWW::Mechanize
,
en bonne sous-classe de LWP::UserAgent
, sait les utiliser.
Habituellement, un objet HTTP::Request
associé à un POST
contient
un paramètre accessible par la méthode content()
. Ceci permet d'avoir
accès (en lecture et en écriture) au contenu du corps de la requête avant
de l'envoyer. Pour les requêtes volumineuses, il est prévu que cet
attribut puisse être une fonction de rappel qui renvoie les données morceaux
par morceaux. Ainsi, la mémoire occupée reste raisonnable.
HTTP::Request::Common
, la classe de base de HTTP::Request
sait
construire automatiquement la fonction en question quand on veut envoyer
des fichiers au serveur. Il suffit pour cela de mettre la variable globale
HTTP::Request::Common::DYNAMIC_FILE_UPLOAD
à une valeur vraie.
C'est la méthode make_request()
de HTML::Form
qui produit l'objet
HTTP::Request
attendu.
my $req; { no warnings; local $HTTP::Request::Common::DYNAMIC_FILE_UPLOAD = 1; $req = $m->current_form()->make_request; }
Note : no warnings
évite que l'avertissement
Name "HTTP::Request::Common::DYNAMIC_FILE_UPLOAD" used only once: possible typo
soit affiché.
Ainsi, pour réaliser un petit affichage qui va nous faire
patienter pendant le téléchargement, il suffit de remplacer
la routine en question par une autre qui réalise un affichage. Cette
nouvelle routine devra évidemment renvoyer les données initialement
renvoyées par celle générée par HTTP::Request::Common
, afin de ne
pas modifier la requête.
Nous allons ici nous contenter d'afficher un pourcentage d'avancement.
Pour cela, nous utilisons la longueur totale de la requête telle que
calculée par HTTP::Request::Common
(et accessible depuis l'objet
HTTP::Request
via la méthode content_length()
), et nous calculons la
taille des données déjà envoyées grâce à une petite addition.
Notre code devient :
my $req; { no warnings; local HTTP::Request::Common::DYNAMIC_FILE_UPLOAD = 1; $req = $m->current_form()->make_request(); # récupère la routine générée par HTTP::Request::Common my $sub = $req->content(); my $done = 0; my $total = $req->content_length(); $req->content( sub { my $data = $sub->(); # appelle la routine générée return unless defined $data; $done += length $data; printf "\rTransfert: %2d%%", $done / $total * 100; return $data; } ); }
Note : Le no warnings
évite en plus ici l'affichage d'un
avertissement Use of uninitialized value in length
quand $data
vaut undef
en fin de lecture des fichiers.
Pour que l'affichage se fasse correctement (nous utilisons \r
), il
ne faut pas oublier de passer la sortie standard en mode non tamponné
(unbuffered). La méthode la plus lisible pour le faire est :
use IO::Handle; STDOUT->autoflush(1);
Il ne reste plus qu'à passer la requête à WWW::Mechanize
pour qu'il
envoie effectivement les photos et qu'on puisse passer à la suite du
traitement.
Dans le cadre de cet article, nous nous intéressons seulement à la commande d'un lot de photos toutes dans le même format.
La page suivante nous propose donc le formulaire suivant pour sélectionner le format de nos tirages :
POST http://asp07.photoprintit.de/microsite/1156/formats.php orderdata[4]=0 (text) orderdata[5]=1 (text) orderdata[11]=0 (text) orderdata[6]=0 (text) album=0 (hidden readonly) album=<UNDEF> (checkbox) [*<UNDEF>/off|1/ ] order[]=<UNDEF> (image) customcopies=1 (text) customformatid_4[]=4 (image) customformatid_5[]=5 (image) customformatid_11[]=11 (image) customformatid_6[]=6 (image) custom[]=<UNDEF> (image)
En comparant avec le formulaire visualisé par notre navigateur, nous voyons que pour le cas qui nous occupe (commande express de photos toutes au même format), seuls les quatre premiers champs nous intéressent.
La valeur dans le champ correspond au nombre de tirages de chaque taille
qui sera fait pour chaque photo. En ce qui concerne les formats, on vérifie
avec le source HTML que le
champ orderdata[4]
correspond au format 9x11, orderdata[5]
au format
10x13, orderdata[11]
au format 11x15 et orderdata[6]
au 13x17.
Pour connaitre le nombre total de photos, il faut donc multiplier la somme
des valeurs de ces champs par le nombre total de photos.
Une fois le format sélectionné, on clique sur le bouton « Commande
Express » qui correspondant au champ order[]
du formulaire.
À noter que même si on prend le choix par défaut, il est préférable de le sélectionner nous-même, pour le cas où le défaut changerait dans le futur (évitons les mauvaises surprises).
$m->form_number(2); $m->set_fields( 'orderdata[4]' => 0, # 9x11 'orderdata[5]' => 1, # 10x13 'orderdata[11]' => 0, # 11x15 'orderdata[6]' => 0, # 13x17 ); $m->click('order[]');
Une fois le format validé, il reste à choisir le mode de livraison. Afin de ne pas nous embarquer dans la gestion de paiement en ligne (et pour nous éviter des frais supplémentaires), nous sélectionnons la « livraison en magasin ».
C'est le formulaire suivant qui nous le propose :
POST http://asp07.photoprintit.de/microsite/1156/cart.php [cartform] transfer_type=FC (radio) [FM/ Livraison à domicile (Payant)|*FC/ Livraison en magasin (Gratuit)] ok[]=<UNDEF> (image) startorder[]=<UNDEF> (image) couponid= (text) ok=<UNDEF> (image)
Nous maîtrisons complètement l'opération, désormais :
$m->form_number(2); $m->field( transfer_type => 'FC' ); # Livraison en magasin $m->click('startorder[]'); # bouton "Confirmer ma commande"
Nous sommes presque au bout de nos peines.
Pour pouvoir aller récuperer nos photos une fois développées, nous devons choisir dans quel magasin nous irons. Ce nouvel écran nous propose une recherche du magasin par code postal ou ville, ou la sélection directe par une liste déroulante.
POST https://asp07.photoprintit.de/microsite/confirm.php [payment] search_zip= (text) search_city= (text) search[]=<UNDEF> (image) loc_id= (hidden readonly) loc_id= (option) [*/SELECTIONNEZ VOTRE MAGASIN CI-DESSOUS|29733/PHOTO STATION, 18 Rue Du Mal Joffre, 01000 Bourg en Bresse|29734/PHOTO STATION, C.Cial Du Fayet, 02100 Saint-Quentin|... accept=<UNDEF> (checkbox) [*<UNDEF>/off|on/ J'accepte les conditions générales de vente. ] startorder[]=<UNDEF> (image)
Dans le cas de notre script, nous avons juste besoin de connaître l'identifiant numérique du magasin qui nous intéresse. On peut l'obtenir simplement à partir des données dans le formulaire.
# trouve le champ de HTML::Form qui contient la liste des magasins $m->form_number(2); my $input = $m->current_form()->find_input( 'loc_id', 'option' ); # utilisation d'une tranche de hash my %cities; @cities{ $input->possible_values } = $input->value_names; # affichage de la liste, par ordre des identifiants print "$_\t$cities{$_}\n" for sort keys %cities;
Il suffit de sauver la sortie du script dans un fichier. C'est ainsi que je trouve la boutique la plus proche de chez moi dans la liste des 288 magasins :
... 29620 PHOTO STATION, 25 Rue Des Boulangers, 68000 Colmar 29621 PHOTO STATION, 53 Rue Du Sauvage, 68100 Mulhouse 29622 PHOTO STATION, 47 Rue Victor Hugo, 69002 Lyon 29623 PHOTO STATION, 45 Rue De La Republique, 69002 Lyon 29624 PHOTO STATION, Centre Cial Part Dieu, 69003 Lyon ...
Dans le script final présenté en fin d'article, une option a été ajoutée pour aller uniquement chercher la liste des magasins sur le site, en envoyant une petite image et en coupant la transaction avant la confirmation finale.
Jusqu'à présent quand le script s'arrêtait, la commande n'était pas validée, et les photos restaient donc sur un coin de disque dur sur le serveur (en attendant qu'une tâche planifiée ne les efface).
Cette fois-ci, nous sommes identifiés, il y a des photos et si on valide,
il faudra aller les chercher chez le marchand. :-)
Avant de valider
pour nos tests finaux, nous allons donc choisir des photos que nous
voulons vraiment développer, et pas juste de petits fichier images pour
économiser la bande passante (et accélerer les tests). J'ai choisi pour
ce test une photo de la plus belle nièce du monde. ;-)
Arrivé à la page de confirmation, il faut cocher la case « J'accepte les conditions générales de vente. » et cliquer sur le bouton « Confirmer ma commande ».
La case à cocher correspond au champ HTML suivant :
<td><input type="checkbox" name="accept"></td>
La méthode de WWW::Mechanize
pour cocher les cases s'appelle tick()
et nécessite de connaître la valeur associée à chaque case à cocher.
Ici, comme on peut le voir, il n'y a qu'une case avec aucune valeur
associée. Heureusement, le dump du formulaire nous a permis de voir
quelle valeur HTML::Form
associe par défaut à une balise <input>
de type checkbox
sans paramètre value
:
accept=<UNDEF> (checkbox) [*<UNDEF>/off|on/| J'accepte les conditions générales de vente.| ]
Nous devrons donc cocher la case on
.
$m->field( loc_id => $magasin ); $m->tick('accept', 'on'); $m->click('startorder[]');
Avant de cocher automatiquement la case accept
, je vous engage au
moins à survoler les conditions générales de vente (disponibles à l'adresse
http://asp.photoprintit.de/microsite/1156/terms.php). Celles-ci
indiquent en effet que vous renoncez à votre droit de rétraction, pour
la simple raison que les produits seront déjà fabriqués (et livrés)
avant la fin du droit de rétractation. Autrement dit, si vous faites
des tests qui vont jusqu'à la confirmation finale, prévoyez comme moi
d'aller chercher les photos. :-)
Après validation, nous recevons une page qui contient le texte suivant :
<td align="right" colspan="3"> <span class="error">Veuillez sélectionner un magasin.</span><br><br> </td> </tr> <tr> <td><input type="checkbox" name="accept" checked></td>
Nous pouvons constater que nous avons bien coché la case concernant les conditions générales de vente. En revanche, la sélection du magasin ne s'est pas passée comme prévu.
Nous allons ajouter le code suivant pour détecter et afficher les erreurs, avant de recommencer nos essais :
if ( $m->content() =~ m!<span class="error">(.*?)</span>! ) { die "Erreur lors de la confirmation : $1"; }
Si on regarde plus attentivement le formulaire, on voit que le champ
loc_id
est en fait présent de deux façons dans le formulaire :
comme un champ texte caché et comme une liste. WWW::Mechanize
fournit la méthode select()
qui simplifie la gestion des champs de
type select
/option
. Nous allons donc essayer de sélectionner le
magasin avec :
$m->select( loc_id => $magasin );
Cette fois, c'est WWW::Mechanize
qui affiche un avertissement
Input "loc_id" is not type "select"
. Et notre script affiche
l'erreur Erreur lors de la confirmation : Veuillez sélectionner
un magasin.
.
Le problème est le suivant : nous avons deux champs qui
portent le même nom. HTML::Form
nous laisse remplir le premier champ
loc_id
(de type hidden
) et envoie alors comme corps de la requête
search_zip=&search_city=&loc_id=29729&loc_id=
. Le serveur à l'autre
bout doit probablement écraser la première valeur (29729
) avec la
seconde (vide). Lors de notre deuxième tentative (avec select()
), le
problème est cette fois que le premier champ nommé loc_id
trouvé par
WWW::Mechanize
n'est pas de type select
mais de type readonly
.
Pour envoyer une requête correcte au serveur nous devons donc mettre
à jour le second champ loc_id
. Nous allons d'abord devoir le
sélectionner avec la méthode find_input()
de HTML::Form
, puis
le remplir avec la méthode value()
(c'est un objet de type
HTML::Form::Input
) :
$m->current_form()->find_input( 'loc_id', 'option' )->value( $CONF{shop} );
Cette fois-ci tout se passe (enfin !) comme prévu, et nous recevons
non pas un 302
(que WWW::Mechanize
aurait suivi tout seul) mais
la page suivante (remise en forme pour des raisons de lisibilité) :
<html> <head> <meta http-equiv="refresh" content="0;http://asp07.photoprintit.de/microsite/summary.php" /> <script type="text/javascript">window.location.replace("http://asp07.photoprintit.de/microsite/summary.php")</script> </head> <body> <a href="http://asp07.photoprintit.de/microsite/summary.php">Klicken Sie hier, falls Ihr Browser Sie nicht automatisch weiterleitet</a> </body></html>
Babelfish traduit grossièrement le texte en allemand par « cliquetez ici, si votre Browser ne vous transmet pas automatiquement », ce que nous traduirons en Perl par :
$m->follow_link( url_regex => qr/summary/ );
Une fois le lien suivi, une page résumant la transaction est affichée. Les champs utiles à récupérer sont le numéro de commande et le numéro de client (vous connaissez normalement l'adresse du magasin !), que l'on peut récupérer tous les deux avec une seule expression régulière (à condition d'être bien sûr de l'ordre dans lequel ils apparaissent dans la page).
Le HTML en question ressemble à ceci (la page envoyée est en UTF-8) :
<p> N° de commande: 235484</p> <p> <table border="0" cellpadding="4" cellspacing="0"> <tr> <td> <h2>La livraison s'effectuera au magasin :</h2> </td> </tr> <tr> <td> <b>PHOTO STATION (Numéro de client: 813023)<br>47 Rue Victor Hugo<br>69002 Lyon<br></b> </td>
Et l'expression régulière qui nous capture toutes les informations d'un coup est la suivante (en évitant soigneusement les caractères non-ASCII) :
my ( $commande, $client ) = $m->content =~ /(?:commande:\ |client: )(\d+)/g;
Vous recevrez de toute façon un email de confirmation avec le numéro de commande et l'adresse du magasin (mais pas le numéro de client) dans votre boîte aux lettres.
Le script suivant est fourni avec de multiples options de ligne de commande qui n'ont pas été décrites dans l'article.
Notez en particulier l'option --list qui se contentera d'envoyer une image PNG de 89 octets (soyons gentils avec les disques durs du fournisseur) et d'aller jusqu'au formulaire de sélection du magasin pour nous afficher la liste complète. Sauvez cette liste dans un coin de votre ~ ou au moins le numéro du magasin le plus près de chez vous. Cette technique d'envoi direct d'une image montre qu'on n'a pas besoin de passer par des fichiers sur le disque pour envoyer des « fichiers » à un serveur web.
L'option --find accepte une expression régulière pour trouver une sous-liste de magasins correspondants dans la liste ainsi obtenue.
Note : Dans le code, on utilise qr/(?=)/
comme valeur pour $re
afin d'avoir une expression régulière qui correspond avec n'importe quelle
chaîne (qr/.*/
fonctionnerait tout aussi bien).
L'utilisation de qr//
pose en effet problème car il s'agit
une écriture particulière (tout comme //
) : quand l'expression
régulière est une chaîne vide, Perl utilise la dernière expression
régulière qui a établi une correspondance avec succès.
Si ni --list ni --find ne sont pas fournies, le script insistera pour voir l'option --shop suivie du numéro d'un magasin. Et ira jusqu'au bout de la commande.
Le hash %CONF
est fait pour que vous y mettiez vos valeurs par défaut
(en particulier pour email
, password
et shop
).
#!/usr/bin/perl use strict; use warnings; use Getopt::Long; use WWW::Mechanize; use IO::Handle; # pas de tampon sur STDOUT STDOUT->autoflush(1); # options par défaut my %CONF = ( email => '', # À remplacer par vos identifiant, password => '', # mot de passe shop => '', # et magasin préféré find => '', list => 0, ); # message d'information my $USAGE = << 'USAGE'; Options disponibles: --email <email> --password <password> --shop <numéro magasin> --find <regexp> --list USAGE GetOptions( \%CONF, "email=s", "password=s", "list!", "shop=i", "find=s", "help!" ) or die $USAGE; # divers cas d'erreur $CONF{$_} or die "$_ requis\n$USAGE" for qw( email password ); die "Au moins un paramètre --shop ou --find ou --list requis\n$USAGE" unless $CONF{shop} || $CONF{find} || $CONF{list}; die "Au moins une image requise\n$USAGE" if $CONF{shop} && @ARGV == 0; die $USAGE if $CONF{help}; # connexion à la page principale my $m = WWW::Mechanize->new; $m->get('http://www.photostation.fr/index.php'); # client HTML $m->follow_link( url_regex => qr/htmlclient/ ); # formulaire d'identification $m->set_fields( external_login_user => $CONF{email}, external_login_pwd => $CONF{password}, ); $m->click('external_login[]'); die "Échec de l'identification\n" if $m->content !~ /\Q$CONF{email}\E/; # les noms des photos sont dans @ARGV if ( $CONF{shop} ) { if ( @ARGV > 10 ) { $m->form_number(2); bless $m->current_form()->find_input('quantity'), 'HTML::Form::TextInput'; $m->field( quantity => scalar @ARGV ); $m->click; } $m->form_number(3); $m->set_fields( "file$_" => $ARGV[$_] ) for 0 .. @ARGV - 1; my $req; $|++; { no warnings; local $HTTP::Request::Common::DYNAMIC_FILE_UPLOAD = 1; $req = $m->current_form()->make_request; my $content_sub = $req->content(); my $done = 0; my $total = $req->content_length; $req->content( sub { my $data = $content_sub->(); return unless defined $data; $done += length $data; printf "\rTransfert: %2d%% (%d/%d)", $done / $total * 100, $done, $total; return $data; } ); } # envoi des photos $m->request($req); print "\n"; } else { # envoi d'une image bidon (blanc 10x10) $m->form_number(3); my $input = $m->current_form()->find_input('file0'); $input->content( pack( 'H*', '89504e470d0a1a0a0000000d494844520000000a0000000a0103000000b7' . 'fc5dfe00000006504c5445000000ffffffa5d99fdd0000000e4944415478' . 'da63f87f8001370200075211774b8b03d80000000049454e44ae426082' ) ); $input->filename('blank10x10.png'); $input->headers( content_type => 'image/png' ); $m->click; } die "Échec de l'envoi des images : " . $m->res()->status_line() unless $m->success(); # taille par défaut # force le format 10x13 $m->form_number(2); $m->set_fields( 'orderdata[4]' => 0, # 9x11 'orderdata[5]' => 1, # 10x13 'orderdata[11]' => 0, # 11x15 'orderdata[6]' => 0, # 13x17 ); $m->click('order[]'); die "Échec de validation du format : " . $m->res()->status_line() unless $m->success(); # choix de la livraison en magasin $m->form_number(2); $m->field( transfer_type => 'FC' ); $m->click('startorder[]'); die "Échec de validation de la livraison : " . $m->res()->status_line() unless $m->success(); # liste des magasins $m->form_number(2); if ( $CONF{shop} ) { # validation finale $m->current_form()->find_input( 'loc_id', 'option' )->value( $CONF{shop} ); $m->tick( 'accept', 'on' ); # accepte les conditions générales de vente $m->click('startorder[]'); # validation die "Échec de la confirmation finale : " . $m->res()->status_line() unless $m->success(); # détection d'éventuelles erreurs if( $m->content() =~ m!<span class="error">(.*?)</span>! ) { die "Erreur lors de la confirmation : $1"; } # suit la redirection $m->follow_link( url_regex => qr/summary/ ); die "Échec de récupération du bilan : " . $m->res()->status_line() unless $m->success(); # affichage des informations de commande print $m->content(); my ( $commande, $client ) = $m->content() =~ /(?:commande:\ |client: )(\d+)/g; print "Numéro de client: $client\n", "Numéro de commande: $commande\n"; } else { my %cities; my $input = $m->current_form()->find_input( 'loc_id', 'option' ); @cities{ $input->possible_values() } = $input->value_names(); # recherche et affichage des éléments qui correspondent my $re = $CONF{find} ? qr/$CONF{find}/i : qr/(?=)/; print map { "$_\t$cities{$_}\n" } grep { $cities{$_} =~ $re } keys %cities; }
Nous avons vu que le site web d'envoi des photos est en réalité
géré par photoprintit.de
. Des URL que nous avons manipulées,
il n'est pas difficile de déduire que l'identifiant de Photo Station
est le 1156
.
L'uniligne suivant va nous permettre de décrouvrir la liste des identifiants
valides pour les autres clients de photoprintit.de
:
$ perl -MLWP::Simple -le '(get("http://asp.photoprintit.de/microsite/$_/htmlclient.php") ne "Unknown customer id" ) && print "ok $_" for 1 .. 8000' ok 1 ok 5 ok 8 ok 10 ok 13 ...
Ce script s'appuie sur le fait que la page renvoie Unknown customer id
si on essaye de passer un identifiant invalide dans l'URL.
Cette recherche sur les 8000 premiers identifiants en a renvoyé 434 valides. Autrement dit, si nous arrivons à faire un module générique, il pourra servir pour chacun des 434 magasins de photo en ligne (la majeure partie d'entre eux sont des sites en allemand ou en hollandais).
Le code que nous avons produit est déjà très générique : nulle part nous n'avons utilisé autre chose que les noms des champs ou les URL pour nous déplacer de page en page.
Philippe "BooK" Bruhat, membre de Paris.pm & Lyon.pm, est l'auteur de
HTTP::Proxy
et le co-traducteur avec Jean Forget de Perl Best Practices,
à paraître chez O'Reilly France.
Merci à David Morel (de Lyon.pm) de m'avoir fourni son script de connexion à eBay, à partir duquel j'ai développé ma propre version décrite dans cet article.
Merci à Michel Grafmeyer et aux mongueurs de Perl pour leur travail de relecture.
Comme pour l'article précédent, les deux scripts (eBay et PhotoStation) sont fournis sur le CD. Ils sont également en ligne aux adresses http://articles.mongueurs.net/magazines/webrobot-02/ebay-zero.pl et http://articles.mongueurs.net/magazines/webrobot-02/photostation.pl.
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.