Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
3.45% |
7 / 203 |
|
7.69% |
1 / 13 |
CRAP | |
0.00% |
0 / 1 |
SpecialBaseDistributor | |
3.45% |
7 / 203 |
|
7.69% |
1 / 13 |
1202.50 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
msgKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
18.52% |
5 / 27 |
|
0.00% |
0 / 1 |
42.62 | |||
showExtensionSelector | |
0.00% |
0 / 67 |
|
0.00% |
0 / 1 |
30 | |||
formatVersion | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
formatBranch | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
showVersionSelector | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
30 | |||
doDownload | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
20 | |||
doStats | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getProvider | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPopularList | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGraphiteStats | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\ExtensionDistributor\Specials; |
4 | |
5 | use HtmlArmor; |
6 | use MediaWiki\Extension\ExtensionDistributor\Providers\ExtDistProvider; |
7 | use MediaWiki\Extension\ExtensionDistributor\Stats\ExtDistGraphiteStats; |
8 | use MediaWiki\Html\Html; |
9 | use MediaWiki\Logger\LoggerFactory; |
10 | use MediaWiki\SpecialPage\SpecialPage; |
11 | use MediaWiki\Title\Title; |
12 | use MediaWiki\Xml\Xml; |
13 | use OOUI\ActionFieldLayout; |
14 | use OOUI\ButtonInputWidget; |
15 | use OOUI\DropdownInputWidget; |
16 | use Psr\Log\LoggerInterface; |
17 | use Wikimedia\Stats\IBufferingStatsdDataFactory; |
18 | |
19 | /** |
20 | * Base class for special pages that allow users to download repository snapshots |
21 | * |
22 | * @author Tim Starling |
23 | */ |
24 | abstract class SpecialBaseDistributor extends SpecialPage { |
25 | |
26 | /** |
27 | * @var string either "extensions" or "skins" |
28 | */ |
29 | protected $type; |
30 | |
31 | /** |
32 | * @var LoggerInterface |
33 | */ |
34 | protected $logger; |
35 | |
36 | /** |
37 | * @var ExtDistProvider |
38 | */ |
39 | protected $provider; |
40 | |
41 | /** @var IBufferingStatsdDataFactory */ |
42 | protected $statsFactory; |
43 | |
44 | /** |
45 | * @param string $pageName |
46 | * @param IBufferingStatsdDataFactory $statsFactory |
47 | */ |
48 | public function __construct( $pageName, IBufferingStatsdDataFactory $statsFactory ) { |
49 | $this->statsFactory = $statsFactory; |
50 | |
51 | parent::__construct( $pageName ); |
52 | } |
53 | |
54 | /** |
55 | * Substitute $TYPE in a message key |
56 | * |
57 | * @param string $key |
58 | * @return mixed |
59 | */ |
60 | protected function msgKey( $key ) { |
61 | return str_replace( '$TYPE', $this->type, $key ); |
62 | } |
63 | |
64 | /** |
65 | * @param string $subpage |
66 | */ |
67 | public function execute( $subpage ) { |
68 | $this->setHeaders(); |
69 | $this->logger = LoggerFactory::getInstance( 'ExtensionDistributor' ); |
70 | |
71 | if ( !$this->getConfig()->get( 'ExtDistAPIConfig' ) ) { |
72 | $this->getOutput()->addWikiMsg( 'extdist-not-configured' ); |
73 | return; |
74 | } |
75 | if ( $subpage ) { |
76 | $parts = explode( '/', $subpage, 2 ); |
77 | |
78 | if ( count( $parts ) == 1 ) { |
79 | $parts[] = false; |
80 | } |
81 | |
82 | [ $name, $version ] = $parts; |
83 | } else { |
84 | $name = $this->getRequest()->getVal( 'extdistname' ); |
85 | $version = $this->getRequest()->getVal( 'extdistversion' ); |
86 | } |
87 | |
88 | if ( !$name ) { |
89 | $this->showExtensionSelector(); |
90 | return; |
91 | } |
92 | |
93 | if ( !in_array( $name, $this->getProvider()->getRepositoryList() ) ) { |
94 | // extdist-no-such-extensions, extdist-no-such-skins |
95 | $this->getOutput()->addWikiMsg( $this->msgKey( 'extdist-no-such-$TYPE' ), $name ); |
96 | $this->showExtensionSelector(); |
97 | return; |
98 | } |
99 | |
100 | if ( !$version ) { |
101 | $this->showVersionSelector( $name ); |
102 | return; |
103 | } |
104 | |
105 | if ( !$this->getProvider()->hasBranch( $name, $version ) ) { |
106 | // extdist-no-such-version-extensions, extdist-no-such-version-skins |
107 | $this->getOutput()->addWikiMsg( |
108 | $this->msgKey( 'extdist-no-such-version-$TYPE' ), $name, $version ); |
109 | return; |
110 | } |
111 | |
112 | $this->doDownload( $name, $version ); |
113 | } |
114 | |
115 | protected function showExtensionSelector() { |
116 | $repos = $this->getProvider()->getRepositoryList(); |
117 | |
118 | if ( !$repos ) { |
119 | // extdist-list-missing-extensions, extdist-list-missing-skins |
120 | $this->getOutput()->addWikiMsg( $this->msgKey( 'extdist-list-missing-$TYPE' ) ); |
121 | return; |
122 | } |
123 | |
124 | $out = $this->getOutput(); |
125 | $out->enableOOUI(); |
126 | $out->addHTML( '<div class="mw-extdist-container"><div class="mw-extdist-form">' ); |
127 | // extdist-choose-extensions, extdist-choose-skins |
128 | $out->addWikiMsg( $this->msgKey( 'extdist-choose-$TYPE' ) ); |
129 | $out->addHTML( |
130 | Xml::openElement( 'form', [ |
131 | 'action' => $this->getPageTitle()->getLocalURL(), |
132 | 'method' => 'GET' ] ) |
133 | ); |
134 | $items = [ [ 'data' => '' ] ]; |
135 | |
136 | natcasesort( $repos ); |
137 | foreach ( $repos as $name ) { |
138 | $items[] = [ 'data' => $name ]; |
139 | } |
140 | |
141 | $config = $this->getConfig(); |
142 | |
143 | // Add JS infuse magic |
144 | $out->addModules( 'ext.extensiondistributor.special' ); |
145 | $out->addModuleStyles( 'ext.extensiondistributor.special.styles' ); |
146 | $out->addJsConfigVars( [ |
147 | 'wgExtDistSnapshotRefs' => $config->get( 'ExtDistSnapshotRefs' ), |
148 | 'wgExtDistDefaultSnapshot' => $config->get( 'ExtDistDefaultSnapshot' ), |
149 | 'wgExtDistCandidateSnapshot' => $config->get( 'ExtDistCandidateSnapshot' ), |
150 | ] ); |
151 | $out->addHTML( |
152 | new DropdownInputWidget( [ |
153 | 'classes' => [ 'mw-extdist-selector' ], |
154 | 'infusable' => true, |
155 | 'options' => $items, |
156 | 'name' => 'extdistname', |
157 | ] ) . |
158 | // only shown to no-JS users via CSS |
159 | new ButtonInputWidget( [ |
160 | 'classes' => [ 'mw-extdist-ext-submit' ], |
161 | 'infusable' => true, |
162 | 'label' => $this->msg( 'extdist-submit-extension' )->text(), |
163 | 'type' => 'submit', |
164 | 'flags' => [ 'primary', 'progressive' ], |
165 | ] ) . |
166 | Xml::closeElement( 'form' ) . "\n" . |
167 | Html::element( 'div', [ 'class' => 'mw-extdist-continue' ] ) . |
168 | "</div>" |
169 | ); |
170 | |
171 | $popularList = $this->getPopularList(); |
172 | if ( !$popularList ) { |
173 | // Close the container div |
174 | $out->addHTML( '</div>' ); |
175 | return; |
176 | } |
177 | |
178 | $out->addHTML( |
179 | '<div class="mw-extdist-popular">' . |
180 | $this->msg( $this->msgKey( 'extdist-popular-$TYPE' ) ) |
181 | ->numParams( count( $popularList ) ) |
182 | ->escaped() . |
183 | "\n<ol>" |
184 | ); |
185 | $linkRenderer = $this->getLinkRenderer(); |
186 | foreach ( $popularList as $popularItem ) { |
187 | $link = $linkRenderer->makeLink( |
188 | $this->getPageTitle( $popularItem ), |
189 | $popularItem, |
190 | [ |
191 | 'data-name' => $popularItem, |
192 | 'class' => 'mw-extdist-plinks', |
193 | ] |
194 | ); |
195 | $out->addHTML( "<li>$link</li>\n" ); |
196 | } |
197 | $out->addHTML( '</ol>' ); |
198 | // Closes popular and container |
199 | $out->addHTML( '</div></div>' ); |
200 | } |
201 | |
202 | /** |
203 | * @param string $version |
204 | * @return string |
205 | */ |
206 | protected function formatVersion( $version ) { |
207 | if ( strpos( $version, 'REL' ) === 0 ) { |
208 | // Strip "REL" prefix, and convert _ to . |
209 | return str_replace( '_', '.', substr( $version, 3 ) ); |
210 | } else { |
211 | // Don't touch it |
212 | return $version; |
213 | } |
214 | } |
215 | |
216 | /** |
217 | * @note Keep this in-sync with the JavaScript version |
218 | * @param string $branch Branch name |
219 | * @return string formatted text |
220 | */ |
221 | protected function formatBranch( $branch ) { |
222 | $version = $this->formatVersion( $branch ); |
223 | $config = $this->getConfig(); |
224 | if ( $branch === 'master' ) { |
225 | // Special case |
226 | return $this->msg( 'extdist-branch-alpha' )->text(); |
227 | } elseif ( $branch === $config->get( 'ExtDistDefaultSnapshot' ) ) { |
228 | return $this->msg( 'extdist-branch-stable' )->params( $version )->text(); |
229 | } elseif ( $branch === $config->get( 'ExtDistCandidateSnapshot' ) ) { |
230 | return $this->msg( 'extdist-branch-candidate' )->params( $version )->text(); |
231 | } else { |
232 | // Don't touch it |
233 | return $version; |
234 | } |
235 | } |
236 | |
237 | /** |
238 | * @param string $repoName |
239 | */ |
240 | protected function showVersionSelector( $repoName ) { |
241 | $config = $this->getConfig(); |
242 | if ( !$config->get( 'ExtDistSnapshotRefs' ) ) { |
243 | // extdist-no-versions-extensions, extdist-no-versions-skins |
244 | $this->getOutput()->addWikiMsg( $this->msgKey( 'extdist-no-versions-$TYPE' ), $repoName ); |
245 | $this->showExtensionSelector(); |
246 | return; |
247 | } |
248 | |
249 | $out = $this->getOutput(); |
250 | // extdist-choose-version-extensions, extdist-choose-version-skins |
251 | $out->addWikiMsg( $this->msgKey( 'extdist-choose-version-$TYPE' ), $repoName ); |
252 | $html = |
253 | Xml::openElement( 'form', [ |
254 | 'action' => $this->getPageTitle()->getLocalURL(), |
255 | 'method' => 'GET' ] ) . |
256 | Html::hidden( 'extdistname', $repoName ); |
257 | $options = []; |
258 | $selected = 0; |
259 | |
260 | foreach ( $config->get( 'ExtDistSnapshotRefs' ) as $branchName ) { |
261 | if ( $this->getProvider()->hasBranch( $repoName, $branchName ) ) { |
262 | $branchDesc = $this->formatBranch( $branchName ); |
263 | $options[] = [ 'data' => $branchName, 'label' => $branchDesc ]; |
264 | $selected++; |
265 | } |
266 | } |
267 | if ( $selected !== 0 ) { |
268 | $out->addHTML( $html ); |
269 | $out->enableOOUI(); |
270 | $out->addHTML( |
271 | new ActionFieldLayout( |
272 | new DropdownInputWidget( [ |
273 | 'id' => 'mw-extdist-selector-version', |
274 | 'infusable' => true, |
275 | 'options' => $options, |
276 | 'value' => $config->get( 'ExtDistDefaultSnapshot' ), |
277 | 'name' => 'extdistversion', |
278 | ] ), |
279 | new ButtonInputWidget( [ |
280 | 'label' => $this->msg( 'extdist-submit-version' )->text(), |
281 | 'type' => 'submit', |
282 | 'flags' => [ 'primary', 'progressive' ], |
283 | ] ), |
284 | [ |
285 | 'align' => 'top' |
286 | ] |
287 | ) . |
288 | Xml::closeElement( 'form' ) . "\n" |
289 | ); |
290 | } else { |
291 | $this->logger->warning( "Couldn't find any branches for \"{$repoName}\"" ); |
292 | $out->wrapWikiMsg( '<div class="error">$1</div>', 'extdist-no-branches' ); |
293 | } |
294 | } |
295 | |
296 | /** |
297 | * @param string $extension |
298 | * @param string $version |
299 | */ |
300 | protected function doDownload( $extension, $version ) { |
301 | if ( !$this->getProvider()->hasBranch( $extension, $version ) ) { |
302 | $this->getOutput()->addWikiMsg( 'extdist-tar-error' ); |
303 | return; |
304 | } |
305 | |
306 | // Show a message |
307 | $sha1 = $this->getProvider()->getBranchSha( $extension, $version ); |
308 | $url = $this->getProvider()->getTarballLocation( $extension, $version ); |
309 | $fileName = $this->getProvider()->getExpectedTarballName( $extension, $version ); |
310 | // extdist-created-extensions, extdist-created-skins |
311 | $this->getOutput()->addWikiMsg( $this->msgKey( 'extdist-created-$TYPE' ), $extension, $sha1, |
312 | $version, $url, $fileName ); |
313 | |
314 | $this->getOutput()->addModuleStyles( 'ext.extensiondistributor.special.styles' ); |
315 | |
316 | // Add link to the extension/skin's page |
317 | $pageTitle = Title::newFromText( |
318 | ( $this->type === ExtDistProvider::EXTENSIONS ? 'Extension:' : 'Skin:' ) . $extension |
319 | ); |
320 | $linkRenderer = $this->getLinkRenderer(); |
321 | if ( $pageTitle->isKnown() ) { |
322 | $this->getOutput()->addHTML( |
323 | Xml::openElement( 'p' ) . |
324 | $linkRenderer->makeKnownLink( |
325 | $pageTitle, |
326 | // extdist-goto-extensions-page, extdist-goto-skins-page |
327 | new HtmlArmor( |
328 | $this |
329 | ->msg( $this->msgKey( 'extdist-goto-$TYPE-page' ), $extension ) |
330 | ->parse() |
331 | ) |
332 | ) . |
333 | Xml::closeElement( 'p' ) . "\n" |
334 | ); |
335 | } |
336 | |
337 | $this->getOutput()->addHTML( |
338 | Xml::openElement( 'p' ) . |
339 | $linkRenderer->makeLink( |
340 | $this->getPageTitle(), |
341 | new HtmlArmor( |
342 | // extdist-want-more-extensions, extdist-want-more-skins |
343 | $this->msg( $this->msgKey( 'extdist-want-more-$TYPE' ) )->escaped() |
344 | ), |
345 | [ 'class' => 'mw-extdist-download' ] |
346 | ) . Xml::closeElement( 'p' ) . "\n" |
347 | ); |
348 | |
349 | $this->doStats( $extension, $version ); |
350 | |
351 | // Redirect to the file |
352 | header( 'Refresh: 5;url=' . $url ); |
353 | } |
354 | |
355 | /** |
356 | * Record some download metrics |
357 | * |
358 | * @param string $repo |
359 | * @param string $version |
360 | */ |
361 | protected function doStats( $repo, $version ) { |
362 | // Overall repo downloads |
363 | $this->statsFactory->increment( "extdist.{$this->type}.$repo" ); |
364 | // Repo split by version |
365 | $this->statsFactory->increment( "extdist.{$this->type}.$repo.$version" ); |
366 | // MediaWiki core version adoption |
367 | $this->statsFactory->increment( "extdist.$version" ); |
368 | } |
369 | |
370 | /** |
371 | * @return ExtDistProvider |
372 | */ |
373 | protected function getProvider() { |
374 | if ( !$this->provider ) { |
375 | $this->provider = ExtDistProvider::getProviderFor( $this->type ); |
376 | $this->provider->setLogger( $this->logger ); |
377 | } |
378 | return $this->provider; |
379 | } |
380 | |
381 | /** @inheritDoc */ |
382 | protected function getGroupName() { |
383 | return 'developer'; |
384 | } |
385 | |
386 | /** |
387 | * Get the list of popular items for this special page, |
388 | * or false if none are configured |
389 | * |
390 | * @return string[]|bool |
391 | */ |
392 | protected function getPopularList() { |
393 | return $this->getGraphiteStats()->getPopularList( $this->type ); |
394 | } |
395 | |
396 | /** |
397 | * @return ExtDistGraphiteStats |
398 | */ |
399 | protected function getGraphiteStats() { |
400 | $stats = new ExtDistGraphiteStats(); |
401 | $stats->setLogger( $this->logger ); |
402 | return $stats; |
403 | } |
404 | |
405 | } |