Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
92.41% |
73 / 79 |
|
75.00% |
9 / 12 |
CRAP | |
0.00% |
0 / 1 |
MediaLinksHandler | |
92.41% |
73 / 79 |
|
75.00% |
9 / 12 |
19.16 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getPage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
run | |
80.95% |
17 / 21 |
|
0.00% |
0 / 1 |
4.11 | |||
getDbResults | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
processDbResults | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
2 | |||
needsWriteAccess | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getParamSettings | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
getETag | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getLastModified | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
hasRepresentation | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMaxNumLinks | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getResponseBodySchemaFileName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Rest\Handler; |
4 | |
5 | use MediaFileTrait; |
6 | use MediaWiki\Page\ExistingPageRecord; |
7 | use MediaWiki\Page\PageLookup; |
8 | use MediaWiki\Rest\Handler; |
9 | use MediaWiki\Rest\LocalizedHttpException; |
10 | use MediaWiki\Rest\Response; |
11 | use MediaWiki\Rest\SimpleHandler; |
12 | use RepoGroup; |
13 | use Wikimedia\Message\MessageValue; |
14 | use Wikimedia\ParamValidator\ParamValidator; |
15 | use Wikimedia\Rdbms\IConnectionProvider; |
16 | |
17 | /** |
18 | * Handler class for Core REST API endpoints that perform operations on revisions |
19 | */ |
20 | class 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 | } |