[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Virtual HTTP service client 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 */ 22 23 /** 24 * Virtual HTTP service client loosely styled after a Virtual File System 25 * 26 * Services can be mounted on path prefixes so that virtual HTTP operations 27 * against sub-paths will map to those services. Operations can actually be 28 * done using HTTP messages over the wire or may simple be emulated locally. 29 * 30 * Virtual HTTP request maps are arrays that use the following format: 31 * - method : GET/HEAD/PUT/POST/DELETE 32 * - url : HTTP/HTTPS URL or virtual service path with a registered prefix 33 * - query : <query parameter field/value associative array> (uses RFC 3986) 34 * - headers : <header name/value associative array> 35 * - body : source to get the HTTP request body from; 36 * this can simply be a string (always), a resource for 37 * PUT requests, and a field/value array for POST request; 38 * array bodies are encoded as multipart/form-data and strings 39 * use application/x-www-form-urlencoded (headers sent automatically) 40 * - stream : resource to stream the HTTP response body to 41 * Request maps can use integer index 0 instead of 'method' and 1 instead of 'url'. 42 * 43 * @author Aaron Schulz 44 * @since 1.23 45 */ 46 class VirtualRESTServiceClient { 47 /** @var MultiHttpClient */ 48 protected $http; 49 /** @var Array Map of (prefix => VirtualRESTService) */ 50 protected $instances = array(); 51 52 const VALID_MOUNT_REGEX = '#^/[0-9a-z]+/([0-9a-z]+/)*$#'; 53 54 /** 55 * @param MultiHttpClient $http 56 */ 57 public function __construct( MultiHttpClient $http ) { 58 $this->http = $http; 59 } 60 61 /** 62 * Map a prefix to service handler 63 * 64 * @param string $prefix Virtual path 65 * @param VirtualRESTService $instance 66 */ 67 public function mount( $prefix, VirtualRESTService $instance ) { 68 if ( !preg_match( self::VALID_MOUNT_REGEX, $prefix ) ) { 69 throw new UnexpectedValueException( "Invalid service mount point '$prefix'." ); 70 } elseif ( isset( $this->instances[$prefix] ) ) { 71 throw new UnexpectedValueException( "A service is already mounted on '$prefix'." ); 72 } 73 $this->instances[$prefix] = $instance; 74 } 75 76 /** 77 * Unmap a prefix to service handler 78 * 79 * @param string $prefix Virtual path 80 */ 81 public function unmount( $prefix ) { 82 if ( !preg_match( self::VALID_MOUNT_REGEX, $prefix ) ) { 83 throw new UnexpectedValueException( "Invalid service mount point '$prefix'." ); 84 } elseif ( !isset( $this->instances[$prefix] ) ) { 85 throw new UnexpectedValueException( "No service is mounted on '$prefix'." ); 86 } 87 unset( $this->instances[$prefix] ); 88 } 89 90 /** 91 * Get the prefix and service that a virtual path is serviced by 92 * 93 * @param string $path 94 * @return array (prefix,VirtualRESTService) or (null,null) if none found 95 */ 96 public function getMountAndService( $path ) { 97 $cmpFunc = function( $a, $b ) { 98 $al = substr_count( $a, '/' ); 99 $bl = substr_count( $b, '/' ); 100 if ( $al === $bl ) { 101 return 0; // should not actually happen 102 } 103 return ( $al < $bl ) ? 1 : -1; // largest prefix first 104 }; 105 106 $matches = array(); // matching prefixes (mount points) 107 foreach ( $this->instances as $prefix => $service ) { 108 if ( strpos( $path, $prefix ) === 0 ) { 109 $matches[] = $prefix; 110 } 111 } 112 usort( $matches, $cmpFunc ); 113 114 // Return the most specific prefix and corresponding service 115 return isset( $matches[0] ) 116 ? array( $matches[0], $this->instances[$matches[0]] ) 117 : array( null, null ); 118 } 119 120 /** 121 * Execute a virtual HTTP(S) request 122 * 123 * This method returns a response map of: 124 * - code : HTTP response code or 0 if there was a serious cURL error 125 * - reason : HTTP response reason (empty if there was a serious cURL error) 126 * - headers : <header name/value associative array> 127 * - body : HTTP response body or resource (if "stream" was set) 128 * - err : Any cURL error string 129 * The map also stores integer-indexed copies of these values. This lets callers do: 130 * <code> 131 * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $client->run( $req ); 132 * </code> 133 * @param array $req Virtual HTTP request array 134 * @return array Response array for request 135 */ 136 public function run( array $req ) { 137 $req = $this->runMulti( array( $req ) ); 138 return $req[0]['response']; 139 } 140 141 /** 142 * Execute a set of virtual HTTP(S) requests concurrently 143 * 144 * A map of requests keys to response maps is returned. Each response map has: 145 * - code : HTTP response code or 0 if there was a serious cURL error 146 * - reason : HTTP response reason (empty if there was a serious cURL error) 147 * - headers : <header name/value associative array> 148 * - body : HTTP response body or resource (if "stream" was set) 149 * - err : Any cURL error string 150 * The map also stores integer-indexed copies of these values. This lets callers do: 151 * <code> 152 * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $responses[0]; 153 * </code> 154 * 155 * @param array $req Map of Virtual HTTP request arrays 156 * @return array $reqs Map of corresponding response values with the same keys/order 157 */ 158 public function runMulti( array $reqs ) { 159 foreach ( $reqs as $index => &$req ) { 160 if ( isset( $req[0] ) ) { 161 $req['method'] = $req[0]; // short-form 162 unset( $req[0] ); 163 } 164 if ( isset( $req[1] ) ) { 165 $req['url'] = $req[1]; // short-form 166 unset( $req[1] ); 167 } 168 $req['chain'] = array(); // chain or list of replaced requests 169 } 170 unset( $req ); // don't assign over this by accident 171 172 $curUniqueId = 0; 173 $armoredIndexMap = array(); // (original index => new index) 174 175 $doneReqs = array(); // (index => request) 176 $executeReqs = array(); // (index => request) 177 $replaceReqsByService = array(); // (prefix => index => request) 178 $origPending = array(); // (index => 1) for original requests 179 180 foreach ( $reqs as $origIndex => $req ) { 181 // Re-index keys to consecutive integers (they will be swapped back later) 182 $index = $curUniqueId++; 183 $armoredIndexMap[$origIndex] = $index; 184 $origPending[$index] = 1; 185 if ( preg_match( '#^(http|ftp)s?://#', $req['url'] ) ) { 186 // Absolute FTP/HTTP(S) URL, run it as normal 187 $executeReqs[$index] = $req; 188 } else { 189 // Must be a virtual HTTP URL; resolve it 190 list( $prefix, $service ) = $this->getMountAndService( $req['url'] ); 191 if ( !$service ) { 192 throw new UnexpectedValueException( "Path '{$req['url']}' has no service." ); 193 } 194 // Set the URL to the mount-relative portion 195 $req['url'] = substr( $req['url'], strlen( $prefix ) ); 196 $replaceReqsByService[$prefix][$index] = $req; 197 } 198 } 199 200 // Function to get IDs that won't collide with keys in $armoredIndexMap 201 $idFunc = function() use ( &$curUniqueId ) { 202 return $curUniqueId++; 203 }; 204 205 $rounds = 0; 206 do { 207 if ( ++$rounds > 5 ) { // sanity 208 throw new Exception( "Too many replacement rounds detected. Aborting." ); 209 } 210 // Resolve the virtual URLs valid and qualified HTTP(S) URLs 211 // and add any required authentication headers for the backend. 212 // Services can also replace requests with new ones, either to 213 // defer the original or to set a proxy response to the original. 214 $newReplaceReqsByService = array(); 215 foreach ( $replaceReqsByService as $prefix => $servReqs ) { 216 $service = $this->instances[$prefix]; 217 foreach ( $service->onRequests( $servReqs, $idFunc ) as $index => $req ) { 218 // Services use unique IDs for replacement requests 219 if ( isset( $servReqs[$index] ) || isset( $origPending[$index] ) ) { 220 // A current or original request which was not modified 221 } else { 222 // Replacement requests with pre-set responses should not execute 223 $newReplaceReqsByService[$prefix][$index] = $req; 224 } 225 if ( isset( $req['response'] ) ) { 226 // Replacement requests with pre-set responses should not execute 227 unset( $executeReqs[$index] ); 228 unset( $origPending[$index] ); 229 $doneReqs[$index] = $req; 230 } else { 231 // Original or mangled request included 232 $executeReqs[$index] = $req; 233 } 234 } 235 } 236 // Update index of requests to inspect for replacement 237 $replaceReqsByService = $newReplaceReqsByService; 238 // Run the actual work HTTP requests 239 foreach ( $this->http->runMulti( $executeReqs ) as $index => $ranReq ) { 240 $doneReqs[$index] = $ranReq; 241 unset( $origPending[$index] ); 242 } 243 $executeReqs = array(); 244 // Services can also replace requests with new ones, either to 245 // defer the original or to set a proxy response to the original. 246 // Any replacement requests executed above will need to be replaced 247 // with new requests (eventually the original). The responses can be 248 // forced instead of having the request sent over the wire. 249 $newReplaceReqsByService = array(); 250 foreach ( $replaceReqsByService as $prefix => $servReqs ) { 251 $service = $this->instances[$prefix]; 252 // Only the request copies stored in $doneReqs actually have the response 253 $servReqs = array_intersect_key( $doneReqs, $servReqs ); 254 foreach ( $service->onResponses( $servReqs, $idFunc ) as $index => $req ) { 255 // Services use unique IDs for replacement requests 256 if ( isset( $servReqs[$index] ) || isset( $origPending[$index] ) ) { 257 // A current or original request which was not modified 258 } else { 259 // Replacement requests with pre-set responses should not execute 260 $newReplaceReqsByService[$prefix][$index] = $req; 261 } 262 if ( isset( $req['response'] ) ) { 263 // Replacement requests with pre-set responses should not execute 264 unset( $origPending[$index] ); 265 $doneReqs[$index] = $req; 266 } else { 267 // Update the request in case it was mangled 268 $executeReqs[$index] = $req; 269 } 270 } 271 } 272 // Update index of requests to inspect for replacement 273 $replaceReqsByService = $newReplaceReqsByService; 274 } while ( count( $origPending ) ); 275 276 $responses = array(); 277 // Update $reqs to include 'response' and normalized request 'headers'. 278 // This maintains the original order of $reqs. 279 foreach ( $reqs as $origIndex => $req ) { 280 $index = $armoredIndexMap[$origIndex]; 281 if ( !isset( $doneReqs[$index] ) ) { 282 throw new UnexpectedValueException( "Response for request '$index' is NULL." ); 283 } 284 $responses[$origIndex] = $doneReqs[$index]['response']; 285 } 286 287 return $responses; 288 } 289 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |