1: <?php
2: // declare(encoding='UTF-8');
3: /**
4: * Classe principale gérant les services web de type (@link(REST, http://fr.wikipedia.org/wiki/Rest).
5: *
6: * Elle contient :
7: * - les constantes indiquant les différentes (@link(méthode HTTP, http://fr.wikipedia.org/wiki/Http) prises en compte.
8: * - les @link(codes HTTP des réponses, http://fr.wikipedia.org/wiki/Liste_des_codes_HTTP)
9: *
10: * Ce serveur REST accepte 4 types de méthodes HTTP : GET, PUT, POST, DELETE.
11: * GET et POST ne pose généralement pas de problème pour les clients HTTP mais ce n'est pas forcément le cas pour PUT et DELETE.
12: * Vous pouvez donc pour réaliser :
13: * - DELETE : utiliser la méthode POST avec action=DELETE dans le corps de la requête.
14: * - PUT : utiliser la méthode POST avec une url ne contenant aucune indication de ressource.
15: * Une autre solution consiste à utiliser n'importe quelle méthode et à ajouter l'entête "X_HTTP_METHOD_OVERRIDE" avec pour
16: * valeur le nom de la méthode que vous souhaitez utiliser. Exemple d'entête : "X_HTTP_METHOD_OVERRIDE: PUT".
17: * Exemple : <code>curl -v -v -H "X_HTTP_METHOD_OVERRIDE: DELETE" "http://www.mondomaine.org/services/apiVersion/[mon-service]/"</code>
18: * Cela fonctionne avec Apache.
19: *
20: * Les classes des services web doivent avoir un nom au format ChatMot "MonService" et être appelée dans l'url par le même nom
21: * en minuscule où les mots sont séparés par des tirets "mon-service".
22: *
23: * Paramètres liés dans config.ini :
24: * - serveur.baseURL : morceau de l'url pour appeler le serveur relative au domaine. Exemple : pour http://www.tela-botanica.org/mon_serveur/
25: * mettre : "/mon_serveur/"
26: * - serveur.baseAlternativeURL : sur le même principe que ci-dessus permet d'affecter une deuxième url (pour gérer des raccourci via htaccess)
27: *
28: * Encodage en entrée : utf8
29: * Encodage en sortie : utf8
30: *
31: * @category Php 5.2
32: * @package Framework
33: * @author Jean-Pascal MILCENT <jpm@tela-botanica.org>
34: * @copyright Copyright (c) 2010, Tela Botanica (accueil@tela-botanica.org)
35: * @license GPL v3 <http://www.gnu.org/licenses/gpl.txt>
36: * @license CECILL v2 <http://www.cecill.info/licences/Licence_CeCILL_V2-en.txt>
37: * @since 0.3
38: * @version $Id: RestServeur.php 457 2014-02-06 17:54:21Z jpm $
39: * @link /doc/framework/
40: */
41: // TODO : gerer les retours : dans ce controleur : code retour et envoi ...
42: class RestServeur {
43:
44: /** Nom de la méthode appelée dans un service pour éxécuter une requête de type GET. */
45: const METHODE_GET = 'consulter';
46:
47: /** Nom de la méthode appelée dans un service pour éxécuter une requête de type POST. */
48: const METHODE_POST = 'modifier';
49:
50: /** Nom de la méthode appelée dans un service pour éxécuter une requête de type DELETE. */
51: const METHODE_DELETE = 'supprimer';
52:
53: /** Nom de la méthode appelée dans un service pour éxécuter une requête de type PUT. */
54: const METHODE_PUT = 'ajouter';
55:
56: /** Code HTTP 200 : OK
57: * Indiquant le succès de l'accès à un service web par la méthode GET.
58: * L'utiliser lors d'une requète de type GET (consulter) pour indiquer le succès de l'opération.
59: * Sera renvoyée par défaut par PHP.
60: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.1
61: */
62: const HTTP_CODE_OK = '200';
63:
64: /** Code HTTP 201 : Created
65: * Indiquant que l'accès à un service web est un succès et que la ressource a été créée ou modifié.
66: * L'utiliser lors d'une requète de type PUT (ajouter) ou POST (modifier) pour indiquer le succès de l'opération.
67: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.2
68: */
69: const HTTP_CODE_CREATION_OK = '201';
70:
71: /** Code HTTP 202 : Accepted
72: * Indiquant que la requête a été acceptée mais que son traitement n'est pas encore terminé.
73: * L'utiliser lors d'une requète de type PUT (ajouter) ou POST (modifier) pour indiquer que les données
74: * seront traitées ultérieurement.
75: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.3
76: */
77: const HTTP_CODE_ACCEPTE_OK = '202';
78:
79: /** Code HTTP 204 : No Content
80: * Indique que l'accès à un service web est un succès et qu'il n'y a pas de contenu à renvoyer.
81: * L'utiliser lors d'une requète de type DELETE (supprimer) pour indiquer le succès de l'opération si aucun
82: * contenu (status) ne doit être renvoyé, sinon utiliser 200 OK.
83: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5
84: */
85: const HTTP_CODE_SUPPRESSION_OK = '204';
86:
87: /** Code HTTP 400 : Bad Request
88: * Indique que les paramètres envoyés au service contiennent des erreurs.
89: * L'utiliser pour indiquer l'échec de l'accès au service. La réponse pourra contenir un message expliquant la source
90: * de l'erreur.
91: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1
92: */
93: const HTTP_CODE_MAUVAISE_REQUETE = '400';
94:
95: /** Code HTTP 401 : Unauthorized
96: * Indiquant que l'accès à un service web est refusé car l'authentification (obligatoire) a échoué pour
97: * accéder à la ressource.
98: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
99: */
100: const HTTP_CODE_ACCES_NON_AUTORISE = '401';
101:
102: /** Code HTTP 403 : Forbidden
103: * Indiquant que le service a compris la requête mais refuse de la réaliser.
104: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
105: */
106: const HTTP_CODE_ACCES_INTERDIT = '403';
107:
108: /** Code HTTP 404 : Not Found
109: * Indiquant que la ressource indiquée par l'url est introuvable.
110: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5
111: */
112: const HTTP_CODE_RESSOURCE_INTROUVABLE = '404';
113:
114: /** Code HTTP 405 : Method Not Allowed
115: * Indique soit :
116: * - que le service web ne possède pas d'accès la ressource correspondant à la méthode HTTP employée.
117: * - que la méthode HTTP enployée n'est pas en accord avec la ressource indiquée par l'url.
118: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.6
119: */
120: const HTTP_CODE_METHODE_NON_AUTORISE = '405';
121:
122: /** Code HTTP 409 : Conflict
123: * Indique qu'un conflit est survenu vis à vis de la ressource.
124: * Par exemple, essayer de créer deux fois la même ressource ou bien tenter de modifier une ressource qui a été modifiée par
125: * ailleurs.
126: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.10
127: */
128: const HTTP_CODE_CONFLIT = '409';
129:
130: /** Code HTTP 411 : Length Required
131: * Indique que des paramètres passés dans le contenu de la requête sont nécessaires au service.
132: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.12
133: */
134: const HTTP_CODE_CONTENU_REQUIS = '411';
135:
136: /** Code HTTP 412 : Precondition Failed
137: * Indique que des entêtes envoyés au serveur sont erronnés.
138: * À utiliser quand un service web reçoit de mauvais entêtes, paramètres et/ou ressources.
139: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.13
140: */
141: const HTTP_CODE_ECHEC_CONDITION = '411';
142:
143: /** Code HTTP 415 : Unsupported Media Type
144: * Indique que le type de média envoyé au serveur n'est pas accepté par la ressource.
145: * À utiliser quand un service web reçoit un mauvais type de contenu.
146: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.16
147: */
148: const HTTP_CODE_CONTENU_NON_SUPPORTE = '415';
149:
150: /** Code HTTP 500 : Internal Server Error.
151: * L'utiliser quand le serveur ou un service soulève une erreur ou une exception.
152: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1
153: */
154: const HTTP_CODE_ERREUR = '500';
155:
156: /** Code HTTP 503 : Service Unavailable
157: * L'utiliser quand le web service est momentanément indisponible.
158: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4
159: */
160: const HTTP_CODE_SERVICE_INDISPONIBLE = '503';
161:
162: /** Code HTTP 501 : Not Implemented
163: * Indique que le web service ne supporte pas la fonctionnalité demandée.
164: * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.2
165: */
166: const HTTP_NON_IMPLEMENTE= '501';
167:
168: /** Motif de l'epression régulière vérfiant la version de l'API. */
169: const MOTIF_API_VERSION = '/^[0-9]+(?:[.][0-9]+)*$/';
170:
171: /** Motif de l'epression régulière vérfiant le nom du service. */
172: const MOTIF_SERVICE_NOM = '/^[a-z0-9]+(?:[-][a-z0-9]+)*$/';
173:
174: /** Mettre à true pour activer l'affichage des messages d'erreurs et de débogage.
175: * @var boolean */
176: private static $debogageActivation = false;
177:
178: /** Indiquer le mode de débogage à utiliser (@see Debug).
179: * @var string */
180: private static $debogageMode = '';
181:
182: /** La méthode de la requête HTTP utilisée.
183: * @var string */
184: private $methode = 'GET';
185:
186: /** Le contenu brut du corps de la requête HTTP (s'il y en a).
187: * @var array */
188: private $requeteDonnees = null;
189:
190: /** Le contenu sous forme de tableau de paires clés-valeurs du corps de la requête HTTP (s'il y en a).
191: * @var array */
192: private $requeteDonneesParsees = null;
193:
194: /** Version de l'API demandée.
195: * Ex. http://www.mondomaine.org/services/[apiVersion]/mon-service/
196: * @var mixed Généralement deux nombres séparés par un point. Ex. : 1.0
197: */
198: private $apiVersion = null;
199:
200: /** Nom du service demandé.
201: * Ex. http://www.mondomaine.org/services/apiVersion/[mon-service]/
202: * @var string par défaut vaut null.
203: */
204: private $service = null;
205:
206: /** Morceaux de l'url servant à préciser la ressource concerné pour le service demandé.
207: * Ex. http://www.mondomaine.org/services/apiVersion/mon-service/[maRessource/maSousResource...]
208: * @var array
209: */
210: private $ressources = array();
211:
212: /** Partie de l'url situé après le '?' servant à paramétrer le service demandé.
213: * Les données proviennent de $_GET où les caractères suivant ont été transformé en '_' undescrore dans les clés :
214: * - chr(32) ( ) (space)
215: * - chr(46) (.) (dot)
216: * - chr(91) ([) (open square bracket)
217: * - chr(128) - chr(159) (various)
218: * En outre nous appliquons la méthode nettoyerGet() qui effectue d'autres remplacement dans les valeurs.
219: * Ex. http://www.mondomaine.org/services/apiVersion/mon-service?monParametre1=maValeur1&monParametre2=maValeur2
220: * @see parametresBruts
221: * @var array
222: */
223: private $parametres = array();
224:
225: /** Partie de l'url situé après le '?' servant à paramétrer le service demandé.
226: * Les données proviennent de $_SERVER['QUERY_STRING'] et n'ont subies aucune transformation au niveau des clés.
227: * Cependant nous appliquons la méthode nettoyerGet() qui effectue d'autres remplacement dans les valeurs.
228: * Ex. http://www.mondomaine.org/services/apiVersion/mon-service?monParametre1=maValeur1&monParametre2=maValeur2
229: * @see parametres
230: * @var array
231: */
232: private $parametresBruts = array();
233:
234: /** Tableau contenant les paramètres de configuration du serveur.
235: * @var array
236: */
237: private static $config = array();
238:
239: /** Tableau contenant les messages d'erreur et/ou d'avertissement du Serveur.
240: * @var array
241: * */
242: private static $messages = array();
243:
244: /** Codes HTTP. */
245: private static $http10 = array(
246: self::HTTP_CODE_OK => 'OK',
247: self::HTTP_CODE_CREATION_OK => 'Created',
248: self::HTTP_CODE_SUPPRESSION_OK => 'No Content',
249: self::HTTP_CODE_MAUVAISE_REQUETE => 'Bad Request',
250: self::HTTP_CODE_ACCES_NON_AUTORISE => 'Unauthorized',
251: self::HTTP_CODE_RESSOURCE_INTROUVABLE => 'Not Found',
252: self::HTTP_CODE_METHODE_NON_AUTORISE => 'Method Not Allowed',
253: self::HTTP_CODE_CONFLIT => 'Conflict',
254: self::HTTP_CODE_CONTENU_REQUIS => 'Length Required',
255: self::HTTP_CODE_ERREUR => 'Internal Server Error'
256: );
257:
258: /** Tableau des noms des paramètres à définir dans le fichier de config car obligatoirement nécessaire à cette classe.*/
259: private $parametres_obligatoires = array('debogage', 'debogage_mode', 'serveur.baseURL', 'chemin_modules');
260:
261: /**
262: * Analyse les données envoyées au serveur, enregistre la méthode HTTP utilisée pour appeler le serveur et parse
263: * l'url appelée pour trouver le service demandé.
264: */
265: public function __construct() {
266: Config::verifierPresenceParametres($this->parametres_obligatoires);
267:
268: self::$debogageActivation = Config::get('debogage');
269: self::$debogageMode = Config::get('debogage_mode');
270:
271: if (isset($_SERVER['REQUEST_URI']) && isset($_SERVER['REQUEST_METHOD']) && isset($_SERVER['QUERY_STRING'])) {
272: $this->initialiserMethode();
273: $this->initialiserRequeteDonnees();
274:
275: $urlParts = $this->decouperUrlChemin();
276:
277: $this->initialiserApiVersion(array_shift($urlParts));
278: $this->initialiserServiceNom(array_shift($urlParts));
279: $this->initialiserRessource($urlParts);
280:
281: $this->initialiserParametres();
282: // Enregistrement en première position des autoload de la méthode gérant les classes des services
283: spl_autoload_register(array(get_class(), 'chargerClasse'));
284: } else {
285: self::envoyerEnteteStatutHttp(self::HTTP_CODE_ERREUR);
286: $e = "La classe Serveur du TBFRamework nécessite, pour fonctionner, l'accès aux variables serveurs REQUEST_URI, REQUEST_METHOD et QUERY_STRING.";
287: self::ajouterMessage($e);
288: }
289: }
290:
291: private function initialiserMethode() {
292: if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']) && count(trim($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) > 0) {
293: $this->methode = trim($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
294: } else {
295: $this->methode = $_SERVER['REQUEST_METHOD'];
296: }
297: }
298:
299: private function initialiserRequeteDonnees() {
300: if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['CONTENT_LENGTH'] > 0) {
301: $this->requeteDonnees = '';
302: $httpContent = fopen('php://input', 'r');
303: while ($data = fread($httpContent, 1024)) {
304: $this->requeteDonnees .= $data;
305: }
306: fclose($httpContent);
307: }
308: }
309:
310: private function decouperUrlChemin() {
311: if (isset($_SERVER['REDIRECT_URL']) && $_SERVER['REDIRECT_URL'] != '') {
312: if (isset($_SERVER['REDIRECT_QUERY_STRING']) && !empty($_SERVER['REDIRECT_QUERY_STRING'])) {
313: $url = $_SERVER['REDIRECT_URL'].'?'.$_SERVER['REDIRECT_QUERY_STRING'];
314: } else {
315: $url = $_SERVER['REDIRECT_URL'];
316: }
317: } else {
318: $url = $_SERVER['REQUEST_URI'];
319: }
320:
321: if (strlen($_SERVER['QUERY_STRING']) == 0) {
322: $tailleURL = strlen($url);
323: } else {
324: $tailleURL = -(strlen($_SERVER['QUERY_STRING']) + 1);
325: }
326:
327: $urlChaine = '';
328: if (strpos($url, Config::get('serveur.baseURL')) !== false) {
329: $urlChaine = substr($url, strlen(Config::get('serveur.baseURL')), $tailleURL);
330: } else if (strpos($url, Config::get('serveur.baseAlternativeURL')) !== false) {
331: $urlChaine = substr($url, strlen(Config::get('serveur.baseAlternativeURL')), $tailleURL);
332: }
333: return explode('/', $urlChaine);
334: }
335:
336: private function initialiserApiVersion($apiVersion) {
337: if ($this->verifierApiVersion($apiVersion)) {
338: $this->apiVersion = $apiVersion;
339: self::$config['chemins']['api'] = Config::get('chemin_modules').$this->apiVersion.DS;
340: self::$config['chemins']['api_bibliotheque'] = self::$config['chemins']['api'].Config::get('dossier_bibliotheque').DS;
341: } else {
342: self::envoyerEnteteStatutHttp(self::HTTP_CODE_MAUVAISE_REQUETE);
343: $e = "Aucune version d'API n'a été spécifiée.\n".
344: "La version doit respecter l'expression régulière suivante : ".self::MOTIF_API_VERSION.".\n".
345: "L'url doit avoir la forme suivante : http://www.mondomaine.org/services/apiVersion/monService/";
346: self::ajouterMessage($e);
347: self::cloreAccesServeur();
348: }
349: }
350:
351: private function verifierApiVersion($apiVersion) {
352: $apiOk = false;
353: if (isset($apiVersion) && !empty($apiVersion) && preg_match(self::MOTIF_API_VERSION, $apiVersion)) {
354: $apiOk = true;
355: }
356: return $apiOk;
357: }
358:
359: private function initialiserServiceNom($serviceNom) {
360: if ($this->verifierServiceNom($serviceNom)) {
361: $this->service = $this->traiterNomService($serviceNom);
362: } else {
363: self::envoyerEnteteStatutHttp(self::HTTP_CODE_MAUVAISE_REQUETE);
364: $e = "Aucune nom de service n'a été spécifié.\n".
365: "La nom du service doit respecter l'expression régulière suivante : ".self::MOTIF_SERVICE_NOM.".\n".
366: "L'url doit avoir la forme suivante : http://www.mondomaine.org/services/apiVersion/monService/";
367: self::ajouterMessage($e);
368: self::cloreAccesServeur();
369: }
370: }
371:
372: private function verifierServiceNom($serviceNom) {
373: $serviceNomOk = false;
374: if (isset($serviceNom) && !empty($serviceNom) && preg_match(self::MOTIF_SERVICE_NOM, $serviceNom)) {
375: $serviceNomOk = true;
376: }
377: return $serviceNomOk;
378: }
379:
380: private function traiterNomService($serviceNom) {
381: return str_replace(' ', '', ucwords(str_replace('-', ' ', strtolower($serviceNom))));
382: }
383:
384: private function initialiserRessource($urlParts) {
385: if (is_array($urlParts) && count($urlParts) > 0) {
386: foreach ($urlParts as $ressource) {
387: // Ne pas utiliser empty() car valeur 0 acceptée
388: if ($ressource != '') {
389: $this->ressources[] = urldecode($ressource);
390: }
391: }
392: }
393: }
394:
395: private function initialiserParametres() {
396: $this->parametres = $this->recupererParametresGet();
397: $this->parametresBruts = $this->recupererParametresBruts();
398: }
399:
400: private function recupererParametresGet() {
401: $_GET = $this->nettoyerParametres($_GET);
402: return $_GET;
403: }
404:
405: private function nettoyerParametres(Array $parametres) {
406: // Pas besoin d'utiliser urldecode car déjà fait par php pour les clés et valeur de $_GET
407: if (isset($parametres) && count($parametres) > 0) {
408: foreach ($parametres as $cle => $valeur) {
409: // les quotes, guillements et points-virgules ont été retirés des caractères à vérifier car
410: //ça n'a plus lieu d'être maintenant que l'on utilise protéger à peu près partout
411: $verifier = array('NULL', "\\", "\x00", "\x1a");
412: $parametres[$cle] = strip_tags(str_replace($verifier, '', $valeur));
413: }
414: }
415: return $parametres;
416: }
417:
418: private function recupererParametresBruts() {
419: $parametres_bruts = array();
420: if (!empty($_SERVER['QUERY_STRING'])) {
421: $paires = explode('&', $_SERVER['QUERY_STRING']);
422: foreach ($paires as $paire) {
423: $nv = explode('=', $paire);
424: $nom = urldecode($nv[0]);
425: $valeur = urldecode($nv[1]);
426: $parametres_bruts[$nom] = $valeur;
427: }
428: $parametres_bruts = $this->nettoyerParametres($parametres_bruts);
429: }
430: return $parametres_bruts;
431: }
432:
433: /**
434: * La méthode __autoload() charge dynamiquement les classes trouvées dans le code.
435: * Cette fonction est appelée par php5 quand il trouve une instanciation de classe dans le code.
436: *
437: *@param string le nom de la classe appelée.
438: *@return void le fichier contenant la classe doit être inclu par la fonction.
439: */
440: public static function chargerClasse($classe) {
441: if (class_exists($classe)) {
442: return null;
443: }
444: $chemins = array('', self::$config['chemins']['api'], self::$config['chemins']['api_bibliotheque']);
445: foreach ($chemins as $chemin) {
446: $chemin = $chemin.$classe.'.php';
447: if (file_exists($chemin)) {
448: require_once $chemin;
449: }
450: }
451: }
452:
453: /**
454: * Execute la requête.
455: */
456: public function executer() {
457: $retour = '';
458: switch ($this->methode) {
459: case 'GET':
460: $retour = $this->get();
461: break;
462: case 'POST':
463: $retour = $this->post();// Retour pour l'alternative PUT
464: break;
465: case 'DELETE':
466: $retour = $this->delete();
467: break;
468: case 'PUT':
469: $retour = $this->put();
470: break;
471: // Autorise l'accès depuis des serveurs tiers
472: /*
473: case 'OPTIONS':
474: header('Access-Control-Allow-Origin: *');
475: header('Access-Control-Allow-Headers:origin, content-type');
476: header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
477: header('Access-Control-Allow-Content-Type: application/json');
478: header('Access-Control-Max-Age: 3628800');
479: header('Access-Control-Allow-Credentials: false');
480: break;
481: */
482: default :
483: self::envoyerEnteteStatutHttp(self::HTTP_CODE_METHODE_NON_AUTORISE);
484: header('Allow: GET, POST, DELETE, PUT');
485: $e = "La méthode HTTP '{$this->methode}' n'est pas prise en compte par ce serveur REST.\n".
486: "Consulter l'entête Allow pour connaître les méthodes autorisées.";
487: self::ajouterMessage($e);
488: }
489: $this->cloreAccesServeur($retour);
490: }
491:
492: /**
493: * Execute a GET request. A GET request fetches a list of resource when no resource name is given, a list of element
494: * when a resource name is given, or a resource element when a resource and resource unique identifier are given. It does not change the
495: * database contents.
496: */
497: private function get() {
498: $retour = '';
499: if ($this->service != null) {
500: $Service = new $this->service(self::$config);
501: if (method_exists($Service, self::METHODE_GET)) {
502: $methodeGet = self::METHODE_GET;
503: $parametres = $Service->utiliserParametresBruts() ? $this->parametresBruts : $this->parametres;
504: $retour = $Service->$methodeGet($this->ressources, $parametres);
505: } else {
506: self::envoyerEnteteStatutHttp(self::HTTP_CODE_RESSOURCE_INTROUVABLE);
507: $e = "Le service '{$this->service}' ne contient pas la méthode '".self::METHODE_GET."' nécessaire ".
508: "lors de l'appel du service via la méthode HTTP GET.";
509: self::ajouterMessage($e);
510: }
511: }
512: return $retour;
513: }
514:
515: private function post() {
516: $retour = '';
517: $paires = $this->parserDonneesRequete();
518: if (count($paires) != 0) {
519: if (isset($paires['action']) && $paires['action'] == 'DELETE') {// Alternative à l'utilisation de DELETE
520: $retour = $this->delete();
521: } else if (count($this->ressources) == 0) {// Alternative à l'utilisation de PUT
522: $retour = $this->put();
523: } else {
524: if ($this->service != null) {
525: $Service = new $this->service(self::$config);
526: if (method_exists($Service, self::METHODE_POST)) {
527: $methodePost = self::METHODE_POST;
528: $info = $Service->$methodePost($this->ressources, $paires);
529: if ($info !== false) {
530: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_CREATION_OK);
531: $retour = $info;
532: }
533: } else {
534: self::envoyerEnteteStatutHttp(self::HTTP_CODE_RESSOURCE_INTROUVABLE);
535: $e = "Le service '{$this->service}' ne contient pas la méthode '".self::METHODE_POST."' nécessaire ".
536: "lors de l'appel du service via la méthode HTTP GET.";
537: self::ajouterMessage($e);
538: }
539: }
540: }
541: } else {
542: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_CONTENU_REQUIS);
543: $e = "Le service '{$this->service}' requiert de fournir le contenu à modifier dans le corps ".
544: "de la requête avec la méthode HTTP POST.";
545: self::ajouterMessage($e);
546: }
547: return $retour;
548: }
549:
550: private function put() {
551: $retour = '';
552: $paires = $this->parserDonneesRequete();
553: if (count($paires) != 0) {
554: if ($this->service != null) {
555: $Service = new $this->service(self::$config);
556: if (method_exists($Service, self::METHODE_PUT)) {
557: $methodePut = self::METHODE_PUT;
558: $info = $Service->$methodePut($this->ressources, $paires);
559: if ($info !== false) {
560: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_CREATION_OK);
561: $retour = $info;
562: }
563: } else {
564: self::envoyerEnteteStatutHttp(self::HTTP_CODE_RESSOURCE_INTROUVABLE);
565: $e = "Le service '{$this->service}' ne contient pas la méthode '".self::METHODE_PUT."' nécessaire ".
566: "lors de l'appel du service via la méthode HTTP GET.";
567: self::ajouterMessage($e);
568: }
569: }
570: } else {
571: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_CONTENU_REQUIS);
572: $e = "Il est nécessaire de fournir du contenu dans le corps de la requête pour créer une nouvelle ressource.";
573: self::ajouterMessage($e);
574: }
575: return $retour;
576: }
577:
578: private function delete() {
579: $retour = '';
580: if (count($this->ressources) != 0) {
581: $paires = $this->parserDonneesRequete();
582: if ($this->service != null) {
583: $Service = new $this->service(self::$config);
584: if (method_exists($Service, self::METHODE_DELETE)) {
585: $methodeDelete = self::METHODE_DELETE;
586: $info = $Service->$methodeDelete($this->ressources, $paires);
587: if ($info === true) {
588: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_SUPPRESSION_OK);
589: } else if ($info === false) {
590: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_RESSOURCE_INTROUVABLE);
591: $e = "La ressource à supprimer est introuvable. Il se peut qu'elle ait été préalablement supprimé.";
592: self::ajouterMessage($e);
593: } else {
594: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_OK);
595: $retour = $info;
596: }
597: } else {
598: self::envoyerEnteteStatutHttp(self::HTTP_CODE_RESSOURCE_INTROUVABLE);
599: $e = "Le service '{$this->service}' ne contient pas la méthode '".self::METHODE_DELETE."' nécessaire ".
600: "lors de l'appel du service via la méthode HTTP GET.";
601: self::ajouterMessage($e);
602: }
603: }
604: } else {
605: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_MAUVAISE_REQUETE);
606: $e = "Il est nécessaire d'indiquer dans l'url la ressource à supprimer.";
607: self::ajouterMessage($e);
608: }
609: return $retour;
610: }
611:
612: /**
613: * Parse les données contenu dans le corps de la requête HTTP (= POST) en :
614: * - décodant les clés et valeurs.
615: * - supprimant les espaces en début et fin des clés et des valeurs.
616: *
617: * @return array Tableau de paires clé et valeur.
618: */
619: private function parserDonneesRequete() {
620: $donnees = array();
621: if ($this->requeteDonneesParsees != null) {
622: $donnees = $this->requeteDonneesParsees;
623: } else if ($this->requeteDonnees != null) {
624: if (preg_match('/application\/json/', $_SERVER['CONTENT_TYPE'])) {
625: $donnees = json_decode($this->requeteDonnees, true);
626: } else {
627: $paires = explode('&', $this->requeteDonnees);
628: foreach ($paires as $paire) {
629: list($cle, $valeur) = explode('=', $paire);
630: $cle = (isset($cle)) ? trim(urldecode($cle)) : '';
631: $valeur = (isset($valeur)) ? trim(urldecode($valeur)) : '';
632: $donnees[$cle] = $valeur;
633: }
634: }
635: $this->requeteDonneesParsees = $donnees;
636: }
637: return $donnees;
638: }
639:
640: /**
641: * Envoyer un entête HTTP (version 1.0) de statut.
642: * Il remplacera systématiquement tout entête HTTP de statut précédement envoyé.
643: * @param int $code entier indiquant le code du statut de l'entête HTTP à envoyer.
644: */
645: public static function envoyerEnteteStatutHttp($code) {
646: if (isset(self::$http10[$code])) {
647: $txt = self::$http10[$code];
648: header("HTTP/1.0 $code $txt", true);
649: }
650: }
651:
652: /**
653: * Termine l'accès au serveur après envoir envoyer les messages.
654: */
655: private static function cloreAccesServeur($retour = '') {
656: // Gestion des exceptions et erreurs générées par les services
657: $retour .= self::gererErreurs();
658:
659: // Envoie des messages d'erreur et d'avertissement du serveur
660: $retour .= self::envoyerMessages();
661:
662: // Autorise l'accès depuis des serveurs tiers
663: //header('Access-Control-Allow-Origin: *');
664:
665: // Envoie sur la sortie standard le contenu de la réponse HTTP
666: print $retour;
667:
668: // Nous terminons le script
669: exit(0);
670: }
671:
672: /**
673: * Si des exceptions ou des erreurs sont soulevées par le serveur ou les services, elles sont gérées par cette méthode.
674: * Si nous avec des erreurs d'un type différent d'E_USER_NOTICE (réservé au débogage), elle sont renvoyées sur la sortie
675: * standard (via echo).
676: * Si seulement des erreurs de type E_USER_NOTICE, sont présentes, elle sont envoyées en fonction du contenu du paramètre de
677: * config "debogage_mode" :
678: * - Debug::MODE_ECHO : les messages sont affichés en utilisant echo au moment où ils sont déclenchés dans le code.
679: * - Debug::MODE_NOTICE : les message sont stockés par le gestionnaire d'exception sous forme d'erreur de type
680: * E_USER_NOTICE et sont renvoyés sur la sortie standard à la fin de l'execution du programme (via echo).
681: * - Debug::MODE_ENTETE_HTTP : les message sont stockés par le gestionnaire d'exception sous forme d'erreur de type
682: * E_USER_NOTICE et sont renvoyés dans un entête HTTP (X_REST_DEBOGAGE_MESSAGES) à la fin de l'execution du programme.
683: * - Autre valeur : les messages sont formatés puis retournés par la fonction de débogage (à vous de les afficher).
684: */
685: public static function gererErreurs() {
686: $retour = '';
687: if (self::$debogageActivation && GestionnaireException::getExceptionsNbre() > 0) {
688:
689: $exceptionsTriees = GestionnaireException::getExceptionsTriees();
690: reset($exceptionsTriees);
691: $debogageSeulement = true;
692: if (array_key_exists(E_USER_ERROR, $exceptionsTriees)) {
693: self::envoyerEnteteStatutHttp(self::HTTP_CODE_ERREUR);
694: $debogageSeulement = false;
695: }
696:
697: $exceptionsFormatees = array();
698: foreach ($exceptionsTriees as $exceptions) {
699: foreach ($exceptions as $e) {
700: if ($debogageSeulement && self::$debogageMode == Debug::MODE_ENTETE_HTTP) {
701: $exceptionsFormatees[] = GestionnaireException::formaterExceptionDebug($e);
702: } else {
703: $retour = GestionnaireException::formaterExceptionXhtml($e);
704: }
705: }
706: }
707:
708: if ($debogageSeulement && self::$debogageMode == Debug::MODE_ENTETE_HTTP) {
709: header('X_REST_DEBOGAGE_MESSAGES: '.json_encode($exceptionsFormatees));
710: }
711: }
712: return $retour;
713: }
714:
715:
716: /**
717: * Permet d'ajouter un message d'erreur ou d'avertissement qui sera envoyé au client.
718: * Le message doit être au format texte et en UTF-8.
719: * @param string $message le message à envoyer.
720: */
721: public static function ajouterMessage($message) {
722: if (isset($message) && !empty($message)) {
723: self::$messages[] = $message;
724: }
725: }
726:
727: /**
728: * Envoie au client les éventuels messages d'erreur et d'avertissement du Serveur.
729: * Le format d'envoie est text/plain encodé en UTF-8.
730: */
731: private static function envoyerMessages() {
732: if (count(self::$messages) > 0) {
733: header("Content-Type: text/plain; charset=utf-8");
734: return implode("\n", self::$messages);
735: }
736: }
737: }
738: ?>
739: