REST API initial commit
Add some of the basic REST API class hierarchies: * EntryPoint * Router * Request * Response * Handler The actual entry point file rest.php has been moved to a separate commit, so this is just an unused library and service. Bug: T221177 Change-Id: Ifca6bcb8a304e8e8b7f52b79c607bdcebf805cd1
This commit is contained in:
parent
1512a0d628
commit
3f0056a252
26 changed files with 2177 additions and 3 deletions
|
|
@ -890,6 +890,46 @@
|
|||
"type": "array",
|
||||
"description": "List of service wiring files to be loaded by the default instance of MediaWikiServices"
|
||||
},
|
||||
"RestRoutes": {
|
||||
"type": "array",
|
||||
"description": "List of route specifications to be added to the REST API",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The HTTP method name"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "An acceptable HTTP method name"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path template. This should start with an initial slash, designating the root of the REST API. Path parameters are enclosed in braces, for example /endpoint/{param}."
|
||||
},
|
||||
"factory": {
|
||||
"type": ["string", "array"],
|
||||
"description": "A factory function to be called to create the handler for this route"
|
||||
},
|
||||
"class": {
|
||||
"type": "string",
|
||||
"description": "The fully-qualified class name of the handler. This should be omitted if a factory is specified."
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"description": "The arguments passed to the handler constructor or factory"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
"description":"Registration information for other extensions",
|
||||
"type": "object",
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ class AutoLoader {
|
|||
'MediaWiki\\Linker\\' => __DIR__ . '/linker/',
|
||||
'MediaWiki\\Permissions\\' => __DIR__ . '/Permissions/',
|
||||
'MediaWiki\\Preferences\\' => __DIR__ . '/preferences/',
|
||||
'MediaWiki\\Rest\\' => __DIR__ . '/Rest/',
|
||||
'MediaWiki\\Revision\\' => __DIR__ . '/Revision/',
|
||||
'MediaWiki\\Session\\' => __DIR__ . '/session/',
|
||||
'MediaWiki\\Shell\\' => __DIR__ . '/shell/',
|
||||
|
|
|
|||
|
|
@ -193,6 +193,13 @@ $wgScript = false;
|
|||
*/
|
||||
$wgLoadScript = false;
|
||||
|
||||
/**
|
||||
* The URL path to the REST API
|
||||
* Defaults to "{$wgScriptPath}/rest.php"
|
||||
* @since 1.34
|
||||
*/
|
||||
$wgRestPath = false;
|
||||
|
||||
/**
|
||||
* The URL path of the skins directory.
|
||||
* Defaults to "{$wgResourceBasePath}/skins".
|
||||
|
|
@ -8081,10 +8088,10 @@ $wgExemptFromUserRobotsControl = null;
|
|||
/** @} */ # End robot policy }
|
||||
|
||||
/************************************************************************//**
|
||||
* @name AJAX and API
|
||||
* @name AJAX, Action API and REST API
|
||||
* Note: The AJAX entry point which this section refers to is gradually being
|
||||
* replaced by the API entry point, api.php. They are essentially equivalent.
|
||||
* Both of them are used for dynamic client-side features, via XHR.
|
||||
* replaced by the Action API entry point, api.php. They are essentially
|
||||
* equivalent. Both of them are used for dynamic client-side features, via XHR.
|
||||
* @{
|
||||
*/
|
||||
|
||||
|
|
|
|||
18
includes/Rest/CopyableStreamInterface.php
Normal file
18
includes/Rest/CopyableStreamInterface.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
/**
|
||||
* An interface for a stream with a copyToStream() function.
|
||||
*/
|
||||
interface CopyableStreamInterface extends \Psr\Http\Message\StreamInterface {
|
||||
/**
|
||||
* Copy this stream to a specified stream resource. For some streams,
|
||||
* this can be implemented without a tight loop in PHP code.
|
||||
*
|
||||
* Note that $stream is not a StreamInterface object.
|
||||
*
|
||||
* @param resource $stream Destination
|
||||
*/
|
||||
function copyToStream( $stream );
|
||||
}
|
||||
73
includes/Rest/EntryPoint.php
Normal file
73
includes/Rest/EntryPoint.php
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
use ExtensionRegistry;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use RequestContext;
|
||||
use Title;
|
||||
|
||||
class EntryPoint {
|
||||
public static function main() {
|
||||
// URL safety checks
|
||||
global $wgRequest;
|
||||
if ( !$wgRequest->checkUrlExtension() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set $wgTitle and the title in RequestContext, as in api.php
|
||||
global $wgTitle;
|
||||
$wgTitle = Title::makeTitle( NS_SPECIAL, 'Badtitle/rest.php' );
|
||||
RequestContext::getMain()->setTitle( $wgTitle );
|
||||
|
||||
$services = MediaWikiServices::getInstance();
|
||||
|
||||
$conf = $services->getMainConfig();
|
||||
$request = new RequestFromGlobals( [
|
||||
'cookiePrefix' => $conf->get( 'CookiePrefix' )
|
||||
] );
|
||||
|
||||
global $IP;
|
||||
$router = new Router(
|
||||
[ "$IP/includes/Rest/coreRoutes.json" ],
|
||||
ExtensionRegistry::getInstance()->getAttribute( 'RestRoutes' ),
|
||||
$conf->get( 'RestPath' ),
|
||||
$services->getLocalServerObjectCache(),
|
||||
new ResponseFactory
|
||||
);
|
||||
|
||||
$response = $router->execute( $request );
|
||||
|
||||
$webResponse = $wgRequest->response();
|
||||
$webResponse->header(
|
||||
'HTTP/' . $response->getProtocolVersion() . ' ' .
|
||||
$response->getStatusCode() . ' ' .
|
||||
$response->getReasonPhrase() );
|
||||
|
||||
foreach ( $response->getRawHeaderLines() as $line ) {
|
||||
$webResponse->header( $line );
|
||||
}
|
||||
|
||||
foreach ( $response->getCookies() as $cookie ) {
|
||||
$webResponse->setCookie(
|
||||
$cookie['name'],
|
||||
$cookie['value'],
|
||||
$cookie['expiry'],
|
||||
$cookie['options'] );
|
||||
}
|
||||
|
||||
$stream = $response->getBody();
|
||||
$stream->rewind();
|
||||
if ( $stream instanceof CopyableStreamInterface ) {
|
||||
$stream->copyToStream( fopen( 'php://output', 'w' ) );
|
||||
} else {
|
||||
while ( true ) {
|
||||
$buffer = $stream->read( 65536 );
|
||||
if ( $buffer === '' ) {
|
||||
break;
|
||||
}
|
||||
echo $buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
99
includes/Rest/Handler.php
Normal file
99
includes/Rest/Handler.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
abstract class Handler {
|
||||
/** @var RequestInterface */
|
||||
private $request;
|
||||
|
||||
/** @var array */
|
||||
private $config;
|
||||
|
||||
/** @var ResponseFactory */
|
||||
private $responseFactory;
|
||||
|
||||
/**
|
||||
* Initialise with dependencies from the Router. This is called after construction.
|
||||
*/
|
||||
public function init( RequestInterface $request, array $config,
|
||||
ResponseFactory $responseFactory
|
||||
) {
|
||||
$this->request = $request;
|
||||
$this->config = $config;
|
||||
$this->responseFactory = $responseFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current request. The return type declaration causes it to raise
|
||||
* a fatal error if init() has not yet been called.
|
||||
*
|
||||
* @return RequestInterface
|
||||
*/
|
||||
public function getRequest(): RequestInterface {
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configuration array for the current route. The return type
|
||||
* declaration causes it to raise a fatal error if init() has not
|
||||
* been called.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig(): array {
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ResponseFactory which can be used to generate Response objects.
|
||||
* This will raise a fatal error if init() has not been
|
||||
* called.
|
||||
*
|
||||
* @return ResponseFactory
|
||||
*/
|
||||
public function getResponseFactory(): ResponseFactory {
|
||||
return $this->responseFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* The subclass should override this to provide the maximum last modified
|
||||
* timestamp for the current request. This is called before execute() in
|
||||
* order to decide whether to send a 304.
|
||||
*
|
||||
* The timestamp can be in any format accepted by ConvertibleTimestamp, or
|
||||
* null to indicate that the timestamp is unknown.
|
||||
*
|
||||
* @return bool|string|int|float|\DateTime|null
|
||||
*/
|
||||
protected function getLastModified() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The subclass should override this to provide an ETag for the current
|
||||
* request. This is called before execute() in order to decide whether to
|
||||
* send a 304.
|
||||
*
|
||||
* See RFC 7232 § 2.3 for semantics.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getETag() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the handler. This is called after parameter validation. The
|
||||
* return value can either be a Response or any type accepted by
|
||||
* ResponseFactory::createFromReturnValue().
|
||||
*
|
||||
* To automatically construct an error response, execute() should throw a
|
||||
* RestException. Such exceptions will not be logged like a normal exception.
|
||||
*
|
||||
* If execute() throws any other kind of exception, the exception will be
|
||||
* logged and a generic 500 error page will be shown.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function execute();
|
||||
}
|
||||
15
includes/Rest/Handler/HelloHandler.php
Normal file
15
includes/Rest/Handler/HelloHandler.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest\Handler;
|
||||
|
||||
use MediaWiki\Rest\SimpleHandler;
|
||||
|
||||
/**
|
||||
* Example handler
|
||||
* @unstable
|
||||
*/
|
||||
class HelloHandler extends SimpleHandler {
|
||||
public function run( $name ) {
|
||||
return [ 'message' => "Hello, $name!" ];
|
||||
}
|
||||
}
|
||||
202
includes/Rest/HeaderContainer.php
Normal file
202
includes/Rest/HeaderContainer.php
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
/**
|
||||
* This is a container for storing headers. The header names are case-insensitive,
|
||||
* but the case is preserved for methods that return headers in bulk. The
|
||||
* header values are a comma-separated list, or equivalently, an array of strings.
|
||||
*
|
||||
* Unlike PSR-7, the container is mutable.
|
||||
*/
|
||||
class HeaderContainer {
|
||||
private $headerLists;
|
||||
private $headerLines;
|
||||
private $headerNames;
|
||||
|
||||
/**
|
||||
* Erase any existing headers and replace them with the specified
|
||||
* header arrays or values.
|
||||
*
|
||||
* @param array $headers
|
||||
*/
|
||||
public function resetHeaders( $headers = [] ) {
|
||||
$this->headerLines = [];
|
||||
$this->headerLists = [];
|
||||
$this->headerNames = [];
|
||||
foreach ( $headers as $name => $value ) {
|
||||
$this->headerNames[ strtolower( $name ) ] = $name;
|
||||
list( $valueParts, $valueLine ) = $this->convertToListAndString( $value );
|
||||
$this->headerLines[$name] = $valueLine;
|
||||
$this->headerLists[$name] = $valueParts;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take an input header value, which may either be a string or an array,
|
||||
* and convert it to an array of header values and a header line.
|
||||
*
|
||||
* The return value is an array where element 0 has the array of header
|
||||
* values, and element 1 has the header line.
|
||||
*
|
||||
* Theoretically, if the input is a string, this could parse the string
|
||||
* and split it on commas. Doing this is complicated, because some headers
|
||||
* can contain double-quoted strings containing commas. The User-Agent
|
||||
* header allows commas in comments delimited by parentheses. So it is not
|
||||
* just explode(",", $value), we would need to parse a grammar defined by
|
||||
* RFC 7231 appendix D which depends on header name.
|
||||
*
|
||||
* It's unclear how much it would help handlers to have fully spec-aware
|
||||
* HTTP header handling just to split on commas. They would probably be
|
||||
* better served by an HTTP header parsing library which provides the full
|
||||
* parse tree.
|
||||
*
|
||||
* @param string $name The header name
|
||||
* @param string|string[] $value The input header value
|
||||
* @return array
|
||||
*/
|
||||
private function convertToListAndString( $value ) {
|
||||
if ( is_array( $value ) ) {
|
||||
return [ array_values( $value ), implode( ', ', $value ) ];
|
||||
} else {
|
||||
return [ [ $value ], $value ];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or replace a header
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|string[] $value
|
||||
*/
|
||||
public function setHeader( $name, $value ) {
|
||||
list( $valueParts, $valueLine ) = $this->convertToListAndString( $value );
|
||||
$lowerName = strtolower( $name );
|
||||
$origName = $this->headerNames[$lowerName] ?? null;
|
||||
if ( $origName !== null ) {
|
||||
unset( $this->headerLines[$origName] );
|
||||
unset( $this->headerLists[$origName] );
|
||||
}
|
||||
$this->headerNames[$lowerName] = $name;
|
||||
$this->headerLines[$name] = $valueLine;
|
||||
$this->headerLists[$name] = $valueParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a header or append to an existing header
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|string[] $value
|
||||
*/
|
||||
public function addHeader( $name, $value ) {
|
||||
list( $valueParts, $valueLine ) = $this->convertToListAndString( $value );
|
||||
$lowerName = strtolower( $name );
|
||||
$origName = $this->headerNames[$lowerName] ?? null;
|
||||
if ( $origName === null ) {
|
||||
$origName = $name;
|
||||
$this->headerNames[$lowerName] = $origName;
|
||||
$this->headerLines[$origName] = $valueLine;
|
||||
$this->headerLists[$origName] = $valueParts;
|
||||
} else {
|
||||
$this->headerLines[$origName] .= ', ' . $valueLine;
|
||||
$this->headerLists[$origName] = array_merge( $this->headerLists[$origName],
|
||||
$valueParts );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a header
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function removeHeader( $name ) {
|
||||
$lowerName = strtolower( $name );
|
||||
$origName = $this->headerNames[$lowerName] ?? null;
|
||||
if ( $origName !== null ) {
|
||||
unset( $this->headerNames[$lowerName] );
|
||||
unset( $this->headerLines[$origName] );
|
||||
unset( $this->headerLists[$origName] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get header arrays indexed by original name
|
||||
*
|
||||
* @return string[][]
|
||||
*/
|
||||
public function getHeaders() {
|
||||
return $this->headerLists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the header with a particular name, or an empty array if there is no
|
||||
* such header.
|
||||
*
|
||||
* @param string $name
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeader( $name ) {
|
||||
$headerName = $this->headerNames[ strtolower( $name ) ] ?? null;
|
||||
if ( $headerName === null ) {
|
||||
return [];
|
||||
}
|
||||
return $this->headerLists[$headerName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the header exists, false otherwise
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function hasHeader( $name ) {
|
||||
return isset( $this->headerNames[ strtolower( $name ) ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the specified header concatenated into a comma-separated string.
|
||||
* If the header does not exist, an empty string is returned.
|
||||
*
|
||||
* @param string $name
|
||||
* @return string
|
||||
*/
|
||||
public function getHeaderLine( $name ) {
|
||||
$headerName = $this->headerNames[ strtolower( $name ) ] ?? null;
|
||||
if ( $headerName === null ) {
|
||||
return '';
|
||||
}
|
||||
return $this->headerLines[$headerName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all header lines
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderLines() {
|
||||
return $this->headerLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of strings of the form "Name: Value", suitable for passing
|
||||
* directly to header() to set response headers. The PHP manual describes
|
||||
* these strings as "raw HTTP headers", so we adopt that terminology.
|
||||
*
|
||||
* @return string[] Header list (integer indexed)
|
||||
*/
|
||||
public function getRawHeaderLines() {
|
||||
$lines = [];
|
||||
foreach ( $this->headerNames as $lowerName => $name ) {
|
||||
if ( $lowerName === 'set-cookie' ) {
|
||||
// As noted by RFC 7230 section 3.2.2, Set-Cookie is the only
|
||||
// header for which multiple values cannot be concatenated into
|
||||
// a single comma-separated line.
|
||||
foreach ( $this->headerLists[$name] as $value ) {
|
||||
$lines[] = "$name: $value";
|
||||
}
|
||||
} else {
|
||||
$lines[] = "$name: " . $this->headerLines[$name];
|
||||
}
|
||||
}
|
||||
return $lines;
|
||||
}
|
||||
}
|
||||
14
includes/Rest/HttpException.php
Normal file
14
includes/Rest/HttpException.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
/**
|
||||
* This is the base exception class for non-fatal exceptions thrown from REST
|
||||
* handlers. The exception is not logged, it is merely converted to an
|
||||
* error response.
|
||||
*/
|
||||
class HttpException extends \Exception {
|
||||
public function __construct( $message, $code = 500 ) {
|
||||
parent::__construct( $message, $code );
|
||||
}
|
||||
}
|
||||
9
includes/Rest/JsonEncodingException.php
Normal file
9
includes/Rest/JsonEncodingException.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
class JsonEncodingException extends \RuntimeException {
|
||||
public function __construct( $message, $code ) {
|
||||
parent::__construct( "JSON encoding error: $message", $code );
|
||||
}
|
||||
}
|
||||
21
includes/Rest/PathTemplateMatcher/PathConflict.php
Normal file
21
includes/Rest/PathTemplateMatcher/PathConflict.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest\PathTemplateMatcher;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PathConflict extends Exception {
|
||||
public $newTemplate;
|
||||
public $newUserData;
|
||||
public $existingTemplate;
|
||||
public $existingUserData;
|
||||
|
||||
public function __construct( $template, $userData, $existingNode ) {
|
||||
$this->newTemplate = $template;
|
||||
$this->newUserData = $userData;
|
||||
$this->existingTemplate = $existingNode['template'];
|
||||
$this->existingUserData = $existingNode['userData'];
|
||||
parent::__construct( "Unable to add path template \"$template\" since it conflicts " .
|
||||
"with the existing template \"{$this->existingTemplate}\"" );
|
||||
}
|
||||
}
|
||||
221
includes/Rest/PathTemplateMatcher/PathMatcher.php
Normal file
221
includes/Rest/PathTemplateMatcher/PathMatcher.php
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest\PathTemplateMatcher;
|
||||
|
||||
/**
|
||||
* A tree-based path routing algorithm.
|
||||
*
|
||||
* This container builds defined routing templates into a tree, allowing
|
||||
* paths to be efficiently matched against all templates. The match time is
|
||||
* independent of the number of registered path templates.
|
||||
*
|
||||
* Efficient matching comes at the cost of a potentially significant setup time.
|
||||
* We measured ~10ms for 1000 templates. Using getCacheData() and
|
||||
* newFromCache(), this setup time may be amortized over multiple requests.
|
||||
*/
|
||||
class PathMatcher {
|
||||
/**
|
||||
* An array of trees indexed by the number of path components in the input.
|
||||
*
|
||||
* A tree node consists of an associative array in which the key is a match
|
||||
* specifier string, and the value is another node. A leaf node, which is
|
||||
* identifiable by its fixed depth in the tree, consists of an associative
|
||||
* array with the following keys:
|
||||
* - template: The path template string
|
||||
* - paramNames: A list of parameter names extracted from the template
|
||||
* - userData: The user data supplied to add()
|
||||
*
|
||||
* A match specifier string may be either "*", which matches any path
|
||||
* component, or a literal string prefixed with "=", which matches the
|
||||
* specified deprefixed string literal.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $treesByLength = [];
|
||||
|
||||
/**
|
||||
* Create a PathMatcher from cache data
|
||||
*
|
||||
* @param array $data The data array previously returned by getCacheData()
|
||||
* @return PathMatcher
|
||||
*/
|
||||
public static function newFromCache( $data ) {
|
||||
$matcher = new self;
|
||||
$matcher->treesByLength = $data;
|
||||
return $matcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a data array for later use by newFromCache().
|
||||
*
|
||||
* The internal format is private to PathMatcher, but note that it includes
|
||||
* any data passed as $userData to add(). The array returned will be
|
||||
* serializable as long as all $userData values are serializable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCacheData() {
|
||||
return $this->treesByLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a path template component is a parameter
|
||||
*
|
||||
* @param string $part
|
||||
* @return bool
|
||||
*/
|
||||
private function isParam( $part ) {
|
||||
$partLength = strlen( $part );
|
||||
return $partLength > 2 && $part[0] === '{' && $part[$partLength - 1] === '}';
|
||||
}
|
||||
|
||||
/**
|
||||
* If a path template component is a parameter, return the parameter name.
|
||||
* Otherwise, return false.
|
||||
*
|
||||
* @param string $part
|
||||
* @return string|false
|
||||
*/
|
||||
private function getParamName( $part ) {
|
||||
if ( $this->isParam( $part ) ) {
|
||||
return substr( $part, 1, -1 );
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively search the match tree, checking whether the proposed path
|
||||
* template, passed as an array of component parts, can be added to the
|
||||
* matcher without ambiguity.
|
||||
*
|
||||
* Ambiguity means that a path exists which matches multiple templates.
|
||||
*
|
||||
* The function calls itself recursively, incrementing $index so as to
|
||||
* ignore a prefix of the input, in order to check deeper parts of the
|
||||
* match tree.
|
||||
*
|
||||
* If a conflict is discovered, the conflicting leaf node is returned.
|
||||
* Otherwise, false is returned.
|
||||
*
|
||||
* @param array $node The tree node to check against
|
||||
* @param string[] $parts The array of path template parts
|
||||
* @param int $index The current index into $parts
|
||||
* @return array|false
|
||||
*/
|
||||
private function findConflict( $node, $parts, $index = 0 ) {
|
||||
if ( $index >= count( $parts ) ) {
|
||||
// If we reached the leaf node then a conflict is detected
|
||||
return $node;
|
||||
}
|
||||
$part = $parts[$index];
|
||||
$result = false;
|
||||
if ( $this->isParam( $part ) ) {
|
||||
foreach ( $node as $key => $childNode ) {
|
||||
$result = $this->findConflict( $childNode, $parts, $index + 1 );
|
||||
if ( $result !== false ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ( isset( $node["=$part"] ) ) {
|
||||
$result = $this->findConflict( $node["=$part"], $parts, $index + 1 );
|
||||
}
|
||||
if ( $result === false && isset( $node['*'] ) ) {
|
||||
$result = $this->findConflict( $node['*'], $parts, $index + 1 );
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a template to the matcher.
|
||||
*
|
||||
* The path template consists of components separated by "/". Each component
|
||||
* may be either a parameter of the form {paramName}, or a literal string.
|
||||
* A parameter matches any input path component, whereas a literal string
|
||||
* matches itself.
|
||||
*
|
||||
* Path templates must not conflict with each other, that is, any input
|
||||
* path must match at most one path template. If a path template conflicts
|
||||
* with another already registered, this function throws a PathConflict
|
||||
* exception.
|
||||
*
|
||||
* @param string $template The path template
|
||||
* @param mixed $userData User data used to identify the matched route to
|
||||
* the caller of match()
|
||||
* @throws PathConflict
|
||||
*/
|
||||
public function add( $template, $userData ) {
|
||||
$parts = explode( '/', $template );
|
||||
$length = count( $parts );
|
||||
if ( !isset( $this->treesByLength[$length] ) ) {
|
||||
$this->treesByLength[$length] = [];
|
||||
}
|
||||
$tree =& $this->treesByLength[$length];
|
||||
$conflict = $this->findConflict( $tree, $parts );
|
||||
if ( $conflict !== false ) {
|
||||
throw new PathConflict( $template, $userData, $conflict );
|
||||
}
|
||||
|
||||
$params = [];
|
||||
foreach ( $parts as $index => $part ) {
|
||||
$paramName = $this->getParamName( $part );
|
||||
if ( $paramName !== false ) {
|
||||
$params[] = $paramName;
|
||||
$key = '*';
|
||||
} else {
|
||||
$key = "=$part";
|
||||
}
|
||||
if ( $index === $length - 1 ) {
|
||||
$tree[$key] = [
|
||||
'template' => $template,
|
||||
'paramNames' => $params,
|
||||
'userData' => $userData
|
||||
];
|
||||
} elseif ( !isset( $tree[$key] ) ) {
|
||||
$tree[$key] = [];
|
||||
}
|
||||
$tree =& $tree[$key];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Match a path against the current match trees.
|
||||
*
|
||||
* If the path matches a previously added path template, an array will be
|
||||
* returned with the following keys:
|
||||
* - params: An array mapping parameter names to their detected values
|
||||
* - userData: The user data passed to add(), which identifies the route
|
||||
*
|
||||
* If the path does not match any template, false is returned.
|
||||
*
|
||||
* @param string $path
|
||||
* @return array|false
|
||||
*/
|
||||
public function match( $path ) {
|
||||
$parts = explode( '/', $path );
|
||||
$length = count( $parts );
|
||||
if ( !isset( $this->treesByLength[$length] ) ) {
|
||||
return false;
|
||||
}
|
||||
$node = $this->treesByLength[$length];
|
||||
|
||||
$paramValues = [];
|
||||
foreach ( $parts as $part ) {
|
||||
if ( isset( $node["=$part"] ) ) {
|
||||
$node = $node["=$part"];
|
||||
} elseif ( isset( $node['*'] ) ) {
|
||||
$node = $node['*'];
|
||||
$paramValues[] = $part;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'params' => array_combine( $node['paramNames'], $paramValues ),
|
||||
'userData' => $node['userData']
|
||||
];
|
||||
}
|
||||
}
|
||||
115
includes/Rest/RequestBase.php
Normal file
115
includes/Rest/RequestBase.php
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
/**
|
||||
* Shared code between RequestData and RequestFromGlobals
|
||||
*/
|
||||
abstract class RequestBase implements RequestInterface {
|
||||
/**
|
||||
* @var HeaderContainer|null
|
||||
*/
|
||||
private $headerCollection;
|
||||
|
||||
/** @var array */
|
||||
private $attributes = [];
|
||||
|
||||
/** @var string */
|
||||
private $cookiePrefix;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param string $cookiePrefix
|
||||
*/
|
||||
protected function __construct( $cookiePrefix ) {
|
||||
$this->cookiePrefix = $cookiePrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this in the implementation class if lazy initialisation of
|
||||
* header values is desired. It should call setHeaders().
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function initHeaders() {
|
||||
}
|
||||
|
||||
public function __clone() {
|
||||
if ( $this->headerCollection !== null ) {
|
||||
$this->headerCollection = clone $this->headerCollection;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase any existing headers and replace them with the specified header
|
||||
* lines.
|
||||
*
|
||||
* Call this either from the constructor or from initHeaders() of the
|
||||
* implementing class.
|
||||
*
|
||||
* @internal
|
||||
* @param string[] $headers The header lines
|
||||
*/
|
||||
protected function setHeaders( $headers ) {
|
||||
$this->headerCollection = new HeaderContainer;
|
||||
$this->headerCollection->resetHeaders( $headers );
|
||||
}
|
||||
|
||||
public function getHeaders() {
|
||||
if ( $this->headerCollection === null ) {
|
||||
$this->initHeaders();
|
||||
}
|
||||
return $this->headerCollection->getHeaders();
|
||||
}
|
||||
|
||||
public function getHeader( $name ) {
|
||||
if ( $this->headerCollection === null ) {
|
||||
$this->initHeaders();
|
||||
}
|
||||
return $this->headerCollection->getHeader( $name );
|
||||
}
|
||||
|
||||
public function hasHeader( $name ) {
|
||||
if ( $this->headerCollection === null ) {
|
||||
$this->initHeaders();
|
||||
}
|
||||
return $this->headerCollection->hasHeader( $name );
|
||||
}
|
||||
|
||||
public function getHeaderLine( $name ) {
|
||||
if ( $this->headerCollection === null ) {
|
||||
$this->initHeaders();
|
||||
}
|
||||
return $this->headerCollection->getHeaderLine( $name );
|
||||
}
|
||||
|
||||
public function setAttributes( $attributes ) {
|
||||
$this->attributes = $attributes;
|
||||
}
|
||||
|
||||
public function getAttributes() {
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
public function getAttribute( $name, $default = null ) {
|
||||
if ( array_key_exists( $name, $this->attributes ) ) {
|
||||
return $this->attributes[$name];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
public function getCookiePrefix() {
|
||||
return $this->cookiePrefix;
|
||||
}
|
||||
|
||||
public function getCookie( $name, $default = null ) {
|
||||
$cookies = $this->getCookieParams();
|
||||
$prefixedName = $this->getCookiePrefix() . $name;
|
||||
if ( array_key_exists( $prefixedName, $cookies ) ) {
|
||||
return $cookies[$prefixedName];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
}
|
||||
104
includes/Rest/RequestData.php
Normal file
104
includes/Rest/RequestData.php
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* This is a Request class that allows data to be injected, for the purposes
|
||||
* of testing or internal requests.
|
||||
*/
|
||||
class RequestData extends RequestBase {
|
||||
private $method;
|
||||
|
||||
/** @var UriInterface */
|
||||
private $uri;
|
||||
|
||||
private $protocolVersion;
|
||||
|
||||
/** @var StreamInterface */
|
||||
private $body;
|
||||
|
||||
private $serverParams;
|
||||
|
||||
private $cookieParams;
|
||||
|
||||
private $queryParams;
|
||||
|
||||
/** @var UploadedFileInterface[] */
|
||||
private $uploadedFiles;
|
||||
|
||||
private $postParams;
|
||||
|
||||
/**
|
||||
* Construct a RequestData from an array of parameters.
|
||||
*
|
||||
* @param array $params An associative array of parameters. All parameters
|
||||
* have defaults. Parameters are:
|
||||
* - method: The HTTP method
|
||||
* - uri: The URI
|
||||
* - protocolVersion: The HTTP protocol version number
|
||||
* - bodyContents: A string giving the request body
|
||||
* - serverParams: Equivalent to $_SERVER
|
||||
* - cookieParams: Equivalent to $_COOKIE
|
||||
* - queryParams: Equivalent to $_GET
|
||||
* - uploadedFiles: An array of objects implementing UploadedFileInterface
|
||||
* - postParams: Equivalent to $_POST
|
||||
* - attributes: The attributes, usually from path template parameters
|
||||
* - headers: An array with the the key being the header name
|
||||
* - cookiePrefix: A prefix to add to cookie names in getCookie()
|
||||
*/
|
||||
public function __construct( $params = [] ) {
|
||||
$this->method = $params['method'] ?? 'GET';
|
||||
$this->uri = $params['uri'] ?? new Uri;
|
||||
$this->protocolVersion = $params['protocolVersion'] ?? '1.1';
|
||||
$this->body = new StringStream( $params['bodyContents'] ?? '' );
|
||||
$this->serverParams = $params['serverParams'] ?? [];
|
||||
$this->cookieParams = $params['cookieParams'] ?? [];
|
||||
$this->queryParams = $params['queryParams'] ?? [];
|
||||
$this->uploadedFiles = $params['uploadedFiles'] ?? [];
|
||||
$this->postParams = $params['postParams'] ?? [];
|
||||
$this->setAttributes( $params['attributes'] ?? [] );
|
||||
$this->setHeaders( $params['headers'] ?? [] );
|
||||
parent::__construct( $params['cookiePrefix'] ?? '' );
|
||||
}
|
||||
|
||||
public function getMethod() {
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
public function getUri() {
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
public function getProtocolVersion() {
|
||||
return $this->protocolVersion;
|
||||
}
|
||||
|
||||
public function getBody() {
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
public function getServerParams() {
|
||||
return $this->serverParams;
|
||||
}
|
||||
|
||||
public function getCookieParams() {
|
||||
return $this->cookieParams;
|
||||
}
|
||||
|
||||
public function getQueryParams() {
|
||||
return $this->queryParams;
|
||||
}
|
||||
|
||||
public function getUploadedFiles() {
|
||||
return $this->uploadedFiles;
|
||||
}
|
||||
|
||||
public function getPostParams() {
|
||||
return $this->postParams;
|
||||
}
|
||||
}
|
||||
101
includes/Rest/RequestFromGlobals.php
Normal file
101
includes/Rest/RequestFromGlobals.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
use GuzzleHttp\Psr7\LazyOpenStream;
|
||||
use GuzzleHttp\Psr7\ServerRequest;
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
|
||||
// phpcs:disable MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals
|
||||
|
||||
/**
|
||||
* This is a request class that gets data directly from the superglobals and
|
||||
* other global PHP state, notably php://input.
|
||||
*/
|
||||
class RequestFromGlobals extends RequestBase {
|
||||
private $uri;
|
||||
private $protocol;
|
||||
private $uploadedFiles;
|
||||
|
||||
/**
|
||||
* @param array $params Associative array of parameters:
|
||||
* - cookiePrefix: The prefix for cookie names used by getCookie()
|
||||
*/
|
||||
public function __construct( $params = [] ) {
|
||||
parent::__construct( $params['cookiePrefix'] ?? '' );
|
||||
}
|
||||
|
||||
// RequestInterface
|
||||
|
||||
public function getMethod() {
|
||||
return $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
}
|
||||
|
||||
public function getUri() {
|
||||
if ( $this->uri === null ) {
|
||||
$this->uri = new Uri( \WebRequest::getGlobalRequestURL() );
|
||||
}
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
// MessageInterface
|
||||
|
||||
public function getProtocolVersion() {
|
||||
if ( $this->protocol === null ) {
|
||||
$serverProtocol = $_SERVER['SERVER_PROTOCOL'] ?? '';
|
||||
$prefixLength = strlen( 'HTTP/' );
|
||||
if ( strncmp( $serverProtocol, 'HTTP/', $prefixLength ) === 0 ) {
|
||||
$this->protocol = substr( $serverProtocol, $prefixLength );
|
||||
} else {
|
||||
$this->protocol = '1.1';
|
||||
}
|
||||
}
|
||||
return $this->protocol;
|
||||
}
|
||||
|
||||
protected function initHeaders() {
|
||||
if ( function_exists( 'apache_request_headers' ) ) {
|
||||
$this->setHeaders( apache_request_headers() );
|
||||
} else {
|
||||
$headers = [];
|
||||
foreach ( $_SERVER as $name => $value ) {
|
||||
if ( substr( $name, 0, 5 ) === 'HTTP_' ) {
|
||||
$name = strtolower( str_replace( '_', '-', substr( $name, 5 ) ) );
|
||||
$headers[$name] = $value;
|
||||
} elseif ( $name === 'CONTENT_LENGTH' ) {
|
||||
$headers['content-length'] = $value;
|
||||
}
|
||||
}
|
||||
$this->setHeaders( $headers );
|
||||
}
|
||||
}
|
||||
|
||||
public function getBody() {
|
||||
return new LazyOpenStream( 'php://input', 'r' );
|
||||
}
|
||||
|
||||
// ServerRequestInterface
|
||||
|
||||
public function getServerParams() {
|
||||
return $_SERVER;
|
||||
}
|
||||
|
||||
public function getCookieParams() {
|
||||
return $_COOKIE;
|
||||
}
|
||||
|
||||
public function getQueryParams() {
|
||||
return $_GET;
|
||||
}
|
||||
|
||||
public function getUploadedFiles() {
|
||||
if ( $this->uploadedFiles === null ) {
|
||||
$this->uploadedFiles = ServerRequest::normalizeFiles( $_FILES );
|
||||
}
|
||||
return $this->uploadedFiles;
|
||||
}
|
||||
|
||||
public function getPostParams() {
|
||||
return $_POST;
|
||||
}
|
||||
}
|
||||
276
includes/Rest/RequestInterface.php
Normal file
276
includes/Rest/RequestInterface.php
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (c) 2019 Wikimedia Foundation.
|
||||
*
|
||||
* This file is partly derived from PSR-7, which requires the following copyright notice:
|
||||
*
|
||||
* Copyright (c) 2014 PHP Framework Interoperability Group
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* A request interface similar to PSR-7's ServerRequestInterface
|
||||
*/
|
||||
interface RequestInterface {
|
||||
// RequestInterface
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP method of the request.
|
||||
*
|
||||
* @return string Returns the request method.
|
||||
*/
|
||||
function getMethod();
|
||||
|
||||
/**
|
||||
* Retrieves the URI instance.
|
||||
*
|
||||
* This method MUST return a UriInterface instance.
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc3986#section-4.3
|
||||
* @return UriInterface Returns a UriInterface instance
|
||||
* representing the URI of the request.
|
||||
*/
|
||||
function getUri();
|
||||
|
||||
// MessageInterface
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP protocol version as a string.
|
||||
*
|
||||
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
|
||||
*
|
||||
* @return string HTTP protocol version.
|
||||
*/
|
||||
function getProtocolVersion();
|
||||
|
||||
/**
|
||||
* Retrieves all message header values.
|
||||
*
|
||||
* The keys represent the header name as it will be sent over the wire, and
|
||||
* each value is an array of strings associated with the header.
|
||||
*
|
||||
* // Represent the headers as a string
|
||||
* foreach ($message->getHeaders() as $name => $values) {
|
||||
* echo $name . ": " . implode(", ", $values);
|
||||
* }
|
||||
*
|
||||
* // Emit headers iteratively:
|
||||
* foreach ($message->getHeaders() as $name => $values) {
|
||||
* foreach ($values as $value) {
|
||||
* header(sprintf('%s: %s', $name, $value), false);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* While header names are not case-sensitive, getHeaders() will preserve the
|
||||
* exact case in which headers were originally specified.
|
||||
*
|
||||
* A single header value may be a string containing a comma-separated list.
|
||||
* Lists will not necessarily be split into arrays. See the comment on
|
||||
* HeaderContainer::convertToListAndString().
|
||||
*
|
||||
* @return string[][] Returns an associative array of the message's headers. Each
|
||||
* key MUST be a header name, and each value MUST be an array of strings
|
||||
* for that header.
|
||||
*/
|
||||
function getHeaders();
|
||||
|
||||
/**
|
||||
* Retrieves a message header value by the given case-insensitive name.
|
||||
*
|
||||
* This method returns an array of all the header values of the given
|
||||
* case-insensitive header name.
|
||||
*
|
||||
* If the header does not appear in the message, this method MUST return an
|
||||
* empty array.
|
||||
*
|
||||
* A single header value may be a string containing a comma-separated list.
|
||||
* Lists will not necessarily be split into arrays. See the comment on
|
||||
* HeaderContainer::convertToListAndString().
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return string[] An array of string values as provided for the given
|
||||
* header. If the header does not appear in the message, this method MUST
|
||||
* return an empty array.
|
||||
*/
|
||||
function getHeader( $name );
|
||||
|
||||
/**
|
||||
* Checks if a header exists by the given case-insensitive name.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return bool Returns true if any header names match the given header
|
||||
* name using a case-insensitive string comparison. Returns false if
|
||||
* no matching header name is found in the message.
|
||||
*/
|
||||
function hasHeader( $name );
|
||||
|
||||
/**
|
||||
* Retrieves a comma-separated string of the values for a single header.
|
||||
*
|
||||
* This method returns all of the header values of the given
|
||||
* case-insensitive header name as a string concatenated together using
|
||||
* a comma.
|
||||
*
|
||||
* NOTE: Not all header values may be appropriately represented using
|
||||
* comma concatenation. For such headers, use getHeader() instead
|
||||
* and supply your own delimiter when concatenating.
|
||||
*
|
||||
* If the header does not appear in the message, this method MUST return
|
||||
* an empty string.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return string A string of values as provided for the given header
|
||||
* concatenated together using a comma. If the header does not appear in
|
||||
* the message, this method MUST return an empty string.
|
||||
*/
|
||||
function getHeaderLine( $name );
|
||||
|
||||
/**
|
||||
* Gets the body of the message.
|
||||
*
|
||||
* @return StreamInterface Returns the body as a stream.
|
||||
*/
|
||||
function getBody();
|
||||
|
||||
// ServerRequestInterface
|
||||
|
||||
/**
|
||||
* Retrieve server parameters.
|
||||
*
|
||||
* Retrieves data related to the incoming request environment,
|
||||
* typically derived from PHP's $_SERVER superglobal. The data IS NOT
|
||||
* REQUIRED to originate from $_SERVER.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function getServerParams();
|
||||
|
||||
/**
|
||||
* Retrieve cookies.
|
||||
*
|
||||
* Retrieves cookies sent by the client to the server.
|
||||
*
|
||||
* The data MUST be compatible with the structure of the $_COOKIE
|
||||
* superglobal.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function getCookieParams();
|
||||
|
||||
/**
|
||||
* Retrieve query string arguments.
|
||||
*
|
||||
* Retrieves the deserialized query string arguments, if any.
|
||||
*
|
||||
* Note: the query params might not be in sync with the URI or server
|
||||
* params. If you need to ensure you are only getting the original
|
||||
* values, you may need to parse the query string from `getUri()->getQuery()`
|
||||
* or from the `QUERY_STRING` server param.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function getQueryParams();
|
||||
|
||||
/**
|
||||
* Retrieve normalized file upload data.
|
||||
*
|
||||
* This method returns upload metadata in a normalized tree, with each leaf
|
||||
* an instance of Psr\Http\Message\UploadedFileInterface.
|
||||
*
|
||||
* @return array An array tree of UploadedFileInterface instances; an empty
|
||||
* array MUST be returned if no data is present.
|
||||
*/
|
||||
function getUploadedFiles();
|
||||
|
||||
/**
|
||||
* Retrieve attributes derived from the request.
|
||||
*
|
||||
* The request "attributes" may be used to allow injection of any
|
||||
* parameters derived from the request: e.g., the results of path
|
||||
* match operations; the results of decrypting cookies; the results of
|
||||
* deserializing non-form-encoded message bodies; etc. Attributes
|
||||
* will be application and request specific, and CAN be mutable.
|
||||
*
|
||||
* @return array Attributes derived from the request.
|
||||
*/
|
||||
function getAttributes();
|
||||
|
||||
/**
|
||||
* Retrieve a single derived request attribute.
|
||||
*
|
||||
* Retrieves a single derived request attribute as described in
|
||||
* getAttributes(). If the attribute has not been previously set, returns
|
||||
* the default value as provided.
|
||||
*
|
||||
* This method obviates the need for a hasAttribute() method, as it allows
|
||||
* specifying a default value to return if the attribute is not found.
|
||||
*
|
||||
* @see getAttributes()
|
||||
* @param string $name The attribute name.
|
||||
* @param mixed|null $default Default value to return if the attribute does not exist.
|
||||
* @return mixed
|
||||
*/
|
||||
function getAttribute( $name, $default = null );
|
||||
|
||||
// MediaWiki extensions to PSR-7
|
||||
|
||||
/**
|
||||
* Erase all attributes from the object and set the attribute array to the
|
||||
* specified value
|
||||
*
|
||||
* @param mixed[] $attributes
|
||||
*/
|
||||
function setAttributes( $attributes );
|
||||
|
||||
/**
|
||||
* Get the current cookie prefix
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getCookiePrefix();
|
||||
|
||||
/**
|
||||
* Add the cookie prefix to a specified cookie name and get the value of
|
||||
* the resulting prefixed cookie. If the cookie does not exist, $default
|
||||
* is returned.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed|null $default
|
||||
* @return mixed The cookie value as a string, or $default
|
||||
*/
|
||||
function getCookie( $name, $default = null );
|
||||
|
||||
/**
|
||||
* Retrieve POST form parameters.
|
||||
*
|
||||
* This will return an array of parameters in the format of $_POST.
|
||||
*
|
||||
* @return array The deserialized POST parameters
|
||||
*/
|
||||
function getPostParams();
|
||||
}
|
||||
112
includes/Rest/Response.php
Normal file
112
includes/Rest/Response.php
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
use HttpStatus;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
class Response implements ResponseInterface {
|
||||
/** @var int */
|
||||
private $statusCode = 200;
|
||||
|
||||
/** @var string */
|
||||
private $reasonPhrase = 'OK';
|
||||
|
||||
/** @var string */
|
||||
private $protocolVersion = '1.1';
|
||||
|
||||
/** @var StreamInterface */
|
||||
private $body;
|
||||
|
||||
/** @var HeaderContainer */
|
||||
private $headerContainer;
|
||||
|
||||
/** @var array */
|
||||
private $cookies = [];
|
||||
|
||||
/**
|
||||
* @internal Use ResponseFactory
|
||||
* @param string $bodyContents
|
||||
*/
|
||||
public function __construct( $bodyContents = '' ) {
|
||||
$this->body = new StringStream( $bodyContents );
|
||||
$this->headerContainer = new HeaderContainer;
|
||||
}
|
||||
|
||||
public function getStatusCode() {
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
public function getReasonPhrase() {
|
||||
return $this->reasonPhrase;
|
||||
}
|
||||
|
||||
public function setStatus( $code, $reasonPhrase = '' ) {
|
||||
$this->statusCode = $code;
|
||||
if ( $reasonPhrase === '' ) {
|
||||
$reasonPhrase = HttpStatus::getMessage( $code ) ?? '';
|
||||
}
|
||||
$this->reasonPhrase = $reasonPhrase;
|
||||
}
|
||||
|
||||
public function getProtocolVersion() {
|
||||
return $this->protocolVersion;
|
||||
}
|
||||
|
||||
public function getHeaders() {
|
||||
return $this->headerContainer->getHeaders();
|
||||
}
|
||||
|
||||
public function hasHeader( $name ) {
|
||||
return $this->headerContainer->hasHeader( $name );
|
||||
}
|
||||
|
||||
public function getHeader( $name ) {
|
||||
return $this->headerContainer->getHeader( $name );
|
||||
}
|
||||
|
||||
public function getHeaderLine( $name ) {
|
||||
return $this->headerContainer->getHeaderLine( $name );
|
||||
}
|
||||
|
||||
public function getBody() {
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
public function setProtocolVersion( $version ) {
|
||||
$this->protocolVersion = $version;
|
||||
}
|
||||
|
||||
public function setHeader( $name, $value ) {
|
||||
$this->headerContainer->setHeader( $name, $value );
|
||||
}
|
||||
|
||||
public function addHeader( $name, $value ) {
|
||||
$this->headerContainer->addHeader( $name, $value );
|
||||
}
|
||||
|
||||
public function removeHeader( $name ) {
|
||||
$this->headerContainer->removeHeader( $name );
|
||||
}
|
||||
|
||||
public function setBody( StreamInterface $body ) {
|
||||
$this->body = $body;
|
||||
}
|
||||
|
||||
public function getRawHeaderLines() {
|
||||
return $this->headerContainer->getRawHeaderLines();
|
||||
}
|
||||
|
||||
public function setCookie( $name, $value, $expire = 0, $options = [] ) {
|
||||
$this->cookies[] = [
|
||||
'name' => $name,
|
||||
'value' => $value,
|
||||
'expire' => $expire,
|
||||
'options' => $options
|
||||
];
|
||||
}
|
||||
|
||||
public function getCookies() {
|
||||
return $this->cookies;
|
||||
}
|
||||
}
|
||||
52
includes/Rest/ResponseFactory.php
Normal file
52
includes/Rest/ResponseFactory.php
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
/**
|
||||
* MOCK UP ONLY
|
||||
* @unstable
|
||||
*/
|
||||
class ResponseFactory {
|
||||
const CT_PLAIN = 'text/plain; charset=utf-8';
|
||||
const CT_JSON = 'application/json';
|
||||
|
||||
public function create404() {
|
||||
$response = new Response( 'Path not found' );
|
||||
$response->setStatus( 404 );
|
||||
$response->setHeader( 'Content-Type', self::CT_PLAIN );
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function create500( $message ) {
|
||||
$response = new Response( $message );
|
||||
$response->setStatus( 500 );
|
||||
$response->setHeader( 'Content-Type', self::CT_PLAIN );
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function createFromException( \Exception $exception ) {
|
||||
if ( $exception instanceof HttpException ) {
|
||||
$response = new Response( $exception->getMessage() );
|
||||
$response->setStatus( $exception->getCode() );
|
||||
$response->setHeader( 'Content-Type', self::CT_PLAIN );
|
||||
return $response;
|
||||
} else {
|
||||
return $this->create500( "Error: exception of type " . gettype( $exception ) );
|
||||
}
|
||||
}
|
||||
|
||||
public function createFromReturnValue( $value ) {
|
||||
if ( is_scalar( $value )
|
||||
|| ( is_object( $value ) && method_exists( $value, '__toString' ) )
|
||||
) {
|
||||
$value = [ 'value' => (string)$value ];
|
||||
}
|
||||
$json = json_encode( $value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
|
||||
if ( $json === false ) {
|
||||
throw new JsonEncodingException( json_last_error_msg(), json_last_error() );
|
||||
}
|
||||
$response = new Response( $json );
|
||||
$response->setHeader( 'Content-Type', self::CT_JSON );
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
277
includes/Rest/ResponseInterface.php
Normal file
277
includes/Rest/ResponseInterface.php
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (c) 2019 Wikimedia Foundation.
|
||||
*
|
||||
* This file is partly derived from PSR-7, which requires the following copyright notice:
|
||||
*
|
||||
* Copyright (c) 2014 PHP Framework Interoperability Group
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* An interface similar to PSR-7's ResponseInterface, the primary difference
|
||||
* being that it is mutable.
|
||||
*/
|
||||
interface ResponseInterface {
|
||||
// ResponseInterface
|
||||
|
||||
/**
|
||||
* Gets the response status code.
|
||||
*
|
||||
* The status code is a 3-digit integer result code of the server's attempt
|
||||
* to understand and satisfy the request.
|
||||
*
|
||||
* @return int Status code.
|
||||
*/
|
||||
function getStatusCode();
|
||||
|
||||
/**
|
||||
* Gets the response reason phrase associated with the status code.
|
||||
*
|
||||
* Because a reason phrase is not a required element in a response
|
||||
* status line, the reason phrase value MAY be empty. Implementations MAY
|
||||
* choose to return the default RFC 7231 recommended reason phrase (or those
|
||||
* listed in the IANA HTTP Status Code Registry) for the response's
|
||||
* status code.
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc7231#section-6
|
||||
* @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||
* @return string Reason phrase; must return an empty string if none present.
|
||||
*/
|
||||
function getReasonPhrase();
|
||||
|
||||
// ResponseInterface mutation
|
||||
|
||||
/**
|
||||
* Set the status code and, optionally, reason phrase.
|
||||
*
|
||||
* If no reason phrase is specified, implementations MAY choose to default
|
||||
* to the RFC 7231 or IANA recommended reason phrase for the response's
|
||||
* status code.
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc7231#section-6
|
||||
* @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||
* @param int $code The 3-digit integer result code to set.
|
||||
* @param string $reasonPhrase The reason phrase to use with the
|
||||
* provided status code; if none is provided, implementations MAY
|
||||
* use the defaults as suggested in the HTTP specification.
|
||||
* @throws \InvalidArgumentException For invalid status code arguments.
|
||||
*/
|
||||
function setStatus( $code, $reasonPhrase = '' );
|
||||
|
||||
// MessageInterface
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP protocol version as a string.
|
||||
*
|
||||
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
|
||||
*
|
||||
* @return string HTTP protocol version.
|
||||
*/
|
||||
function getProtocolVersion();
|
||||
|
||||
/**
|
||||
* Retrieves all message header values.
|
||||
*
|
||||
* The keys represent the header name as it will be sent over the wire, and
|
||||
* each value is an array of strings associated with the header.
|
||||
*
|
||||
* // Represent the headers as a string
|
||||
* foreach ($message->getHeaders() as $name => $values) {
|
||||
* echo $name . ': ' . implode(', ', $values);
|
||||
* }
|
||||
*
|
||||
* // Emit headers iteratively:
|
||||
* foreach ($message->getHeaders() as $name => $values) {
|
||||
* foreach ($values as $value) {
|
||||
* header(sprintf('%s: %s', $name, $value), false);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* While header names are not case-sensitive, getHeaders() will preserve the
|
||||
* exact case in which headers were originally specified.
|
||||
*
|
||||
* @return string[][] Returns an associative array of the message's headers.
|
||||
* Each key MUST be a header name, and each value MUST be an array of
|
||||
* strings for that header.
|
||||
*/
|
||||
function getHeaders();
|
||||
|
||||
/**
|
||||
* Checks if a header exists by the given case-insensitive name.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return bool Returns true if any header names match the given header
|
||||
* name using a case-insensitive string comparison. Returns false if
|
||||
* no matching header name is found in the message.
|
||||
*/
|
||||
function hasHeader( $name );
|
||||
|
||||
/**
|
||||
* Retrieves a message header value by the given case-insensitive name.
|
||||
*
|
||||
* This method returns an array of all the header values of the given
|
||||
* case-insensitive header name.
|
||||
*
|
||||
* If the header does not appear in the message, this method MUST return an
|
||||
* empty array.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return string[] An array of string values as provided for the given
|
||||
* header. If the header does not appear in the message, this method MUST
|
||||
* return an empty array.
|
||||
*/
|
||||
function getHeader( $name );
|
||||
|
||||
/**
|
||||
* Retrieves a comma-separated string of the values for a single header.
|
||||
*
|
||||
* This method returns all of the header values of the given
|
||||
* case-insensitive header name as a string concatenated together using
|
||||
* a comma.
|
||||
*
|
||||
* NOTE: Not all header values may be appropriately represented using
|
||||
* comma concatenation. For such headers, use getHeader() instead
|
||||
* and supply your own delimiter when concatenating.
|
||||
*
|
||||
* If the header does not appear in the message, this method MUST return
|
||||
* an empty string.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return string A string of values as provided for the given header
|
||||
* concatenated together using a comma. If the header does not appear in
|
||||
* the message, this method MUST return an empty string.
|
||||
*/
|
||||
function getHeaderLine( $name );
|
||||
|
||||
/**
|
||||
* Gets the body of the message.
|
||||
*
|
||||
* @return StreamInterface Returns the body as a stream.
|
||||
*/
|
||||
function getBody();
|
||||
|
||||
// MessageInterface mutation
|
||||
|
||||
/**
|
||||
* Set the HTTP protocol version.
|
||||
*
|
||||
* The version string MUST contain only the HTTP version number (e.g.,
|
||||
* "1.1", "1.0").
|
||||
*
|
||||
* @param string $version HTTP protocol version
|
||||
*/
|
||||
function setProtocolVersion( $version );
|
||||
|
||||
/**
|
||||
* Set or replace the specified header.
|
||||
*
|
||||
* While header names are case-insensitive, the casing of the header will
|
||||
* be preserved by this function, and returned from getHeaders().
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @param string|string[] $value Header value(s).
|
||||
* @throws \InvalidArgumentException for invalid header names or values.
|
||||
*/
|
||||
function setHeader( $name, $value );
|
||||
|
||||
/**
|
||||
* Append the given value to the specified header.
|
||||
*
|
||||
* Existing values for the specified header will be maintained. The new
|
||||
* value(s) will be appended to the existing list. If the header did not
|
||||
* exist previously, it will be added.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name to add.
|
||||
* @param string|string[] $value Header value(s).
|
||||
* @throws \InvalidArgumentException for invalid header names.
|
||||
* @throws \InvalidArgumentException for invalid header values.
|
||||
*/
|
||||
function addHeader( $name, $value );
|
||||
|
||||
/**
|
||||
* Remove the specified header.
|
||||
*
|
||||
* Header resolution MUST be done without case-sensitivity.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name to remove.
|
||||
*/
|
||||
function removeHeader( $name );
|
||||
|
||||
/**
|
||||
* Set the message body
|
||||
*
|
||||
* The body MUST be a StreamInterface object.
|
||||
*
|
||||
* @param StreamInterface $body Body.
|
||||
* @throws \InvalidArgumentException When the body is not valid.
|
||||
*/
|
||||
function setBody( StreamInterface $body );
|
||||
|
||||
// MediaWiki extensions to PSR-7
|
||||
|
||||
/**
|
||||
* Get the full header lines including colon-separated name and value, for
|
||||
* passing directly to header(). Not including the status line.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
function getRawHeaderLines();
|
||||
|
||||
/**
|
||||
* Set a cookie
|
||||
*
|
||||
* The name will have the cookie prefix added to it before it is sent over
|
||||
* the network.
|
||||
*
|
||||
* @param string $name The name of the cookie, not including prefix.
|
||||
* @param string $value The value to be stored in the cookie.
|
||||
* @param int|null $expire Unix timestamp (in seconds) when the cookie should expire.
|
||||
* 0 (the default) causes it to expire $wgCookieExpiration seconds from now.
|
||||
* null causes it to be a session cookie.
|
||||
* @param array $options Assoc of additional cookie options:
|
||||
* prefix: string, name prefix ($wgCookiePrefix)
|
||||
* domain: string, cookie domain ($wgCookieDomain)
|
||||
* path: string, cookie path ($wgCookiePath)
|
||||
* secure: bool, secure attribute ($wgCookieSecure)
|
||||
* httpOnly: bool, httpOnly attribute ($wgCookieHttpOnly)
|
||||
*/
|
||||
public function setCookie( $name, $value, $expire = 0, $options = [] );
|
||||
|
||||
/**
|
||||
* Get all previously set cookies as a list of associative arrays with
|
||||
* the following keys:
|
||||
*
|
||||
* - name: The cookie name
|
||||
* - value: The cookie value
|
||||
* - expire: The requested expiry time
|
||||
* - options: An associative array of further options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCookies();
|
||||
}
|
||||
231
includes/Rest/Router.php
Normal file
231
includes/Rest/Router.php
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
use AppendIterator;
|
||||
use BagOStuff;
|
||||
use MediaWiki\Rest\PathTemplateMatcher\PathMatcher;
|
||||
use Wikimedia\ObjectFactory;
|
||||
|
||||
/**
|
||||
* The REST router is responsible for gathering handler configuration, matching
|
||||
* an input path and HTTP method against the defined routes, and constructing
|
||||
* and executing the relevant handler for a request.
|
||||
*/
|
||||
class Router {
|
||||
/** @var string[] */
|
||||
private $routeFiles;
|
||||
|
||||
/** @var array */
|
||||
private $extraRoutes;
|
||||
|
||||
/** @var array|null */
|
||||
private $routesFromFiles;
|
||||
|
||||
/** @var int[]|null */
|
||||
private $routeFileTimestamps;
|
||||
|
||||
/** @var string */
|
||||
private $rootPath;
|
||||
|
||||
/** @var \BagOStuff */
|
||||
private $cacheBag;
|
||||
|
||||
/** @var PathMatcher[]|null Path matchers by method */
|
||||
private $matchers;
|
||||
|
||||
/** @var string|null */
|
||||
private $configHash;
|
||||
|
||||
/** @var ResponseFactory */
|
||||
private $responseFactory;
|
||||
|
||||
/**
|
||||
* @param string[] $routeFiles List of names of JSON files containing routes
|
||||
* @param array $extraRoutes Extension route array
|
||||
* @param string $rootPath The base URL path
|
||||
* @param BagOStuff $cacheBag A cache in which to store the matcher trees
|
||||
* @param ResponseFactory $responseFactory
|
||||
*/
|
||||
public function __construct( $routeFiles, $extraRoutes, $rootPath,
|
||||
BagOStuff $cacheBag, ResponseFactory $responseFactory
|
||||
) {
|
||||
$this->routeFiles = $routeFiles;
|
||||
$this->extraRoutes = $extraRoutes;
|
||||
$this->rootPath = $rootPath;
|
||||
$this->cacheBag = $cacheBag;
|
||||
$this->responseFactory = $responseFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache data, or false if it is missing or invalid
|
||||
*
|
||||
* @return bool|array
|
||||
*/
|
||||
private function fetchCacheData() {
|
||||
$cacheData = $this->cacheBag->get( $this->getCacheKey() );
|
||||
if ( $cacheData && $cacheData['CONFIG-HASH'] === $this->getConfigHash() ) {
|
||||
unset( $cacheData['CONFIG-HASH'] );
|
||||
return $cacheData;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The cache key
|
||||
*/
|
||||
private function getCacheKey() {
|
||||
return $this->cacheBag->makeKey( __CLASS__, '1' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a config version hash for cache invalidation
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getConfigHash() {
|
||||
if ( $this->configHash === null ) {
|
||||
$this->configHash = md5( json_encode( [
|
||||
$this->extraRoutes,
|
||||
$this->getRouteFileTimestamps()
|
||||
] ) );
|
||||
}
|
||||
return $this->configHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the defined JSON files and return the merged routes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getRoutesFromFiles() {
|
||||
if ( $this->routesFromFiles === null ) {
|
||||
$this->routeFileTimestamps = [];
|
||||
foreach ( $this->routeFiles as $fileName ) {
|
||||
$this->routeFileTimestamps[$fileName] = filemtime( $fileName );
|
||||
$routes = json_decode( file_get_contents( $fileName ), true );
|
||||
if ( $this->routesFromFiles === null ) {
|
||||
$this->routesFromFiles = $routes;
|
||||
} else {
|
||||
$this->routesFromFiles = array_merge( $this->routesFromFiles, $routes );
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->routesFromFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of last modification times of the defined route files.
|
||||
*
|
||||
* @return int[] Last modification times
|
||||
*/
|
||||
private function getRouteFileTimestamps() {
|
||||
if ( $this->routeFileTimestamps === null ) {
|
||||
$this->routeFileTimestamps = [];
|
||||
foreach ( $this->routeFiles as $fileName ) {
|
||||
$this->routeFileTimestamps[$fileName] = filemtime( $fileName );
|
||||
}
|
||||
}
|
||||
return $this->routeFileTimestamps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an iterator for all defined routes, including loading the routes from
|
||||
* the JSON files.
|
||||
*
|
||||
* @return AppendIterator
|
||||
*/
|
||||
private function getAllRoutes() {
|
||||
$iterator = new AppendIterator;
|
||||
$iterator->append( new \ArrayIterator( $this->getRoutesFromFiles() ) );
|
||||
$iterator->append( new \ArrayIterator( $this->extraRoutes ) );
|
||||
return $iterator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of PathMatcher objects indexed by HTTP method
|
||||
*
|
||||
* @return PathMatcher[]
|
||||
*/
|
||||
private function getMatchers() {
|
||||
if ( $this->matchers === null ) {
|
||||
$cacheData = $this->fetchCacheData();
|
||||
$matchers = [];
|
||||
if ( $cacheData ) {
|
||||
foreach ( $cacheData as $method => $data ) {
|
||||
$matchers[$method] = PathMatcher::newFromCache( $data );
|
||||
}
|
||||
} else {
|
||||
foreach ( $this->getAllRoutes() as $spec ) {
|
||||
$methods = $spec['method'] ?? [ 'GET' ];
|
||||
if ( !is_array( $methods ) ) {
|
||||
$methods = [ $methods ];
|
||||
}
|
||||
foreach ( $methods as $method ) {
|
||||
if ( !isset( $matchers[$method] ) ) {
|
||||
$matchers[$method] = new PathMatcher;
|
||||
}
|
||||
$matchers[$method]->add( $spec['path'], $spec );
|
||||
}
|
||||
}
|
||||
|
||||
$cacheData = [ 'CONFIG-HASH' => $this->getConfigHash() ];
|
||||
foreach ( $matchers as $method => $matcher ) {
|
||||
$cacheData[$method] = $matcher->getCacheData();
|
||||
}
|
||||
$this->cacheBag->set( $this->getCacheKey(), $cacheData );
|
||||
}
|
||||
$this->matchers = $matchers;
|
||||
}
|
||||
return $this->matchers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the handler for a request and execute it
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function execute( RequestInterface $request ) {
|
||||
$matchers = $this->getMatchers();
|
||||
$matcher = $matchers[$request->getMethod()] ?? null;
|
||||
if ( $matcher === null ) {
|
||||
return $this->responseFactory->create404();
|
||||
}
|
||||
$path = $request->getUri()->getPath();
|
||||
if ( substr_compare( $path, $this->rootPath, 0, strlen( $this->rootPath ) ) !== 0 ) {
|
||||
return $this->responseFactory->create404();
|
||||
}
|
||||
$relPath = substr( $path, strlen( $this->rootPath ) );
|
||||
$match = $matcher->match( $relPath );
|
||||
if ( !$match ) {
|
||||
return $this->responseFactory->create404();
|
||||
}
|
||||
$request->setAttributes( $match['params'] );
|
||||
$spec = $match['userData'];
|
||||
$objectFactorySpec = array_intersect_key( $spec,
|
||||
[ 'factory' => true, 'class' => true, 'args' => true ] );
|
||||
$handler = ObjectFactory::getObjectFromSpec( $objectFactorySpec );
|
||||
$handler->init( $request, $spec, $this->responseFactory );
|
||||
|
||||
try {
|
||||
return $this->executeHandler( $handler );
|
||||
} catch ( HttpException $e ) {
|
||||
return $this->responseFactory->createFromException( $e );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a fully-constructed handler
|
||||
* @param Handler $handler
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
private function executeHandler( $handler ): ResponseInterface {
|
||||
$response = $handler->execute();
|
||||
if ( !( $response instanceof ResponseInterface ) ) {
|
||||
$response = $this->responseFactory->createFromReturnValue( $response );
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
19
includes/Rest/SimpleHandler.php
Normal file
19
includes/Rest/SimpleHandler.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
/**
|
||||
* A handler base class which unpacks attributes from the path template and
|
||||
* passes them as formal parameters to run().
|
||||
*
|
||||
* run() must be declared in the subclass. It cannot be declared as abstract
|
||||
* here because it has a variable parameter list.
|
||||
*
|
||||
* @package MediaWiki\Rest
|
||||
*/
|
||||
class SimpleHandler extends Handler {
|
||||
public function execute() {
|
||||
$params = array_values( $this->getRequest()->getAttributes() );
|
||||
return $this->run( ...$params );
|
||||
}
|
||||
}
|
||||
18
includes/Rest/Stream.php
Normal file
18
includes/Rest/Stream.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
use GuzzleHttp\Psr7;
|
||||
|
||||
class Stream extends Psr7\Stream implements CopyableStreamInterface {
|
||||
private $stream;
|
||||
|
||||
public function __construct( $stream, $options = [] ) {
|
||||
$this->stream = $stream;
|
||||
parent::__construct( $stream, $options );
|
||||
}
|
||||
|
||||
public function copyToStream( $target ) {
|
||||
stream_copy_to_stream( $this->stream, $target );
|
||||
}
|
||||
}
|
||||
139
includes/Rest/StringStream.php
Normal file
139
includes/Rest/StringStream.php
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Rest;
|
||||
|
||||
/**
|
||||
* A stream class which uses a string as the underlying storage. Surprisingly,
|
||||
* Guzzle does not appear to have one of these. BufferStream does not do what
|
||||
* we want.
|
||||
*
|
||||
* The normal use of this class should be to first write to the stream, then
|
||||
* rewind, then read back the whole buffer with getContents().
|
||||
*
|
||||
* Seeking is supported, however seeking past the end of the string does not
|
||||
* fill with null bytes as in a real file, it throws an exception instead.
|
||||
*/
|
||||
class StringStream implements CopyableStreamInterface {
|
||||
private $contents = '';
|
||||
private $offset = 0;
|
||||
|
||||
/**
|
||||
* Construct a StringStream with the given contents.
|
||||
*
|
||||
* The offset will start at 0, ready for reading. If appending to the
|
||||
* given string is desired, you should first seek to the end.
|
||||
*
|
||||
* @param string $contents
|
||||
*/
|
||||
public function __construct( $contents = '' ) {
|
||||
$this->contents = $contents;
|
||||
}
|
||||
|
||||
public function copyToStream( $stream ) {
|
||||
if ( $this->offset !== 0 ) {
|
||||
$block = substr( $this->contents, $this->offset );
|
||||
} else {
|
||||
$block = $this->contents;
|
||||
}
|
||||
fwrite( $stream, $block );
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
return $this->contents;
|
||||
}
|
||||
|
||||
public function close() {
|
||||
}
|
||||
|
||||
public function detach() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getSize() {
|
||||
return strlen( $this->contents );
|
||||
}
|
||||
|
||||
public function tell() {
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
public function eof() {
|
||||
return $this->offset >= strlen( $this->contents );
|
||||
}
|
||||
|
||||
public function isSeekable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function seek( $offset, $whence = SEEK_SET ) {
|
||||
switch ( $whence ) {
|
||||
case SEEK_SET:
|
||||
$this->offset = $offset;
|
||||
break;
|
||||
|
||||
case SEEK_CUR:
|
||||
$this->offset += $offset;
|
||||
break;
|
||||
|
||||
case SEEK_END:
|
||||
$this->offset = strlen( $this->contents ) + $offset;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException( "Invalid value for \$whence" );
|
||||
}
|
||||
if ( $this->offset > strlen( $this->contents ) ) {
|
||||
throw new \InvalidArgumentException( "Cannot seek beyond the end of a StringStream" );
|
||||
}
|
||||
if ( $this->offset < 0 ) {
|
||||
throw new \InvalidArgumentException( "Cannot seek before the start of a StringStream" );
|
||||
}
|
||||
}
|
||||
|
||||
public function rewind() {
|
||||
$this->offset = 0;
|
||||
}
|
||||
|
||||
public function isWritable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function write( $string ) {
|
||||
if ( $this->offset === strlen( $this->contents ) ) {
|
||||
$this->contents .= $string;
|
||||
} else {
|
||||
$this->contents = substr_replace( $this->contents, $string,
|
||||
$this->offset, strlen( $string ) );
|
||||
}
|
||||
$this->offset += strlen( $string );
|
||||
return strlen( $string );
|
||||
}
|
||||
|
||||
public function isReadable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function read( $length ) {
|
||||
if ( $this->offset === 0 && $length >= strlen( $this->contents ) ) {
|
||||
$ret = $this->contents;
|
||||
} else {
|
||||
$ret = substr( $this->contents, $this->offset, $length );
|
||||
}
|
||||
$this->offset += strlen( $ret );
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function getContents() {
|
||||
if ( $this->offset === 0 ) {
|
||||
$ret = $this->contents;
|
||||
} else {
|
||||
$ret = substr( $this->contents, $this->offset );
|
||||
}
|
||||
$this->offset = strlen( $this->contents );
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function getMetadata( $key = null ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
6
includes/Rest/coreRoutes.json
Normal file
6
includes/Rest/coreRoutes.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"path": "/user/{name}/hello",
|
||||
"class": "MediaWiki\\Rest\\Handler\\HelloHandler"
|
||||
}
|
||||
]
|
||||
|
|
@ -143,6 +143,9 @@ if ( $wgScript === false ) {
|
|||
if ( $wgLoadScript === false ) {
|
||||
$wgLoadScript = "$wgScriptPath/load.php";
|
||||
}
|
||||
if ( $wgRestPath === false ) {
|
||||
$wgRestPath = "$wgScriptPath/rest.php";
|
||||
}
|
||||
|
||||
if ( $wgArticlePath === false ) {
|
||||
if ( $wgUsePathInfo ) {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ class ExtensionProcessor implements Processor {
|
|||
protected static $coreAttributes = [
|
||||
'SkinOOUIThemes',
|
||||
'TrackingCategories',
|
||||
'RestRoutes',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue