[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/libs/virtualrest/ -> VirtualRESTServiceClient.php (source)

   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  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1