Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
44.64% covered (danger)
44.64%
50 / 112
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
EntryPoint
44.64% covered (danger)
44.64%
50 / 112
37.50% covered (danger)
37.50%
3 / 8
80.24
0.00% covered (danger)
0.00%
0 / 1
 createRouter
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
2
 getMainRequest
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 doSetup
68.18% covered (warning)
68.18%
15 / 22
0.00% covered (danger)
0.00%
0 / 1
2.13
 getTextFormatters
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getRouteFiles
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setRouter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
80.00% covered (warning)
80.00%
24 / 30
0.00% covered (danger)
0.00%
0 / 1
6.29
1<?php
2
3namespace MediaWiki\Rest;
4
5use MediaWiki\Config\Config;
6use MediaWiki\Config\ServiceOptions;
7use MediaWiki\Context\IContextSource;
8use MediaWiki\Context\RequestContext;
9use MediaWiki\EntryPointEnvironment;
10use MediaWiki\MainConfigNames;
11use MediaWiki\MediaWikiEntryPoint;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\Registration\ExtensionRegistry;
14use MediaWiki\Rest\BasicAccess\CompoundAuthorizer;
15use MediaWiki\Rest\BasicAccess\MWBasicAuthorizer;
16use MediaWiki\Rest\Reporter\MWErrorReporter;
17use MediaWiki\Rest\Validator\Validator;
18use MWExceptionRenderer;
19use Wikimedia\Message\ITextFormatter;
20
21/**
22 * @internal
23 */
24class EntryPoint extends MediaWikiEntryPoint {
25
26    private RequestInterface $request;
27    private ?Router $router = null;
28    private ?CorsUtils $cors  = null;
29
30    /**
31     * @internal Public for use in core tests
32     *
33     * @param MediaWikiServices $services
34     * @param IContextSource $context
35     * @param RequestInterface $request
36     * @param ResponseFactory $responseFactory
37     * @param CorsUtils $cors
38     *
39     * @return Router
40     */
41    public static function createRouter(
42        MediaWikiServices $services,
43        IContextSource $context,
44        RequestInterface $request,
45        ResponseFactory $responseFactory,
46        CorsUtils $cors
47    ): Router {
48        $conf = $services->getMainConfig();
49
50        $authority = $context->getAuthority();
51        $authorizer = new CompoundAuthorizer();
52        $authorizer
53            ->addAuthorizer( new MWBasicAuthorizer( $authority ) )
54            ->addAuthorizer( $cors );
55
56        $objectFactory = $services->getObjectFactory();
57        $restValidator = new Validator( $objectFactory,
58            $request,
59            $authority
60        );
61
62        $stats = $services->getStatsFactory();
63
64        return ( new Router(
65            self::getRouteFiles( $conf ),
66            ExtensionRegistry::getInstance()->getAttribute( 'RestRoutes' ),
67            new ServiceOptions( Router::CONSTRUCTOR_OPTIONS, $conf ),
68            $services->getLocalServerObjectCache(),
69            $responseFactory,
70            $authorizer,
71            $authority,
72            $objectFactory,
73            $restValidator,
74            new MWErrorReporter(),
75            $services->getHookContainer(),
76            $context->getRequest()->getSession()
77        ) )
78            ->setCors( $cors )
79            ->setStats( $stats );
80    }
81
82    /**
83     * @internal
84     * @return RequestInterface The RequestInterface object used by this entry point.
85     */
86    public static function getMainRequest(): RequestInterface {
87        static $mainRequest = null;
88
89        if ( $mainRequest === null ) {
90            $conf = MediaWikiServices::getInstance()->getMainConfig();
91            $mainRequest = new RequestFromGlobals( [
92                'cookiePrefix' => $conf->get( MainConfigNames::CookiePrefix )
93            ] );
94        }
95
96        return $mainRequest;
97    }
98
99    protected function doSetup() {
100        parent::doSetup();
101
102        $context = RequestContext::getMain();
103
104        $responseFactory = new ResponseFactory( $this->getTextFormatters() );
105        $responseFactory->setShowExceptionDetails(
106            MWExceptionRenderer::shouldShowExceptionDetails()
107        );
108
109        $this->cors = new CorsUtils(
110            new ServiceOptions(
111                CorsUtils::CONSTRUCTOR_OPTIONS,
112                $this->getServiceContainer()->getMainConfig()
113            ),
114            $responseFactory,
115            $context->getUser()
116        );
117
118        if ( !$this->router ) {
119            $this->router = $this->createRouter(
120                $this->getServiceContainer(),
121                $context,
122                $this->request,
123                $responseFactory,
124                $this->cors
125            );
126        }
127    }
128
129    /**
130     * Get a TextFormatter array from MediaWikiServices
131     *
132     * @return ITextFormatter[]
133     */
134    private function getTextFormatters() {
135        $services = $this->getServiceContainer();
136
137        $code = $services->getContentLanguageCode()->toString();
138        $langs = array_unique( [ $code, 'en' ] );
139        $textFormatters = [];
140        $factory = $services->getMessageFormatterFactory();
141
142        foreach ( $langs as $lang ) {
143            $textFormatters[] = $factory->getTextFormatter( $lang );
144        }
145
146        return $textFormatters;
147    }
148
149    /**
150     * @param Config $conf
151     *
152     * @return string[]
153     */
154    private static function getRouteFiles( $conf ) {
155        global $IP;
156        $extensionsDir = $conf->get( MainConfigNames::ExtensionDirectory );
157        // Always include the "official" routes. Include additional routes if specified.
158        $routeFiles = array_merge(
159            [
160                'includes/Rest/coreRoutes.json',
161            ],
162            $conf->get( MainConfigNames::RestAPIAdditionalRouteFiles )
163        );
164        foreach ( $routeFiles as &$file ) {
165            if (
166                str_starts_with( $file, '/' )
167            ) {
168                // Allow absolute paths on non-Windows
169            } elseif (
170                str_starts_with( $file, 'extensions/' )
171            ) {
172                // Support hacks like Wikibase.ci.php
173                $file = substr_replace( $file, $extensionsDir,
174                    0, strlen( 'extensions' ) );
175            } else {
176                $file = "$IP/$file";
177            }
178        }
179
180        return $routeFiles;
181    }
182
183    public function __construct(
184        RequestInterface $request,
185        RequestContext $context,
186        EntryPointEnvironment $environment,
187        MediaWikiServices $mediaWikiServices
188    ) {
189        parent::__construct( $context, $environment, $mediaWikiServices );
190
191        $this->request = $request;
192    }
193
194    /**
195     * Sets the router to use.
196     * Intended for testing.
197     *
198     * @param Router $router
199     */
200    public function setRouter( Router $router ): void {
201        $this->router = $router;
202    }
203
204    public function execute() {
205        $this->startOutputBuffer();
206
207        // IDEA: Move the call to cors->modifyResponse() into Module,
208        //       so it's in the same class as cors->createPreflightResponse().
209        $response = $this->cors->modifyResponse(
210            $this->request,
211            $this->router->execute( $this->request )
212        );
213
214        $webResponse = $this->getResponse();
215
216        $webResponse->header(
217            'HTTP/' . $response->getProtocolVersion() . ' ' . $response->getStatusCode() . ' ' .
218            $response->getReasonPhrase()
219        );
220
221        foreach ( $response->getRawHeaderLines() as $line ) {
222            $webResponse->header( $line );
223        }
224
225        foreach ( $response->getCookies() as $cookie ) {
226            $webResponse->setCookie(
227                $cookie['name'],
228                $cookie['value'],
229                $cookie['expiry'],
230                $cookie['options']
231            );
232        }
233
234        // Clear all errors that might have been displayed if display_errors=On
235        $this->discardOutputBuffer();
236
237        $stream = $response->getBody();
238        $stream->rewind();
239
240        $this->prepareForOutput();
241
242        if ( $stream instanceof CopyableStreamInterface ) {
243            $stream->copyToStream( fopen( 'php://output', 'w' ) );
244        } else {
245            while ( true ) {
246                $buffer = $stream->read( 65536 );
247                if ( $buffer === '' ) {
248                    break;
249                }
250                $this->print( $buffer );
251            }
252        }
253    }
254
255}