1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40:
41:
42: class RestServeur {
43:
44:
45: const METHODE_GET = 'consulter';
46:
47:
48: const METHODE_POST = 'modifier';
49:
50:
51: const METHODE_DELETE = 'supprimer';
52:
53:
54: const METHODE_PUT = 'ajouter';
55:
56: 57: 58:
59: const HTTP_CODE_OK = '200';
60:
61: 62:
63: const HTTP_CODE_CREATION_OK = '201';
64:
65: 66: 67:
68: const HTTP_CODE_SUPPRESSION_OK = '204';
69:
70: 71: 72:
73: const HTTP_CODE_MAUVAISE_REQUETE = '400';
74:
75: 76:
77: const HTTP_CODE_ACCES_NON_AUTORISE = '401';
78:
79:
80: const HTTP_CODE_RESSOURCE_INTROUVABLE = '404';
81:
82: 83: 84:
85: const HTTP_CODE_METHODE_NON_AUTORISE = '405';
86:
87: 88: 89:
90: const HTTP_CODE_CONFLIT = '409';
91:
92:
93: const HTTP_CODE_CONTENU_REQUIS = '411';
94:
95: 96:
97: const HTTP_CODE_ERREUR = '500';
98:
99:
100: const MOTIF_API_VERSION = '/^[0-9]+(?:[.][0-9]+)*$/';
101:
102:
103: const MOTIF_SERVICE_NOM = '/^[a-z0-9]+(?:[-][a-z0-9]+)*$/';
104:
105: 106:
107: private static $debogageActivation = false;
108:
109: 110:
111: private static $debogageMode = '';
112:
113: 114:
115: private $methode = 'GET';
116:
117: 118:
119: private $requeteDonnees = null;
120:
121: 122:
123: private $requeteDonneesParsees = null;
124:
125: 126: 127: 128:
129: private $apiVersion = null;
130:
131: 132: 133: 134:
135: private $service = null;
136:
137: 138: 139: 140:
141: private $ressources = array();
142:
143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153:
154: private $parametres = array();
155:
156: 157: 158: 159: 160: 161: 162:
163: private $parametresBruts = array();
164:
165: 166: 167:
168: private static $config = array();
169:
170: 171: 172:
173: private static $messages = array();
174:
175:
176: private static $http10 = array(
177: self::HTTP_CODE_OK => 'OK',
178: self::HTTP_CODE_CREATION_OK => 'Created',
179: self::HTTP_CODE_SUPPRESSION_OK => 'No Content',
180: self::HTTP_CODE_MAUVAISE_REQUETE => 'Bad Request',
181: self::HTTP_CODE_ACCES_NON_AUTORISE => 'Unauthorized',
182: self::HTTP_CODE_RESSOURCE_INTROUVABLE => 'Not Found',
183: self::HTTP_CODE_METHODE_NON_AUTORISE => 'Method Not Allowed',
184: self::HTTP_CODE_CONFLIT => 'Conflict',
185: self::HTTP_CODE_CONTENU_REQUIS => 'Length Required',
186: self::HTTP_CODE_ERREUR => 'Internal Server Error'
187: );
188:
189:
190: private $parametres_obligatoires = array('debogage', 'debogage_mode', 'serveur.baseURL', 'chemin_modules');
191:
192: 193: 194: 195:
196: public function __construct() {
197: Config::verifierPresenceParametres($this->parametres_obligatoires);
198:
199: self::$debogageActivation = Config::get('debogage');
200: self::$debogageMode = Config::get('debogage_mode');
201:
202: if (isset($_SERVER['REQUEST_URI']) && isset($_SERVER['REQUEST_METHOD']) && isset($_SERVER['QUERY_STRING'])) {
203: $this->initialiserMethode();
204: $this->initialiserRequeteDonnees();
205:
206: $urlParts = $this->decouperUrlChemin();
207:
208: $this->initialiserApiVersion(array_shift($urlParts));
209: $this->initialiserServiceNom(array_shift($urlParts));
210: $this->initialiserRessource($urlParts);
211:
212: $this->initialiserParametres();
213:
214: spl_autoload_register(array(get_class(), 'chargerClasse'));
215: } else {
216: self::envoyerEnteteStatutHttp(self::HTTP_CODE_ERREUR);
217: $e = "La classe Serveur du TBFRamework nécessite, pour fonctionner, l'accès aux variables serveurs REQUEST_URI, REQUEST_METHOD et QUERY_STRING.";
218: self::ajouterMessage($e);
219: }
220: }
221:
222: private function initialiserMethode() {
223: if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']) && count(trim($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) > 0) {
224: $this->methode = trim($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
225: } else {
226: $this->methode = $_SERVER['REQUEST_METHOD'];
227: }
228: }
229:
230: private function initialiserRequeteDonnees() {
231: if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['CONTENT_LENGTH'] > 0) {
232: $this->requeteDonnees = '';
233: $httpContent = fopen('php://input', 'r');
234: while ($data = fread($httpContent, 1024)) {
235: $this->requeteDonnees .= $data;
236: }
237: fclose($httpContent);
238: }
239: }
240:
241: private function decouperUrlChemin() {
242: if (isset($_SERVER['REDIRECT_URL']) && $_SERVER['REDIRECT_URL'] != '') {
243: if (isset($_SERVER['REDIRECT_QUERY_STRING']) && !empty($_SERVER['REDIRECT_QUERY_STRING'])) {
244: $url = $_SERVER['REDIRECT_URL'].'?'.$_SERVER['REDIRECT_QUERY_STRING'];
245: } else {
246: $url = $_SERVER['REDIRECT_URL'];
247: }
248: } else {
249: $url = $_SERVER['REQUEST_URI'];
250: }
251:
252: if (strlen($_SERVER['QUERY_STRING']) == 0) {
253: $tailleURL = strlen($url);
254: } else {
255: $tailleURL = -(strlen($_SERVER['QUERY_STRING']) + 1);
256: }
257:
258: $urlChaine = '';
259: if (strpos($url, Config::get('serveur.baseURL')) !== false) {
260: $urlChaine = substr($url, strlen(Config::get('serveur.baseURL')), $tailleURL);
261: } else if (strpos($url, Config::get('serveur.baseAlternativeURL')) !== false) {
262: $urlChaine = substr($url, strlen(Config::get('serveur.baseAlternativeURL')), $tailleURL);
263: }
264: return explode('/', $urlChaine);
265: }
266:
267: private function initialiserApiVersion($apiVersion) {
268: if ($this->verifierApiVersion($apiVersion)) {
269: $this->apiVersion = $apiVersion;
270: self::$config['chemins']['api'] = Config::get('chemin_modules').$this->apiVersion.DS;
271: self::$config['chemins']['api_bibliotheque'] = self::$config['chemins']['api'].Config::get('dossier_bibliotheque').DS;
272: } else {
273: self::envoyerEnteteStatutHttp(self::HTTP_CODE_MAUVAISE_REQUETE);
274: $e = "Aucune version d'API n'a été spécifiée.\n".
275: "La version doit respecter l'expression régulière suivante : ".self::MOTIF_API_VERSION.".\n".
276: "L'url doit avoir la forme suivante : http://www.mondomaine.org/services/apiVersion/monService/";
277: self::ajouterMessage($e);
278: self::cloreAccesServeur();
279: }
280: }
281:
282: private function verifierApiVersion($apiVersion) {
283: $apiOk = false;
284: if (isset($apiVersion) && !empty($apiVersion) && preg_match(self::MOTIF_API_VERSION, $apiVersion)) {
285: $apiOk = true;
286: }
287: return $apiOk;
288: }
289:
290: private function initialiserServiceNom($serviceNom) {
291: if ($this->verifierServiceNom($serviceNom)) {
292: $this->service = $this->traiterNomService($serviceNom);
293: } else {
294: self::envoyerEnteteStatutHttp(self::HTTP_CODE_MAUVAISE_REQUETE);
295: $e = "Aucune nom de service n'a été spécifié.\n".
296: "La nom du service doit respecter l'expression régulière suivante : ".self::MOTIF_SERVICE_NOM.".\n".
297: "L'url doit avoir la forme suivante : http://www.mondomaine.org/services/apiVersion/monService/";
298: self::ajouterMessage($e);
299: self::cloreAccesServeur();
300: }
301: }
302:
303: private function verifierServiceNom($serviceNom) {
304: $serviceNomOk = false;
305: if (isset($serviceNom) && !empty($serviceNom) && preg_match(self::MOTIF_SERVICE_NOM, $serviceNom)) {
306: $serviceNomOk = true;
307: }
308: return $serviceNomOk;
309: }
310:
311: private function traiterNomService($serviceNom) {
312: return str_replace(' ', '', ucwords(str_replace('-', ' ', strtolower($serviceNom))));
313: }
314:
315: private function initialiserRessource($urlParts) {
316: if (is_array($urlParts) && count($urlParts) > 0) {
317: foreach ($urlParts as $ressource) {
318:
319: if ($ressource != '') {
320: $this->ressources[] = urldecode($ressource);
321: }
322: }
323: }
324: }
325:
326: private function initialiserParametres() {
327: $this->parametres = $this->recupererParametresGet();
328: $this->parametresBruts = $this->recupererParametresBruts();
329: }
330:
331: private function recupererParametresGet() {
332: $_GET = $this->nettoyerParametres($_GET);
333: return $_GET;
334: }
335:
336: private function nettoyerParametres(Array $parametres) {
337:
338: if (isset($parametres) && count($parametres) > 0) {
339: foreach ($parametres as $cle => $valeur) {
340:
341:
342: $verifier = array('NULL', "\\", "\x00", "\x1a");
343: $parametres[$cle] = strip_tags(str_replace($verifier, '', $valeur));
344: }
345: }
346: return $parametres;
347: }
348:
349: private function recupererParametresBruts() {
350: $parametres_bruts = array();
351: if (!empty($_SERVER['QUERY_STRING'])) {
352: $paires = explode('&', $_SERVER['QUERY_STRING']);
353: foreach ($paires as $paire) {
354: $nv = explode('=', $paire);
355: $nom = urldecode($nv[0]);
356: $valeur = urldecode($nv[1]);
357: $parametres_bruts[$nom] = $valeur;
358: }
359: $parametres_bruts = $this->nettoyerParametres($parametres_bruts);
360: }
361: return $parametres_bruts;
362: }
363:
364: 365: 366: 367: 368: 369: 370:
371: public static function chargerClasse($classe) {
372: if (class_exists($classe)) {
373: return null;
374: }
375: $chemins = array('', self::$config['chemins']['api'], self::$config['chemins']['api_bibliotheque']);
376: foreach ($chemins as $chemin) {
377: $chemin = $chemin.$classe.'.php';
378: if (file_exists($chemin)) {
379: require_once $chemin;
380: }
381: }
382: }
383:
384: 385: 386:
387: public function executer() {
388: $retour = '';
389: switch ($this->methode) {
390: case 'GET':
391: $retour = $this->get();
392: break;
393: case 'POST':
394: $retour = $this->post();
395: break;
396: case 'DELETE':
397: $retour = $this->delete();
398: break;
399: case 'PUT':
400: $retour = $this->put();
401: break;
402:
403: case 'OPTIONS':
404: 405: 406: 407: 408: 409:
410: break;
411: default :
412: self::envoyerEnteteStatutHttp(self::HTTP_CODE_METHODE_NON_AUTORISE);
413: header('Allow: GET, POST, DELETE, PUT');
414: $e = "La méthode HTTP '{$this->methode}' n'est pas prise en compte par ce serveur REST.\n".
415: "Consulter l'entête Allow pour connaître les méthodes autorisées.";
416: self::ajouterMessage($e);
417: }
418: $this->cloreAccesServeur($retour);
419: }
420:
421: 422: 423: 424: 425:
426: private function get() {
427: $retour = '';
428: if ($this->service != null) {
429: $Service = new $this->service(self::$config);
430: if (method_exists($Service, self::METHODE_GET)) {
431: $methodeGet = self::METHODE_GET;
432: $parametres = $Service->utiliserParametresBruts() ? $this->parametresBruts : $this->parametres;
433: $retour = $Service->$methodeGet($this->ressources, $parametres);
434: } else {
435: self::envoyerEnteteStatutHttp(self::HTTP_CODE_RESSOURCE_INTROUVABLE);
436: $e = "Le service '{$this->service}' ne contient pas la méthode '".self::METHODE_GET."' nécessaire ".
437: "lors de l'appel du service via la méthode HTTP GET.";
438: self::ajouterMessage($e);
439: }
440: }
441: return $retour;
442: }
443:
444: private function post() {
445: $retour = '';
446: $paires = $this->parserDonneesRequete();
447: if (count($paires) != 0) {
448: if (isset($paires['action']) && $paires['action'] == 'DELETE') {
449: $retour = $this->delete();
450: } else if (count($this->ressources) == 0) {
451: $retour = $this->put();
452: } else {
453: if ($this->service != null) {
454: $Service = new $this->service(self::$config);
455: if (method_exists($Service, self::METHODE_POST)) {
456: $methodePost = self::METHODE_POST;
457: $info = $Service->$methodePost($this->ressources, $paires);
458: if ($info !== false) {
459: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_CREATION_OK);
460: $retour = $info;
461: }
462: } else {
463: self::envoyerEnteteStatutHttp(self::HTTP_CODE_RESSOURCE_INTROUVABLE);
464: $e = "Le service '{$this->service}' ne contient pas la méthode '".self::METHODE_POST."' nécessaire ".
465: "lors de l'appel du service via la méthode HTTP GET.";
466: self::ajouterMessage($e);
467: }
468: }
469: }
470: } else {
471: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_CONTENU_REQUIS);
472: $e = "Le service '{$this->service}' requiert de fournir le contenu à modifier dans le corps ".
473: "de la requête avec la méthode HTTP POST.";
474: self::ajouterMessage($e);
475: }
476: return $retour;
477: }
478:
479: private function put() {
480: $retour = '';
481: $paires = $this->parserDonneesRequete();
482: if (count($paires) != 0) {
483: if ($this->service != null) {
484: $Service = new $this->service(self::$config);
485: if (method_exists($Service, self::METHODE_PUT)) {
486: $methodePut = self::METHODE_PUT;
487: $info = $Service->$methodePut($this->ressources, $paires);
488: if ($info !== false) {
489: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_CREATION_OK);
490: $retour = $info;
491: }
492: } else {
493: self::envoyerEnteteStatutHttp(self::HTTP_CODE_RESSOURCE_INTROUVABLE);
494: $e = "Le service '{$this->service}' ne contient pas la méthode '".self::METHODE_PUT."' nécessaire ".
495: "lors de l'appel du service via la méthode HTTP GET.";
496: self::ajouterMessage($e);
497: }
498: }
499: } else {
500: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_CONTENU_REQUIS);
501: $e = "Il est nécessaire de fournir du contenu dans le corps de la requête pour créer une nouvelle ressource.";
502: self::ajouterMessage($e);
503: }
504: return $retour;
505: }
506:
507: private function delete() {
508: $retour = '';
509: if (count($this->ressources) != 0) {
510: $paires = $this->parserDonneesRequete();
511: if ($this->service != null) {
512: $Service = new $this->service(self::$config);
513: if (method_exists($Service, self::METHODE_DELETE)) {
514: $methodeDelete = self::METHODE_DELETE;
515: $info = $Service->$methodeDelete($this->ressources, $paires);
516: if ($info === true) {
517: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_SUPPRESSION_OK);
518: } else if ($info === false) {
519: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_RESSOURCE_INTROUVABLE);
520: $e = "La ressource à supprimer est introuvable. Il se peut qu'elle ait été préalablement supprimé.";
521: self::ajouterMessage($e);
522: } else {
523: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_OK);
524: $retour = $info;
525: }
526: } else {
527: self::envoyerEnteteStatutHttp(self::HTTP_CODE_RESSOURCE_INTROUVABLE);
528: $e = "Le service '{$this->service}' ne contient pas la méthode '".self::METHODE_DELETE."' nécessaire ".
529: "lors de l'appel du service via la méthode HTTP GET.";
530: self::ajouterMessage($e);
531: }
532: }
533: } else {
534: $this->envoyerEnteteStatutHttp(self::HTTP_CODE_MAUVAISE_REQUETE);
535: $e = "Il est nécessaire d'indiquer dans l'url la ressource à supprimer.";
536: self::ajouterMessage($e);
537: }
538: return $retour;
539: }
540:
541: 542: 543: 544: 545: 546: 547:
548: private function parserDonneesRequete() {
549: $donnees = array();
550: if ($this->requeteDonneesParsees != null) {
551: $donnees = $this->requeteDonneesParsees;
552: } else if ($this->requeteDonnees != null) {
553: if (preg_match('/application\/json/', $_SERVER['CONTENT_TYPE'])) {
554: $donnees = json_decode($this->requeteDonnees, true);
555: } else {
556: $paires = explode('&', $this->requeteDonnees);
557: foreach ($paires as $paire) {
558: list($cle, $valeur) = explode('=', $paire);
559: $cle = (isset($cle)) ? trim(urldecode($cle)) : '';
560: $valeur = (isset($valeur)) ? trim(urldecode($valeur)) : '';
561: $donnees[$cle] = $valeur;
562: }
563: }
564: $this->requeteDonneesParsees = $donnees;
565: }
566: return $donnees;
567: }
568:
569: 570: 571: 572: 573:
574: public static function envoyerEnteteStatutHttp($code) {
575: if (isset(self::$http10[$code])) {
576: $txt = self::$http10[$code];
577: header("HTTP/1.0 $code $txt", true);
578: }
579: }
580:
581: 582: 583:
584: private static function cloreAccesServeur($retour = '') {
585:
586: $retour .= self::gererErreurs();
587:
588:
589: $retour .= self::envoyerMessages();
590:
591:
592:
593:
594:
595: print $retour;
596:
597:
598: exit(0);
599: }
600:
601: 602: 603: 604: 605: 606: 607: 608: 609: 610: 611: 612: 613:
614: public static function gererErreurs() {
615: $retour = '';
616: if (self::$debogageActivation && GestionnaireException::getExceptionsNbre() > 0) {
617:
618: $exceptionsTriees = GestionnaireException::getExceptionsTriees();
619: reset($exceptionsTriees);
620: $debogageSeulement = true;
621: if (array_key_exists(E_USER_ERROR, $exceptionsTriees)) {
622: self::envoyerEnteteStatutHttp(self::HTTP_CODE_ERREUR);
623: $debogageSeulement = false;
624: }
625:
626: $exceptionsFormatees = array();
627: foreach ($exceptionsTriees as $exceptions) {
628: foreach ($exceptions as $e) {
629: if ($debogageSeulement && self::$debogageMode == Debug::MODE_ENTETE_HTTP) {
630: $exceptionsFormatees[] = GestionnaireException::formaterExceptionDebug($e);
631: } else {
632: $retour = GestionnaireException::formaterExceptionXhtml($e);
633: }
634: }
635: }
636:
637: if ($debogageSeulement && self::$debogageMode == Debug::MODE_ENTETE_HTTP) {
638: header('X_REST_DEBOGAGE_MESSAGES: '.json_encode($exceptionsFormatees));
639: }
640: }
641: return $retour;
642: }
643:
644:
645: 646: 647: 648: 649:
650: public static function ajouterMessage($message) {
651: if (isset($message) && !empty($message)) {
652: self::$messages[] = $message;
653: }
654: }
655:
656: 657: 658: 659:
660: private static function envoyerMessages() {
661: if (count(self::$messages) > 0) {
662: header("Content-Type: text/plain; charset=utf-8");
663: return implode("\n", self::$messages);
664: }
665: }
666: }
667: ?>