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