Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 132
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
TranslationStatsSpecialPage
0.00% covered (danger)
0.00%
0 / 132
0.00% covered (danger)
0.00%
0 / 13
812
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isIncludable
0.00% covered (danger)
0.00%
0 / 1
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
 execute
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 form
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
56
 eInput
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 eLabel
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 eRadio
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 eLanguage
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 languageSelector
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 eGroup
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 groupSelector
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 embed
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\Statistics;
5
6use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
7use MediaWiki\Extension\Translate\Utilities\JsSelectToInput;
8use MediaWiki\Extension\Translate\Utilities\Utilities;
9use MediaWiki\Html\FormOptions;
10use MediaWiki\Html\Html;
11use SpecialPage;
12use Xml;
13use XmlSelect;
14use function wfEscapeWikiText;
15
16/**
17 * Includable special page for generating graphs for statistics.
18 *
19 * @file
20 * @author Niklas Laxström
21 * @author Siebrand Mazeland
22 * @license GPL-2.0-or-later
23 */
24class TranslationStatsSpecialPage extends SpecialPage {
25    /** @var TranslationStatsDataProvider */
26    private $dataProvider;
27    private const GRAPH_CONTAINER_ID = 'translationStatsGraphContainer';
28    private const GRAPH_CONTAINER_CLASS = 'mw-translate-translationstats-container';
29
30    public function __construct( TranslationStatsDataProvider $dataProvider ) {
31        parent::__construct( 'TranslationStats' );
32        $this->dataProvider = $dataProvider;
33    }
34
35    /** @inheritDoc */
36    public function isIncludable(): bool {
37        return true;
38    }
39
40    /** @inheritDoc */
41    protected function getGroupName(): string {
42        return 'translation';
43    }
44
45    /** @inheritDoc */
46    public function execute( $par ): void {
47        $graphOpts = new TranslationStatsGraphOptions();
48        $graphOpts->bindArray( $this->getRequest()->getValues() );
49
50        $pars = explode( ';', (string)$par );
51        foreach ( $pars as $item ) {
52            if ( !str_contains( $item, '=' ) ) {
53                continue;
54            }
55
56            [ $key, $value ] = array_map( 'trim', explode( '=', $item, 2 ) );
57            if ( $graphOpts->hasValue( $key ) ) {
58                $graphOpts->setValue( $key, $value );
59            }
60        }
61
62        $graphOpts->normalize( $this->dataProvider->getGraphTypes() );
63        $opts = $graphOpts->getFormOptions();
64
65        if ( $this->including() ) {
66            $this->getOutput()->addHTML( $this->embed( $opts ) );
67        } else {
68            $this->form( $opts );
69        }
70    }
71
72    /**
73     * Constructs the form which can be used to generate custom graphs.
74     *
75     * @suppress SecurityCheck-DoubleEscaped Intentionally outputting what user should type
76     */
77    private function form( FormOptions $opts ): void {
78        $script = $this->getConfig()->get( 'Script' );
79
80        $this->setHeaders();
81        $out = $this->getOutput();
82        $out->addModules( 'ext.translate.special.translationstats' );
83        $out->addHelpLink( 'Help:Extension:Translate/Statistics_and_reporting' );
84        $out->addWikiMsg( 'translate-statsf-intro' );
85        $out->addHTML(
86            Xml::fieldset( $this->msg( 'translate-statsf-options' )->text() ) . Html::openElement(
87                'form',
88                [ 'action' => $script, 'id' => 'translationStatsConfig' ]
89            ) . Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
90            Html::hidden( 'preview', 1 ) . '<table>'
91        );
92        $submit = Xml::submitButton( $this->msg( 'translate-statsf-submit' )->text() );
93        $out->addHTML(
94            $this->eInput( 'width', $opts ) . $this->eInput( 'height', $opts ) .
95            '<tr><td colspan="2"><hr /></td></tr>' . $this->eInput( 'start', $opts, 24 ) .
96            $this->eInput( 'days', $opts ) .
97            $this->eRadio( 'scale', $opts, [ 'years', 'months', 'weeks', 'days', 'hours' ] ) .
98            $this->eRadio( 'count', $opts, $this->dataProvider->getGraphTypes() ) .
99            '<tr><td colspan="2"><hr /></td></tr>' . $this->eLanguage( 'language', $opts ) .
100            $this->eGroup( 'group', $opts ) . '<tr><td colspan="2"><hr /></td></tr>' .
101            '<tr><td colspan="2">' . $submit . '</td></tr>'
102        );
103        $out->addHTML( '</table></form></fieldset>' );
104        if ( !$opts['preview'] ) {
105            return;
106        }
107        $spiParams = [];
108        foreach ( $opts->getChangedValues() as $key => $v ) {
109            if ( $key === 'preview' ) {
110                continue;
111            }
112            if ( is_array( $v ) ) {
113                $v = implode( ',', $v );
114                if ( !strlen( $v ) ) {
115                    continue;
116                }
117            }
118            $spiParams[] = $key . '=' . wfEscapeWikiText( $v );
119        }
120
121        $spiParams = $spiParams ? '/' . implode( ';', $spiParams ) : '';
122
123        $titleText = $this->getPageTitle()->getPrefixedText();
124        $out->addHTML( Html::element( 'hr' ) );
125        // Element to render the graph
126        $out->addHTML(
127            Html::rawElement(
128                'div',
129                [
130                    'id' => self::GRAPH_CONTAINER_ID,
131                    'style' => 'margin: 2em auto; display: block',
132                    'class' => self::GRAPH_CONTAINER_CLASS,
133                ]
134            )
135        );
136
137        $out->addHTML(
138            Html::element(
139                'pre',
140                [ 'aria-label' => $this->msg( 'translate-statsf-embed' )->text() ],
141                "{{{$titleText}{$spiParams}}}"
142            )
143        );
144    }
145
146    /// Construct HTML for a table row with label and input in two columns.
147    private function eInput( string $name, FormOptions $opts, int $width = 4 ): string {
148        $value = $opts[$name];
149        return '<tr><td>' . $this->eLabel( $name ) . '</td><td>' .
150            Xml::input( $name, $width, $value, [ 'id' => $name ] ) . '</td></tr>' . "\n";
151    }
152
153    /// Construct HTML for a label for option.
154    private function eLabel( string $name ): string {
155        // Give grep a chance to find the usages:
156        // translate-statsf-width, translate-statsf-height, translate-statsf-start,
157        // translate-statsf-days, translate-statsf-scale, translate-statsf-count,
158        // translate-statsf-language, translate-statsf-group
159        $label = 'translate-statsf-' . $name;
160        $label = $this->msg( $label )->escaped();
161        return Xml::tags( 'label', [ 'for' => $name ], $label );
162    }
163
164    /// Construct HTML for a table row with label and radio input in two columns.
165    private function eRadio( string $name, FormOptions $opts, array $alts ): string {
166        // Give grep a chance to find the usages:
167        // translate-statsf-scale, translate-statsf-count
168        $label = 'translate-statsf-' . $name;
169        $label = $this->msg( $label )->escaped();
170        $s = '<tr><td>' . $label . '</td><td>';
171        $options = [];
172        foreach ( $alts as $alt ) {
173            $id = "$name-$alt";
174            $radio = Xml::radio(
175                    $name,
176                    $alt,
177                    $alt === $opts[$name],
178                    [ 'id' => $id ]
179                ) . ' ';
180            $options[] = $radio . ' ' . $this->eLabel( $id );
181        }
182        $s .= implode( ' ', $options );
183        $s .= '</td></tr>' . "\n";
184        return $s;
185    }
186
187    /// Construct HTML for a table row with label and language selector in two columns.
188    private function eLanguage( string $name, FormOptions $opts ): string {
189        $value = implode( ',', $opts[$name] );
190
191        $select = $this->languageSelector();
192        $select->setTargetId( 'language' );
193        return '<tr><td>' . $this->eLabel( $name ) . '</td><td>' . $select->getHtmlAndPrepareJS() .
194            '<br />' . Xml::input( $name, 20, $value, [ 'id' => $name ] ) . '</td></tr>' . "\n";
195    }
196
197    /// Construct a JavaScript enhanced language selector.
198    private function languageSelector(): JsSelectToInput {
199        $languages = Utilities::getLanguageNames( $this->getLanguage()->getCode() );
200        ksort( $languages );
201        $selector = new XmlSelect( 'mw-language-selector', 'mw-language-selector' );
202        foreach ( $languages as $code => $name ) {
203            $selector->addOption( "$code - $name", $code );
204        }
205        return new JsSelectToInput( $selector );
206    }
207
208    /// Constructs HTML for a table row with label and group selector in two columns.
209    private function eGroup( string $name, FormOptions $opts ): string {
210        $value = implode( ',', $opts[$name] );
211
212        $select = $this->groupSelector();
213        $select->setTargetId( 'group' );
214        return '<tr><td>' . $this->eLabel( $name ) . '</td><td>' . $select->getHtmlAndPrepareJS() .
215            '<br />' . Xml::input( $name, 20, $value, [ 'id' => $name ] ) . '</td></tr>' . "\n";
216    }
217
218    /// Construct a JavaScript enhanced group selector.
219    private function groupSelector(): JsSelectToInput {
220        $groups = MessageGroups::singleton()->getGroups();
221        foreach ( $groups as $key => $group ) {
222            if ( !$group->exists() ) {
223                unset( $groups[$key] );
224            }
225        }
226        ksort( $groups );
227        $selector = new XmlSelect( 'mw-group-selector', 'mw-group-selector' );
228        foreach ( $groups as $code => $name ) {
229            $selector->addOption( $name->getLabel(), $code );
230        }
231        return new JsSelectToInput( $selector );
232    }
233
234    private function embed( FormOptions $opts ): string {
235        $this->getOutput()->addModules( 'ext.translate.translationstats.embedded' );
236        return Html::rawElement(
237            'div',
238            [
239                'class' => self::GRAPH_CONTAINER_CLASS,
240            ],
241            Html::hidden(
242                'translationStatsGraphOptions',
243                json_encode( $opts->getAllValues() )
244            )
245        );
246    }
247}