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