Introduce the Restbase Virtual REST Service class
authorMarko Obrovac <mobrovac@wikimedia.org>
Mon, 2 Mar 2015 14:35:21 +0000 (15:35 +0100)
committerMarko Obrovac <mobrovac@wikimedia.org>
Mon, 9 Mar 2015 21:05:58 +0000 (22:05 +0100)
Restbase, the REST content API service, is to be queried instead of
Parsoid by current Parsoid users (most importantly VE). This patch
introduces the Restbase virtual REST service class and transparently
maps Parsoid calls into Restbase ones if parsoidCompat is set when
creating the service object.

Additionally, $wgVirtualRestConfig is introduced in DefaultSettings.php. This
is a first step towards global service configuration and management.

Bug: T89066
Change-Id: I4d4043e5052327bbd789331f1c05b607c45fe7cb

autoload.php
includes/DefaultSettings.php
includes/libs/virtualrest/ParsoidVirtualRESTService.php
includes/libs/virtualrest/RestbaseVirtualRESTService.php [new file with mode: 0644]

index faf8252..da61403 100644 (file)
@@ -995,6 +995,7 @@ $wgAutoloadLocalClasses = array(
        'ResourceLoaderUserOptionsModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserOptionsModule.php',
        'ResourceLoaderUserTokensModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserTokensModule.php',
        'ResourceLoaderWikiModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderWikiModule.php',
+       'RestbaseVirtualRESTService' => __DIR__ . '/includes/libs/virtualrest/RestbaseVirtualRESTService.php',
        'ResultWrapper' => __DIR__ . '/includes/db/DatabaseUtility.php',
        'RevDelArchiveItem' => __DIR__ . '/includes/revisiondelete/RevDelArchiveItem.php',
        'RevDelArchiveList' => __DIR__ . '/includes/revisiondelete/RevDelArchiveList.php',
index 8080774..52c0eab 100644 (file)
@@ -7420,6 +7420,34 @@ $wgPageLanguageUseDB = false;
  */
 $wgUseLinkNamespaceDBFields = true;
 
+/**
+ * Global configuration variable for Virtual REST Services.
+ * Parameters for different services are to be declared inside
+ * $wgVirtualRestConfig['modules'], which is to be treated as an associative
+ * array. Global parameters will be merged with service-specific ones. The
+ * result will then be passed to VirtualRESTService::__construct() in the
+ * module.
+ *
+ * Example config for Parsoid:
+ *
+ *   $wgVirtualRestConfig['modules']['parsoid'] = array(
+ *     'url' => 'http://localhost:8000',
+ *     'prefix' => 'enwiki',
+ *   );
+ *
+ * @var array
+ * @since 1.25
+ */
+$wgVirtualRestConfig = array(
+       'modules' => array(),
+       'global' => array(
+               # Timeout in seconds
+               'timeout' => 360,
+               'forwardCookies' => false,
+               'HTTPProxy' => null
+       )
+);
+
 /**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
index 769cecf..32a27f7 100644 (file)
@@ -34,13 +34,18 @@ class ParsoidVirtualRESTService extends VirtualRESTService {
         *   * body: array( 'wikitext' => ... ) or array( 'wikitext' => ..., 'body' => true/false )
         *   * $title is optional
         * @param array $params Key/value map
-        *   - URL            : Parsoid server URL
+        *   - url            : Parsoid server URL
         *   - prefix         : Parsoid prefix for this wiki
         *   - timeout        : Parsoid timeout (optional)
         *   - forwardCookies : Cookies to forward to Parsoid, or false. (optional)
         *   - HTTPProxy      : Parsoid HTTP proxy (optional)
         */
        public function __construct( array $params ) {
+               // for backwards compatibility:
+               if ( isset( $params['URL'] ) ) {
+                       $params['url'] = $params['URL'];
+                       unset( $params['URL'] );
+               }
                parent::__construct( $params );
        }
 
@@ -63,7 +68,7 @@ class ParsoidVirtualRESTService extends VirtualRESTService {
                                throw new Exception( "Request type must be either 'page' or 'transform'" );
                        }
 
-                       $req['url'] = $this->params['URL'] . '/' . urlencode( $this->params['prefix'] ) . '/';
+                       $req['url'] = $this->params['url'] . '/' . urlencode( $this->params['prefix'] ) . '/';
 
                        if ( $reqType === 'page' ) {
                                $title = $parts[3];
diff --git a/includes/libs/virtualrest/RestbaseVirtualRESTService.php b/includes/libs/virtualrest/RestbaseVirtualRESTService.php
new file mode 100644 (file)
index 0000000..8fe5b92
--- /dev/null
@@ -0,0 +1,177 @@
+<?php
+/**
+ * Virtual HTTP service client for Restbase
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+/**
+ * Virtual REST service for Restbase
+ * @since 1.25
+ */
+class RestbaseVirtualRESTService extends VirtualRESTService {
+       /**
+        * Example requests:
+        *  GET /local/v1/page/{title}/html{/revision}
+        *  POST /local/v1/transform/html/to/wikitext{/title}{/revision}
+        *   * body: array( 'html' => ... )
+        *  POST /local/v1/transform/wikitext/to/html{/title}{/revision}
+        *   * body: array( 'wikitext' => ... ) or array( 'wikitext' => ..., 'bodyOnly' => true/false )
+        *
+        * @param array $params Key/value map
+        *   - url            : Restbase server URL
+        *   - domain         : Wiki domain to use
+        *   - timeout        : request timeout in seconds (optional)
+        *   - forwardCookies : cookies to forward to Restbase/Parsoid (as a Cookie
+        *                       header string) or false (optional)
+        *                       Note: forwardCookies will in the future be a boolean
+        *                       only, signifing request cookies should be forwarded
+        *                       to the service; the current state is due to the way
+        *                       VE handles this particular parameter
+        *   - HTTPProxy      : HTTP proxy to use (optional)
+        *   - parsoidCompat  : whether to parse URL as if they were meant for Parsoid
+        *                       boolean (optional)
+        */
+       public function __construct( array $params ) {
+               // set up defaults and merge them with the given params
+               $mparams = array_merge( array(
+                       'url' => 'http://localhost:7231',
+                       'domain' => 'localhost',
+                       'timeout' => 100,
+                       'forwardCookies' => false,
+                       'HTTPProxy' => null,
+                       'parsoidCompat' => false
+               ), $params );
+               // ensure the correct domain format
+               $mparams['domain'] = preg_replace(
+                               '/^(https?:\/\/)?([^\/:]+?)(\/|:\d+\/?)?$/',
+                               '$2',
+                               $mparams['domain']
+               );
+               parent::__construct( $mparams );
+       }
+
+       public function onRequests( array $reqs, Closure $idGenFunc ) {
+
+               if ( $this->params['parsoidCompat'] ) {
+                       return $this->onParsoidRequests( $reqs, $idGenFunc );
+               }
+
+               $result = array();
+               foreach ( $reqs as $key => $req ) {
+                       // replace /local/ with the current domain
+                       $req['url'] = preg_replace( '/^\/local\//', '/' . $this->params['domain'] . '/', $req['url'] );
+                       // and prefix it with the service URL
+                       $req['url'] = $this->params['url'] . $req['url'];
+                       // set the appropriate proxy, timeout and headers
+                       if ( $this->params['HTTPProxy'] ) {
+                               $req['proxy'] = $this->params['HTTPProxy'];
+                       }
+                       if ( $this->params['timeout'] != null ) {
+                               $req['reqTimeout'] = $this->params['timeout'];
+                       }
+                       if ( $this->params['forwardCookies'] ) {
+                               $req['headers']['Cookie'] = $this->params['forwardCookies'];
+                       }
+                       $result[$key] = $req;
+               }
+
+               return $result;
+
+       }
+
+       /**
+        * Remaps Parsoid requests to Restbase paths
+        */
+       public function onParsoidRequests( array $reqs, Closure $idGeneratorFunc ) {
+
+               $result = array();
+               foreach ( $reqs as $key => $req ) {
+                       $parts = explode( '/', $req['url'] );
+                       list(
+                               $targetWiki, // 'local'
+                               $version, // 'v1'
+                               $reqType // 'page' or 'transform'
+                       ) = $parts;
+                       if ( $targetWiki !== 'local' ) {
+                               throw new Exception( "Only 'local' target wiki is currently supported" );
+                       } elseif ( $reqType !== 'page' && $reqType !== 'transform' ) {
+                               throw new Exception( "Request type must be either 'page' or 'transform'" );
+                       }
+                       $req['url'] = $this->params['url'] . '/' . $this->params['domain'] . '/v1/' . $reqType . '/';
+                       if ( $reqType === 'page' ) {
+                               $title = $parts[3];
+                               if ( $parts[4] !== 'html' ) {
+                                       throw new Exception( "Only 'html' output format is currently supported" );
+                               }
+                               $req['url'] .= 'html/' . $title;
+                               if ( isset( $parts[5] ) ) {
+                                       $req['url'] .= '/' . $parts[5];
+                               } elseif ( isset( $req['query']['oldid'] ) && $req['query']['oldid'] ) {
+                                       $req['url'] .= '/' . $req['query']['oldid'];
+                                       unset( $req['query']['oldid'] );
+                               }
+                       } elseif ( $reqType === 'transform' ) {
+                               // from / to transform
+                               $req['url'] .= $parts[3] . '/to/' . $parts[5];
+                               // the title
+                               if ( isset( $parts[6] ) ) {
+                                       $req['url'] .= '/' . $parts[6];
+                               }
+                               // revision id
+                               if ( isset( $parts[7] ) ) {
+                                       $req['url'] .= '/' . $parts[7];
+                               } elseif ( isset( $req['body']['oldid'] ) && $req['body']['oldid'] ) {
+                                       $req['url'] .= '/' . $req['body']['oldid'];
+                                       unset( $req['body']['oldid'] );
+                               }
+                               if ( $parts[4] !== 'to' ) {
+                                       throw new Exception( "Part index 4 is not 'to'" );
+                               }
+                               if ( $parts[3] === 'html' & $parts[5] === 'wikitext' ) {
+                                       if ( !isset( $req['body']['html'] ) ) {
+                                               throw new Exception( "You must set an 'html' body key for this request" );
+                                       }
+                               } elseif ( $parts[3] == 'wikitext' && $parts[5] == 'html' ) {
+                                       if ( !isset( $req['body']['wikitext'] ) ) {
+                                               throw new Exception( "You must set a 'wikitext' body key for this request" );
+                                       }
+                                       if ( isset( $req['body']['body'] ) ) {
+                                               $req['body']['bodyOnly'] = $req['body']['body'];
+                                               unset( $req['body']['body'] );
+                                       }
+                               } else {
+                                       throw new Exception( "Transformation unsupported" );
+                               }
+                       }
+                       // set the appropriate proxy, timeout and headers
+                       if ( $this->params['HTTPProxy'] ) {
+                               $req['proxy'] = $this->params['HTTPProxy'];
+                       }
+                       if ( $this->params['timeout'] != null ) {
+                               $req['reqTimeout'] = $this->params['timeout'];
+                       }
+                       if ( $this->params['forwardCookies'] ) {
+                               $req['headers']['Cookie'] = $this->params['forwardCookies'];
+                       }
+                       $result[$key] = $req;
+               }
+
+               return $result;
+
+       }
+
+}