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