MediaWiki
REL1_24
|
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 }