Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
3.45% covered (danger)
3.45%
7 / 203
7.69% covered (danger)
7.69%
1 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialBaseDistributor
3.45% covered (danger)
3.45%
7 / 203
7.69% covered (danger)
7.69%
1 / 13
1202.50
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 / 3
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 IBufferingStatsdDataFactory;
7use MediaWiki\Extension\ExtensionDistributor\Providers\ExtDistProvider;
8use MediaWiki\Extension\ExtensionDistributor\Stats\ExtDistGraphiteStats;
9use MediaWiki\Html\Html;
10use MediaWiki\Logger\LoggerFactory;
11use MediaWiki\SpecialPage\SpecialPage;
12use MediaWiki\Title\Title;
13use OOUI\ActionFieldLayout;
14use OOUI\ButtonInputWidget;
15use OOUI\DropdownInputWidget;
16use Psr\Log\LoggerInterface;
17use Xml;
18
19/**
20 * Base class for special pages that allow users to download repository snapshots
21 *
22 * @author Tim Starling
23 */
24abstract 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    protected function getGroupName() {
382        return 'developer';
383    }
384
385    /**
386     * Get the list of popular items for this special page,
387     * or false if none are configured
388     *
389     * @return string[]|bool
390     */
391    protected function getPopularList() {
392        return $this->getGraphiteStats()->getPopularList( $this->type );
393    }
394
395    /**
396     * @return ExtDistGraphiteStats
397     */
398    protected function getGraphiteStats() {
399        $stats = new ExtDistGraphiteStats();
400        $stats->setLogger( $this->logger );
401        return $stats;
402    }
403
404}