Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 156
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialStatistics
0.00% covered (danger)
0.00%
0 / 155
0.00% covered (danger)
0.00%
0 / 10
812
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
12
 formatRow
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 getPageStats
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
20
 getEditStats
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 getUserStats
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupStats
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
30
 getOtherStats
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
56
 formatRowHeader
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Specials;
8
9use MediaWiki\Html\Html;
10use MediaWiki\MainConfigNames;
11use MediaWiki\Parser\Sanitizer;
12use MediaWiki\SiteStats\SiteStats;
13use MediaWiki\SpecialPage\SpecialPage;
14use MediaWiki\Title\Title;
15use MediaWiki\User\UserGroupManager;
16use MediaWiki\User\UserGroupMembership;
17
18/**
19 * Special page lists various statistics, including the contents of
20 * `site_stats`, plus page view details if enabled
21 *
22 * @ingroup SpecialPage
23 */
24class SpecialStatistics extends SpecialPage {
25    private int $edits;
26    private int $good;
27    private int $images;
28    private int $total;
29    private int $users;
30    private int $activeUsers;
31
32    public function __construct(
33        private readonly UserGroupManager $userGroupManager
34    ) {
35        parent::__construct( 'Statistics' );
36    }
37
38    /** @inheritDoc */
39    public function execute( $par ) {
40        $this->setHeaders();
41        $this->outputHeader();
42        $this->getOutput()->addModuleStyles( 'mediawiki.special' );
43
44        $this->edits = SiteStats::edits();
45        $this->good = SiteStats::articles();
46        $this->images = SiteStats::images();
47        $this->total = SiteStats::pages();
48        $this->users = SiteStats::users();
49        $this->activeUsers = SiteStats::activeUsers();
50
51        $text = Html::openElement( 'table', [ 'class' => [ 'wikitable', 'mw-statistics-table' ] ] );
52
53        # Statistic - pages
54        $text .= $this->getPageStats();
55
56        # Statistic - edits
57        $text .= $this->getEditStats();
58
59        # Statistic - users
60        $text .= $this->getUserStats();
61
62        # Statistic - usergroups
63        $text .= $this->getGroupStats();
64
65        # Statistic - other
66        $extraStats = [];
67        if ( $this->getHookRunner()->onSpecialStatsAddExtra(
68            $extraStats, $this->getContext() )
69        ) {
70            $text .= $this->getOtherStats( $extraStats );
71        }
72
73        $text .= Html::closeElement( 'table' );
74
75        # Customizable footer
76        $footer = $this->msg( 'statistics-footer' );
77        if ( !$footer->isBlank() ) {
78            $text .= "\n" . $footer->parse();
79        }
80
81        $this->getOutput()->addHTML( $text );
82    }
83
84    /**
85     * Format a row
86     * @param string $text Description of the row
87     * @param float|string $number A statistical number
88     * @param array $trExtraParams Params to table row, see Html::element
89     * @param string $descMsg Message key
90     * @param array|string $descMsgParam Message parameters
91     * @return string Table row in HTML format
92     */
93    private function formatRow( $text, $number, $trExtraParams = [],
94        $descMsg = '', $descMsgParam = ''
95    ) {
96        if ( $descMsg ) {
97            $msg = $this->msg( $descMsg, $descMsgParam );
98            if ( !$msg->isDisabled() ) {
99                $descriptionHtml = $this->msg( 'parentheses' )->rawParams( $msg->parse() )
100                    ->escaped();
101                $text .= "<br />" . Html::rawElement(
102                    'small',
103                    [ 'class' => 'mw-statistic-desc' ],
104                    " $descriptionHtml"
105                );
106            }
107        }
108
109        return Html::rawElement( 'tr', $trExtraParams,
110            Html::rawElement( 'td', [], $text ) .
111            Html::rawElement( 'td', [ 'class' => 'mw-statistics-numbers' ], $number )
112        );
113    }
114
115    /**
116     * Each of these methods is pretty self-explanatory, get a particular
117     * row for the table of statistics
118     * @return string
119     */
120    private function getPageStats() {
121        $linkRenderer = $this->getLinkRenderer();
122
123        $specialAllPagesTitle = SpecialPage::getTitleFor( 'Allpages' );
124        $pageStatsHtml = Html::rawElement( 'tr', [],
125            Html::rawElement( 'th', [ 'colspan' => '2' ],
126                $this->msg( 'statistics-header-pages' )->parse()
127            ) ) .
128                $this->formatRow(
129                    $this->getConfig()->get( MainConfigNames::MiserMode )
130                        ? $this->msg( 'statistics-articles' )->escaped()
131                        : $linkRenderer->makeKnownLink(
132                            $specialAllPagesTitle,
133                            $this->msg( 'statistics-articles' )->text(),
134                            [], [ 'hideredirects' => 1 ] ),
135                    $this->getLanguage()->formatNum( $this->good ),
136                    [ 'class' => 'mw-statistics-articles' ],
137                    'statistics-articles-desc' ) .
138                $this->formatRow( $linkRenderer->makeKnownLink( $specialAllPagesTitle,
139                    $this->msg( 'statistics-pages' )->text() ),
140                    $this->getLanguage()->formatNum( $this->total ),
141                    [ 'class' => 'mw-statistics-pages' ],
142                    'statistics-pages-desc' );
143
144        // Show the image row only, when there are files or upload is possible
145        if ( $this->images !== 0 || $this->getConfig()->get( MainConfigNames::EnableUploads ) ) {
146            $pageStatsHtml .= $this->formatRow(
147                $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'MediaStatistics' ),
148                $this->msg( 'statistics-files' )->text() ),
149                $this->getLanguage()->formatNum( $this->images ),
150                [ 'class' => 'mw-statistics-files' ], 'statistics-files-desc' );
151        }
152
153        return $pageStatsHtml;
154    }
155
156    private function getEditStats(): string {
157        return Html::rawElement( 'tr', [],
158            Html::rawElement( 'th', [ 'colspan' => '2' ],
159                $this->msg( 'statistics-header-edits' )->parse()
160            ) ) .
161            $this->formatRow( $this->msg( 'statistics-edits' )->parse(),
162                $this->getLanguage()->formatNum( $this->edits ),
163                [ 'class' => 'mw-statistics-edits' ]
164            ) .
165            $this->formatRow( $this->msg( 'statistics-edits-average' )->parse(),
166                $this->getLanguage()->formatNum(
167                    sprintf( '%.2f', $this->total ? $this->edits / $this->total : 0 )
168                ), [ 'class' => 'mw-statistics-edits-average' ]
169            );
170    }
171
172    private function getUserStats(): string {
173        return Html::rawElement( 'tr', [],
174            Html::rawElement( 'th', [ 'colspan' => '2' ],
175                $this->msg( 'statistics-header-users' )->parse()
176            ) ) .
177            $this->formatRow( $this->msg( 'statistics-users' )->parse() . ' ' .
178                $this->getLinkRenderer()->makeKnownLink(
179                    SpecialPage::getTitleFor( 'Listusers' ),
180                    $this->msg( 'listgrouprights-members' )->text()
181                ),
182                $this->getLanguage()->formatNum( $this->users ),
183                [ 'class' => 'mw-statistics-users' ]
184            ) .
185            $this->formatRow( $this->msg( 'statistics-users-active' )->parse() . ' ' .
186                $this->getLinkRenderer()->makeKnownLink(
187                    SpecialPage::getTitleFor( 'Activeusers' ),
188                    $this->msg( 'listgrouprights-members' )->text()
189                ),
190                $this->getLanguage()->formatNum( $this->activeUsers ),
191                [ 'class' => 'mw-statistics-users-active' ],
192                'statistics-users-active-desc',
193                $this->getLanguage()->formatNum(
194                    $this->getConfig()->get( MainConfigNames::ActiveUserDays ) )
195            );
196    }
197
198    private function getGroupStats(): string {
199        $linkRenderer = $this->getLinkRenderer();
200        $lang = $this->getLanguage();
201        $text = '';
202        foreach ( $this->userGroupManager->listAllGroups() as $group ) {
203            $groupnameLocalized = $lang->getGroupName( $group );
204            $linkTarget = UserGroupMembership::getGroupPage( $group )
205                ?: Title::makeTitleSafe( NS_PROJECT, $group );
206
207            if ( $linkTarget ) {
208                $grouppage = $linkRenderer->makeLink(
209                    $linkTarget,
210                    $groupnameLocalized
211                );
212            } else {
213                $grouppage = htmlspecialchars( $groupnameLocalized );
214            }
215
216            $grouplink = $linkRenderer->makeKnownLink(
217                SpecialPage::getTitleFor( 'Listusers' ),
218                $this->msg( 'listgrouprights-members' )->text(),
219                [],
220                [ 'group' => $group ]
221            );
222            # Add a class when a usergroup contains no members to allow hiding these rows
223            $classZero = '';
224            $countUsers = SiteStats::numberingroup( $group );
225            if ( $countUsers == 0 ) {
226                $classZero = ' statistics-group-zero';
227            }
228            $text .= $this->formatRow( $grouppage . ' ' . $grouplink,
229                $this->getLanguage()->formatNum( $countUsers ),
230                [ 'class' => 'statistics-group-' . Sanitizer::escapeClass( $group ) .
231                    $classZero ] );
232        }
233
234        return $text;
235    }
236
237    /**
238     * Conversion of external statistics into an internal representation
239     * Following a ([<header-message>][<item-message>] = number) pattern
240     *
241     * @param array $stats
242     * @return string
243     */
244    private function getOtherStats( array $stats ) {
245        $return = '';
246
247        foreach ( $stats as $header => $items ) {
248            // Identify the structure used
249            if ( is_array( $items ) ) {
250                // Ignore headers that are recursively set as legacy header
251                if ( $header !== 'statistics-header-hooks' ) {
252                    $return .= $this->formatRowHeader( $header );
253                }
254
255                // Collect all items that belong to the same header
256                foreach ( $items as $key => $value ) {
257                    if ( is_array( $value ) ) {
258                        $name = $value['name'];
259                        $number = $value['number'];
260                    } else {
261                        $name = $this->msg( $key )->parse();
262                        $number = $value;
263                    }
264
265                    $return .= $this->formatRow(
266                        $name,
267                        $this->getLanguage()->formatNum( htmlspecialchars( $number ) ),
268                        [ 'class' => 'mw-statistics-hook', 'id' => 'mw-' . $key ]
269                    );
270                }
271            } else {
272                // Create the legacy header only once
273                if ( $return === '' ) {
274                    $return .= $this->formatRowHeader( 'statistics-header-hooks' );
275                }
276
277                // Recursively remap the legacy structure
278                $return .= $this->getOtherStats( [ 'statistics-header-hooks' =>
279                    [ $header => $items ] ] );
280            }
281        }
282
283        return $return;
284    }
285
286    /**
287     * Format row header
288     *
289     * @param string $header
290     * @return string
291     */
292    private function formatRowHeader( $header ) {
293        return Html::rawElement( 'tr', [],
294            Html::rawElement( 'th', [ 'colspan' => '2' ], $this->msg( $header )->parse() )
295        );
296    }
297
298    /** @inheritDoc */
299    protected function getGroupName() {
300        return 'wiki';
301    }
302}
303
304/**
305 * Retain the old class name for backwards compatibility.
306 * @deprecated since 1.41
307 */
308class_alias( SpecialStatistics::class, 'SpecialStatistics' );