Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 107
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialGadgetUsage
0.00% covered (danger)
0.00%
0 / 107
0.00% covered (danger)
0.00%
0 / 11
506
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isExpensive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
2
 getOrderFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 outputTableStart
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 outputTableEnd
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 formatResult
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 getDefaultGadgets
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 outputResults
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
56
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Extension\Gadgets\Special;
22
23use MediaWiki\Extension\Gadgets\GadgetRepo;
24use MediaWiki\Html\Html;
25use MediaWiki\Output\OutputPage;
26use MediaWiki\Skin\Skin;
27use MediaWiki\SpecialPage\QueryPage;
28use MediaWiki\Title\TitleValue;
29use stdClass;
30use Wikimedia\Rdbms\IConnectionProvider;
31use Wikimedia\Rdbms\IExpression;
32use Wikimedia\Rdbms\IReadableDatabase;
33use Wikimedia\Rdbms\IResultWrapper;
34use Wikimedia\Rdbms\LikeValue;
35
36/**
37 * Special:GadgetUsage lists all the gadgets on the wiki along with number of users.
38 *
39 * @copyright 2015 Niharika Kohli
40 */
41class SpecialGadgetUsage extends QueryPage {
42    public function __construct(
43        private readonly GadgetRepo $gadgetRepo,
44        private readonly IConnectionProvider $dbProvider,
45    ) {
46        parent::__construct( 'GadgetUsage' );
47        // Show all gadgets
48        $this->limit = 1000;
49        $this->shownavigation = false;
50    }
51
52    /**
53     * @inheritDoc
54     */
55    public function execute( $par ) {
56        parent::execute( $par );
57        $this->addHelpLink( 'Extension:Gadgets' );
58    }
59
60    /** @inheritDoc */
61    public function isExpensive() {
62        return true;
63    }
64
65    /**
66     * Define the database query that is used to generate the stats table.
67     * The query is essentially:
68     *
69     * SELECT up_property, COUNT(*), count(qcc_title)
70     * FROM user_properties
71     * LEFT JOIN user ON up_user = user_id
72     * LEFT JOIN querycachetwo ON user_name = qcc_title AND qcc_namespace = 2 AND qcc_type = 'activeusers'
73     * WHERE up_property LIKE 'gadget-%' AND up_value NOT IN ('0','')
74     * GROUP BY up_property;
75     *
76     * @return array
77     */
78    public function getQueryInfo() {
79        $dbr = $this->dbProvider->getReplicaDatabase();
80
81        return [
82            'tables' => [ 'user_properties', 'user', 'querycachetwo' ],
83            'fields' => [
84                'title' => 'up_property',
85                'value' => 'COUNT(*)',
86                // Need to pick fields existing in the querycache table so that the results are cachable
87                'namespace' => 'COUNT( qcc_title )'
88            ],
89            'conds' => [
90                $dbr->expr( 'up_property', IExpression::LIKE, new LikeValue( 'gadget-', $dbr->anyString() ) ),
91                // Simulate php falsy condition to ignore disabled user preferences
92                $dbr->expr( 'up_value', '!=', [ '0', '' ] ),
93            ],
94            'options' => [
95                'GROUP BY' => [ 'up_property' ]
96            ],
97            'join_conds' => [
98                'user' => [
99                    'LEFT JOIN', [
100                        'up_user = user_id'
101                    ]
102                ],
103                'querycachetwo' => [
104                    'LEFT JOIN', [
105                        'user_name = qcc_title',
106                        'qcc_namespace' => NS_USER,
107                        'qcc_type' => 'activeusers',
108                    ]
109                ]
110            ]
111        ];
112    }
113
114    /** @inheritDoc */
115    public function getOrderFields() {
116        return [ 'value' ];
117    }
118
119    /**
120     * Output the start of the table
121     * Including opening <table>, the thead element with column headers
122     * and the opening <tbody>.
123     */
124    protected function outputTableStart() {
125        $html = '';
126        $headers = [ 'gadgetusage-gadget', 'gadgetusage-usercount', 'gadgetusage-activeusers' ];
127        foreach ( $headers as $h ) {
128            if ( $h === 'gadgetusage-gadget' ) {
129                $html .= Html::element( 'th', [], $this->msg( $h )->text() );
130            } else {
131                $html .= Html::element( 'th', [ 'data-sort-type' => 'number' ],
132                    $this->msg( $h )->text() );
133            }
134        }
135
136        $this->getOutput()->addHTML(
137            Html::openElement( 'table', [ 'class' => [ 'sortable', 'wikitable' ] ] ) .
138            Html::rawElement( 'thead', [], Html::rawElement( 'tr', [], $html ) ) .
139            Html::openElement( 'tbody', [] )
140        );
141        $this->getOutput()->addModuleStyles( 'jquery.tablesorter.styles' );
142        $this->getOutput()->addModules( 'jquery.tablesorter' );
143    }
144
145    /**
146     * Output the end of the table
147     * </tbody></table>
148     */
149    protected function outputTableEnd() {
150        $this->getOutput()->addHTML(
151            Html::closeElement( 'tbody' ) .
152            Html::closeElement( 'table' )
153        );
154    }
155
156    /**
157     * @param Skin $skin
158     * @param stdClass $result Result row
159     * @return string|bool String of HTML
160     */
161    public function formatResult( $skin, $result ) {
162        $gadgetTitle = substr( $result->title, 7 );
163        $gadgetUserCount = $this->getLanguage()->formatNum( $result->value );
164        if ( $gadgetTitle ) {
165            $html = '';
166            // "Gadget" column
167            $link = $this->getLinkRenderer()->makeLink(
168                new TitleValue( NS_SPECIAL, 'Gadgets', 'gadget-' . $gadgetTitle ),
169                $gadgetTitle
170            );
171            $html .= Html::rawElement( 'td', [], $link );
172            // "Number of users" column
173            $html .= Html::element( 'td', [], $gadgetUserCount );
174            // "Active users" column
175            $activeUserCount = $this->getLanguage()->formatNum( $result->namespace );
176            $html .= Html::element( 'td', [], $activeUserCount );
177            return Html::rawElement( 'tr', [], $html );
178        }
179        return false;
180    }
181
182    /**
183     * Get a list of default gadgets
184     * @param array $gadgetIds list of gagdet ids registered in the wiki
185     * @return array
186     */
187    protected function getDefaultGadgets( $gadgetIds ) {
188        $gadgetsList = [];
189        foreach ( $gadgetIds as $g ) {
190            $gadget = $this->gadgetRepo->getGadget( $g );
191            if ( $gadget->isOnByDefault() ) {
192                $gadgetsList[] = $gadget->getName();
193            }
194        }
195        asort( $gadgetsList, SORT_STRING | SORT_FLAG_CASE );
196        return $gadgetsList;
197    }
198
199    /**
200     * Format and output report results using the given information plus
201     * OutputPage
202     *
203     * @param OutputPage $out OutputPage to print to
204     * @param Skin $skin User skin to use
205     * @param IReadableDatabase $dbr Database (read) connection to use
206     * @param IResultWrapper $res Result pointer
207     * @param int $num Number of available result rows
208     * @param int $offset Paging offset
209     */
210    protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
211        $gadgetIds = $this->gadgetRepo->getGadgetIds();
212        $defaultGadgets = $this->getDefaultGadgets( $gadgetIds );
213        $out->addHtml(
214            $this->msg( 'gadgetusage-intro' )
215                ->numParams( $this->getConfig()->get( 'ActiveUserDays' ) )->parseAsBlock()
216        );
217        if ( $num > 0 ) {
218            $this->outputTableStart();
219            // Append default gadgets to the table with 'default' in the total and active user fields
220            foreach ( $defaultGadgets as $default ) {
221                $html = '';
222                // "Gadget" column
223                $link = $this->getLinkRenderer()->makeLink(
224                    new TitleValue( NS_SPECIAL, 'Gadgets', 'gadget-' . $default ),
225                    $default
226                );
227                $html .= Html::rawElement( 'td', [], $link );
228                // "Number of users" column
229                $html .= Html::element( 'td', [ 'data-sort-value' => 'Infinity' ],
230                    $this->msg( 'gadgetusage-default' )->text() );
231                // "Active users" column
232                // @phan-suppress-next-line PhanPluginDuplicateAdjacentStatement
233                $html .= Html::element( 'td', [ 'data-sort-value' => 'Infinity' ],
234                    $this->msg( 'gadgetusage-default' )->text() );
235                $out->addHTML( Html::rawElement( 'tr', [], $html ) );
236            }
237            foreach ( $res as $row ) {
238                // Remove the 'gadget-' part of the result string and compare if it's present
239                // in $defaultGadgets, if not we format it and add it to the output
240                $name = substr( $row->title, 7 );
241
242                // Only pick gadgets which are in the list $gadgetIds to make sure they exist
243                if ( !in_array( $name, $defaultGadgets, true ) && in_array( $name, $gadgetIds, true ) ) {
244                    $line = $this->formatResult( $skin, $row );
245                    if ( $line ) {
246                        $out->addHTML( $line );
247                    }
248                }
249            }
250            // Close table element
251            $this->outputTableEnd();
252        } else {
253            $out->addHtml(
254                $this->msg( 'gadgetusage-noresults' )->parseAsBlock()
255            );
256        }
257    }
258
259    /**
260     * @inheritDoc
261     */
262    protected function getGroupName() {
263        return 'wiki';
264    }
265}