Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.51% covered (success)
93.51%
72 / 77
81.82% covered (warning)
81.82%
9 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
MediaLinksHandler
93.51% covered (success)
93.51%
72 / 77
81.82% covered (warning)
81.82%
9 / 11
18.09
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getPage
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 run
80.95% covered (warning)
80.95%
17 / 21
0.00% covered (danger)
0.00%
0 / 1
4.11
 getDbResults
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 processDbResults
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
2
 needsWriteAccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getParamSettings
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getETag
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getLastModified
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 hasRepresentation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMaxNumLinks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Rest\Handler;
4
5use MediaFileTrait;
6use MediaWiki\Page\ExistingPageRecord;
7use MediaWiki\Page\PageLookup;
8use MediaWiki\Rest\LocalizedHttpException;
9use MediaWiki\Rest\Response;
10use MediaWiki\Rest\SimpleHandler;
11use RepoGroup;
12use Wikimedia\Message\MessageValue;
13use Wikimedia\ParamValidator\ParamValidator;
14use Wikimedia\Rdbms\IConnectionProvider;
15
16/**
17 * Handler class for Core REST API endpoints that perform operations on revisions
18 */
19class MediaLinksHandler extends SimpleHandler {
20    use MediaFileTrait;
21
22    /** int The maximum number of media links to return */
23    private const MAX_NUM_LINKS = 100;
24
25    /** @var IConnectionProvider */
26    private $dbProvider;
27
28    /** @var RepoGroup */
29    private $repoGroup;
30
31    /** @var PageLookup */
32    private $pageLookup;
33
34    /**
35     * @var ExistingPageRecord|false|null
36     */
37    private $page = false;
38
39    /**
40     * @param IConnectionProvider $dbProvider
41     * @param RepoGroup $repoGroup
42     * @param PageLookup $pageLookup
43     */
44    public function __construct(
45        IConnectionProvider $dbProvider,
46        RepoGroup $repoGroup,
47        PageLookup $pageLookup
48    ) {
49        $this->dbProvider = $dbProvider;
50        $this->repoGroup = $repoGroup;
51        $this->pageLookup = $pageLookup;
52    }
53
54    /**
55     * @return ExistingPageRecord|null
56     */
57    private function getPage(): ?ExistingPageRecord {
58        if ( $this->page === false ) {
59            $this->page = $this->pageLookup->getExistingPageByText(
60                    $this->getValidatedParams()['title']
61                );
62        }
63        return $this->page;
64    }
65
66    /**
67     * @param string $title
68     * @return Response
69     * @throws LocalizedHttpException
70     */
71    public function run( $title ) {
72        $page = $this->getPage();
73        if ( !$page ) {
74            throw new LocalizedHttpException(
75                MessageValue::new( 'rest-nonexistent-title' )->plaintextParams( $title ),
76                404
77            );
78        }
79
80        if ( !$this->getAuthority()->authorizeRead( 'read', $page ) ) {
81            throw new LocalizedHttpException(
82                MessageValue::new( 'rest-permission-denied-title' )->plaintextParams( $title ),
83                403
84            );
85        }
86
87        // @todo: add continuation if too many links are found
88        $results = $this->getDbResults( $page->getId() );
89        if ( count( $results ) > $this->getMaxNumLinks() ) {
90            throw new LocalizedHttpException(
91                MessageValue::new( 'rest-media-too-many-links' )
92                    ->plaintextParams( $title )
93                    ->numParams( $this->getMaxNumLinks() ),
94                400
95            );
96        }
97        $response = $this->processDbResults( $results );
98        return $this->getResponseFactory()->createJson( $response );
99    }
100
101    /**
102     * @param int $pageId the id of the page to load media links for
103     * @return array the results
104     */
105    private function getDbResults( int $pageId ) {
106        return $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder()
107            ->select( 'il_to' )
108            ->from( 'imagelinks' )
109            ->where( [ 'il_from' => $pageId ] )
110            ->orderBy( 'il_to' )
111            ->limit( $this->getMaxNumLinks() + 1 )
112            ->caller( __METHOD__ )->fetchFieldValues();
113    }
114
115    /**
116     * @param array $results database results, or an empty array if none
117     * @return array response data
118     */
119    private function processDbResults( $results ) {
120        // Using "private" here means an equivalent of the Action API's "anon-public-user-private"
121        // caching model would be necessary, if caching is ever added to this endpoint.
122        $performer = $this->getAuthority();
123        $findTitles = array_map( static function ( $title ) use ( $performer ) {
124            return [
125                'title' => $title,
126                'private' => $performer,
127            ];
128        }, $results );
129
130        $files = $this->repoGroup->findFiles( $findTitles );
131        [ $maxWidth, $maxHeight ] = self::getImageLimitsFromOption(
132            $this->getAuthority()->getUser(),
133            'imagesize'
134        );
135        $transforms = [
136            'preferred' => [
137                'maxWidth' => $maxWidth,
138                'maxHeight' => $maxHeight,
139            ]
140        ];
141        $response = [];
142        foreach ( $files as $file ) {
143            $response[] = $this->getFileInfo( $file, $performer, $transforms );
144        }
145
146        $response = [
147            'files' => $response
148        ];
149
150        return $response;
151    }
152
153    public function needsWriteAccess() {
154        return false;
155    }
156
157    public function getParamSettings() {
158        return [
159            'title' => [
160                self::PARAM_SOURCE => 'path',
161                ParamValidator::PARAM_TYPE => 'string',
162                ParamValidator::PARAM_REQUIRED => true,
163            ],
164        ];
165    }
166
167    /**
168     * @return string|null
169     * @throws LocalizedHttpException
170     */
171    protected function getETag(): ?string {
172        $page = $this->getPage();
173        if ( !$page ) {
174            return null;
175        }
176
177        // XXX: use hash of the rendered HTML?
178        return '"' . $page->getLatest() . '@' . wfTimestamp( TS_MW, $page->getTouched() ) . '"';
179    }
180
181    /**
182     * @return string|null
183     * @throws LocalizedHttpException
184     */
185    protected function getLastModified(): ?string {
186        $page = $this->getPage();
187        return $page ? $page->getTouched() : null;
188    }
189
190    /**
191     * @return bool
192     */
193    protected function hasRepresentation() {
194        return (bool)$this->getPage();
195    }
196
197    /**
198     * For testing
199     *
200     * @unstable
201     */
202    protected function getMaxNumLinks(): int {
203        return self::MAX_NUM_LINKS;
204    }
205}