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