MediaWiki  REL1_24
VirtualRESTServiceClient.php
Go to the documentation of this file.
00001 <?php
00046 class VirtualRESTServiceClient {
00048     protected $http;
00050     protected $instances = array();
00051 
00052     const VALID_MOUNT_REGEX = '#^/[0-9a-z]+/([0-9a-z]+/)*$#';
00053 
00057     public function __construct( MultiHttpClient $http ) {
00058         $this->http = $http;
00059     }
00060 
00067     public function mount( $prefix, VirtualRESTService $instance ) {
00068         if ( !preg_match( self::VALID_MOUNT_REGEX, $prefix ) ) {
00069             throw new UnexpectedValueException( "Invalid service mount point '$prefix'." );
00070         } elseif ( isset( $this->instances[$prefix] ) ) {
00071             throw new UnexpectedValueException( "A service is already mounted on '$prefix'." );
00072         }
00073         $this->instances[$prefix] = $instance;
00074     }
00075 
00081     public function unmount( $prefix ) {
00082         if ( !preg_match( self::VALID_MOUNT_REGEX, $prefix ) ) {
00083             throw new UnexpectedValueException( "Invalid service mount point '$prefix'." );
00084         } elseif ( !isset( $this->instances[$prefix] ) ) {
00085             throw new UnexpectedValueException( "No service is mounted on '$prefix'." );
00086         }
00087         unset( $this->instances[$prefix] );
00088     }
00089 
00096     public function getMountAndService( $path ) {
00097         $cmpFunc = function( $a, $b ) {
00098             $al = substr_count( $a, '/' );
00099             $bl = substr_count( $b, '/' );
00100             if ( $al === $bl ) {
00101                 return 0; // should not actually happen
00102             }
00103             return ( $al < $bl ) ? 1 : -1; // largest prefix first
00104         };
00105 
00106         $matches = array(); // matching prefixes (mount points)
00107         foreach ( $this->instances as $prefix => $service ) {
00108             if ( strpos( $path, $prefix ) === 0 ) {
00109                 $matches[] = $prefix;
00110             }
00111         }
00112         usort( $matches, $cmpFunc );
00113 
00114         // Return the most specific prefix and corresponding service
00115         return isset( $matches[0] )
00116             ? array( $matches[0], $this->instances[$matches[0]] )
00117             : array( null, null );
00118     }
00119 
00136     public function run( array $req ) {
00137         $req = $this->runMulti( array( $req ) );
00138         return $req[0]['response'];
00139     }
00140 
00158     public function runMulti( array $reqs ) {
00159         foreach ( $reqs as $index => &$req ) {
00160             if ( isset( $req[0] ) ) {
00161                 $req['method'] = $req[0]; // short-form
00162                 unset( $req[0] );
00163             }
00164             if ( isset( $req[1] ) ) {
00165                 $req['url'] = $req[1]; // short-form
00166                 unset( $req[1] );
00167             }
00168             $req['chain'] = array(); // chain or list of replaced requests
00169         }
00170         unset( $req ); // don't assign over this by accident
00171 
00172         $curUniqueId = 0;
00173         $armoredIndexMap = array(); // (original index => new index)
00174 
00175         $doneReqs = array(); // (index => request)
00176         $executeReqs = array(); // (index => request)
00177         $replaceReqsByService = array(); // (prefix => index => request)
00178         $origPending = array(); // (index => 1) for original requests
00179 
00180         foreach ( $reqs as $origIndex => $req ) {
00181             // Re-index keys to consecutive integers (they will be swapped back later)
00182             $index = $curUniqueId++;
00183             $armoredIndexMap[$origIndex] = $index;
00184             $origPending[$index] = 1;
00185             if ( preg_match( '#^(http|ftp)s?://#', $req['url'] ) ) {
00186                 // Absolute FTP/HTTP(S) URL, run it as normal
00187                 $executeReqs[$index] = $req;
00188             } else {
00189                 // Must be a virtual HTTP URL; resolve it
00190                 list( $prefix, $service ) = $this->getMountAndService( $req['url'] );
00191                 if ( !$service ) {
00192                     throw new UnexpectedValueException( "Path '{$req['url']}' has no service." );
00193                 }
00194                 // Set the URL to the mount-relative portion
00195                 $req['url'] = substr( $req['url'], strlen( $prefix ) );
00196                 $replaceReqsByService[$prefix][$index] = $req;
00197             }
00198         }
00199 
00200         // Function to get IDs that won't collide with keys in $armoredIndexMap
00201         $idFunc = function() use ( &$curUniqueId ) {
00202             return $curUniqueId++;
00203         };
00204 
00205         $rounds = 0;
00206         do {
00207             if ( ++$rounds > 5 ) { // sanity
00208                 throw new Exception( "Too many replacement rounds detected. Aborting." );
00209             }
00210             // Resolve the virtual URLs valid and qualified HTTP(S) URLs
00211             // and add any required authentication headers for the backend.
00212             // Services can also replace requests with new ones, either to
00213             // defer the original or to set a proxy response to the original.
00214             $newReplaceReqsByService = array();
00215             foreach ( $replaceReqsByService as $prefix => $servReqs ) {
00216                 $service = $this->instances[$prefix];
00217                 foreach ( $service->onRequests( $servReqs, $idFunc ) as $index => $req ) {
00218                     // Services use unique IDs for replacement requests
00219                     if ( isset( $servReqs[$index] ) || isset( $origPending[$index] ) ) {
00220                         // A current or original request which was not modified
00221                     } else {
00222                         // Replacement requests with pre-set responses should not execute
00223                         $newReplaceReqsByService[$prefix][$index] = $req;
00224                     }
00225                     if ( isset( $req['response'] ) ) {
00226                         // Replacement requests with pre-set responses should not execute
00227                         unset( $executeReqs[$index] );
00228                         unset( $origPending[$index] );
00229                         $doneReqs[$index] = $req;
00230                     } else {
00231                         // Original or mangled request included
00232                         $executeReqs[$index] = $req;
00233                     }
00234                 }
00235             }
00236             // Update index of requests to inspect for replacement
00237             $replaceReqsByService = $newReplaceReqsByService;
00238             // Run the actual work HTTP requests
00239             foreach ( $this->http->runMulti( $executeReqs ) as $index => $ranReq ) {
00240                 $doneReqs[$index] = $ranReq;
00241                 unset( $origPending[$index] );
00242             }
00243             $executeReqs = array();
00244             // Services can also replace requests with new ones, either to
00245             // defer the original or to set a proxy response to the original.
00246             // Any replacement requests executed above will need to be replaced
00247             // with new requests (eventually the original). The responses can be
00248             // forced instead of having the request sent over the wire.
00249             $newReplaceReqsByService = array();
00250             foreach ( $replaceReqsByService as $prefix => $servReqs ) {
00251                 $service = $this->instances[$prefix];
00252                 // Only the request copies stored in $doneReqs actually have the response
00253                 $servReqs = array_intersect_key( $doneReqs, $servReqs );
00254                 foreach ( $service->onResponses( $servReqs, $idFunc ) as $index => $req ) {
00255                     // Services use unique IDs for replacement requests
00256                     if ( isset( $servReqs[$index] ) || isset( $origPending[$index] ) ) {
00257                         // A current or original request which was not modified
00258                     } else {
00259                         // Replacement requests with pre-set responses should not execute
00260                         $newReplaceReqsByService[$prefix][$index] = $req;
00261                     }
00262                     if ( isset( $req['response'] ) ) {
00263                         // Replacement requests with pre-set responses should not execute
00264                         unset( $origPending[$index] );
00265                         $doneReqs[$index] = $req;
00266                     } else {
00267                         // Update the request in case it was mangled
00268                         $executeReqs[$index] = $req;
00269                     }
00270                 }
00271             }
00272             // Update index of requests to inspect for replacement
00273             $replaceReqsByService = $newReplaceReqsByService;
00274         } while ( count( $origPending ) );
00275 
00276         $responses = array();
00277         // Update $reqs to include 'response' and normalized request 'headers'.
00278         // This maintains the original order of $reqs.
00279         foreach ( $reqs as $origIndex => $req ) {
00280             $index = $armoredIndexMap[$origIndex];
00281             if ( !isset( $doneReqs[$index] ) ) {
00282                 throw new UnexpectedValueException( "Response for request '$index' is NULL." );
00283             }
00284             $responses[$origIndex] = $doneReqs[$index]['response'];
00285         }
00286 
00287         return $responses;
00288     }
00289 }