Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 211
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
AggregateGroupsSpecialPage
0.00% covered (danger)
0.00%
0 / 211
0.00% covered (danger)
0.00%
0 / 9
870
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
 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 / 23
0.00% covered (danger)
0.00%
0 / 1
56
 showAggregateGroup
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 1
20
 showAggregateGroups
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
12
 listSubgroups
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
56
 htmlIdForGroup
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupToggleIcon
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 getLanguageSelector
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use AggregateMessageGroup;
7use MediaWiki\Cache\LinkBatchFactory;
8use MediaWiki\Extension\Translate\MessageProcessing\MessageGroupMetadata;
9use MediaWiki\Extension\Translate\Utilities\Utilities;
10use MediaWiki\Html\Html;
11use SpecialPage;
12use WikiPageMessageGroup;
13use Xml;
14use XmlSelect;
15
16/**
17 * Contains logic for special page Special:AggregateGroups.
18 *
19 * @author Santhosh Thottingal
20 * @author Niklas Laxström
21 * @author Siebrand Mazeland
22 * @author Kunal Grover
23 * @license GPL-2.0-or-later
24 */
25class AggregateGroupsSpecialPage extends SpecialPage {
26    /** @var bool */
27    private $hasPermission = false;
28    /** @var LinkBatchFactory */
29    private $linkBatchFactory;
30    /** @var XmlSelect */
31    private $languageSelector;
32    /** @var MessageGroupMetadata */
33    private $messageGroupMetadata;
34
35    public function __construct( LinkBatchFactory $linkBatchFactory, MessageGroupMetadata $messageGroupMetadata ) {
36        parent::__construct( 'AggregateGroups', 'translate-manage' );
37        $this->linkBatchFactory = $linkBatchFactory;
38        $this->messageGroupMetadata = $messageGroupMetadata;
39    }
40
41    protected function getGroupName(): string {
42        return 'translation';
43    }
44
45    public function execute( $parameters ): void {
46        $this->setHeaders();
47        $this->addHelpLink( 'Help:Extension:Translate/Page translation administration' );
48
49        $out = $this->getOutput();
50        $out->addModuleStyles( 'ext.translate.specialpages.styles' );
51
52        // Check permissions
53        if ( $this->getUser()->isAllowed( 'translate-manage' ) ) {
54            $this->hasPermission = true;
55        }
56
57        $groupsPreload = MessageGroups::getGroupsByType( AggregateMessageGroup::class );
58        $this->messageGroupMetadata->preloadGroups( array_keys( $groupsPreload ), __METHOD__ );
59
60        $groups = MessageGroups::getAllGroups();
61        uasort( $groups, [ MessageGroups::class, 'groupLabelSort' ] );
62        $aggregates = [];
63        $pages = [];
64        foreach ( $groups as $group ) {
65            if ( $group instanceof WikiPageMessageGroup ) {
66                $pages[] = $group;
67            } elseif ( $group instanceof AggregateMessageGroup ) {
68                // Filter out AggregateGroups configured in YAML
69                $subgroups = $this->messageGroupMetadata->getSubgroups( $group->getId() );
70                if ( $subgroups !== null ) {
71                    $aggregates[] = $group;
72                }
73            }
74        }
75
76        if ( !$pages ) {
77            // @todo Use different message
78            $out->addWikiMsg( 'tpt-list-nopages' );
79
80            return;
81        }
82
83        $this->showAggregateGroups( $aggregates );
84    }
85
86    protected function showAggregateGroup( AggregateMessageGroup $group ): string {
87        $id = $group->getId();
88        $label = $group->getLabel();
89        $desc = $group->getDescription( $this->getContext() );
90        $sourceLanguage = $this->messageGroupMetadata->get( $id, 'sourcelanguagecode' );
91
92        $edit = '';
93        $remove = '';
94        $editGroup = '';
95
96        // Add divs for editing Aggregate Groups
97        if ( $this->hasPermission ) {
98            // Group edit and remove buttons
99            $edit = Html::element( 'span', [ 'class' => 'tp-aggregate-edit-ag-button' ] );
100            $remove = Html::element( 'span', [ 'class' => 'tp-aggregate-remove-ag-button' ] );
101
102            // Edit group div
103            $languageSelector = $this->getLanguageSelector( 'edit', $sourceLanguage ?: '-' );
104
105            $editGroupNameLabel = $this->msg( 'tpt-aggregategroup-edit-name' )->escaped();
106            $editGroupName = Html::input(
107                'tp-agg-name',
108                $label,
109                'text',
110                [ 'class' => 'tp-aggregategroup-edit-name', 'maxlength' => '200' ]
111            );
112            $editGroupDescriptionLabel = $this->msg( 'tpt-aggregategroup-edit-description' )->escaped();
113            $editGroupDescription = Html::input(
114                'tp-agg-desc',
115                $desc,
116                'text',
117                [ 'class' => 'tp-aggregategroup-edit-description' ]
118            );
119            $saveButton = Xml::submitButton(
120                $this->msg( 'tpt-aggregategroup-update' )->text(),
121                [ 'class' => 'tp-aggregategroup-update' ]
122            );
123            $cancelButton = Xml::submitButton(
124                $this->msg( 'tpt-aggregategroup-update-cancel' )->text(),
125                [ 'class' => 'tp-aggregategroup-update-cancel' ]
126            );
127            $editGroup = Html::rawElement(
128                'div',
129                [ 'class' => 'tp-edit-group hidden' ],
130                $editGroupNameLabel .
131                $editGroupName .
132                '<br />' .
133                $editGroupDescriptionLabel .
134                $editGroupDescription .
135                '<br />' .
136                $languageSelector .
137                '<br />' .
138                $saveButton .
139                $cancelButton
140            );
141        }
142
143        // Not calling $parent->getGroups() because it has done filtering already
144        $subGroups = $this->messageGroupMetadata->getSubgroups( $id );
145        $shouldExpand = count( $subGroups ) <= 3;
146        $subGroupsId = $this->htmlIdForGroup( $group->getId(), 'tp-subgroup-' );
147
148        // Aggregate Group info div
149        $groupName = Html::rawElement(
150            'h2',
151            [ 'class' => 'tp-name' ],
152            $this->getGroupToggleIcon( $subGroupsId, $shouldExpand ) . htmlspecialchars( $label ) . $edit . $remove
153        );
154        $groupDesc = Html::element(
155            'p',
156            [ 'class' => 'tp-desc' ],
157            $desc
158        );
159        $groupInfo = Html::rawElement(
160            'div',
161            [ 'class' => 'tp-display-group' ],
162            $groupName . $groupDesc
163        );
164
165        $out = Html::openElement(
166            'div',
167            [
168                'class' => 'mw-tpa-group js-mw-tpa-group' . ( $shouldExpand ? ' mw-tpa-group-open' : '' ),
169                'data-groupid' => $id,
170                'data-id' => $this->htmlIdForGroup( $group->getId() )
171            ]
172        );
173        $out .= $groupInfo;
174        $out .= $editGroup;
175        $out .= Html::openElement( 'div', [ 'class' => 'tp-sub-groups', 'id' => $subGroupsId ] );
176        $out .= $this->listSubgroups( $id, $subGroups );
177        $out .= Html::closeElement( 'div' );
178        $out .= '</div>';
179
180        return $out;
181    }
182
183    /** @param AggregateMessageGroup[] $aggregates */
184    private function showAggregateGroups( array $aggregates ): void {
185        $out = $this->getOutput();
186        $out->addModules( 'ext.translate.special.aggregategroups' );
187
188        $nojs = Html::errorBox(
189            $this->msg( 'tux-nojs' )->escaped(),
190            '',
191            'tux-nojs'
192        );
193
194        $out->addHTML( $nojs );
195
196        // Add new group if user has permissions
197        if ( $this->hasPermission ) {
198            $out->addHTML(
199                "<a class='tpt-add-new-group' href='#'>" .
200                    $this->msg( 'tpt-aggregategroup-add-new' )->escaped() .
201                    '</a>'
202            );
203            $languageSelector = $this->getLanguageSelector( 'add', '-' );
204            $newGroupNameLabel = $this->msg( 'tpt-aggregategroup-new-name' )->escaped();
205            $newGroupName = Html::element( 'input', [ 'class' => 'tp-aggregategroup-add-name', 'maxlength' => '200' ] );
206            $newGroupDescriptionLabel = $this->msg( 'tpt-aggregategroup-new-description' )->escaped();
207            $newGroupDescription = Html::element( 'input', [ 'class' => 'tp-aggregategroup-add-description' ] );
208            $saveButton = Html::element(
209                'input',
210                [
211                    'type' => 'button',
212                    'value' => $this->msg( 'tpt-aggregategroup-save' )->text(),
213                    'id' => 'tpt-aggregategroups-save',
214                    'class' => 'tp-aggregate-save-button'
215                ]
216            );
217            $closeButton = Html::element(
218                'input',
219                [
220                    'type' => 'button',
221                    'value' => $this->msg( 'tpt-aggregategroup-close' )->text(),
222                    'id' => 'tpt-aggregategroups-close'
223                ]
224            );
225            $newGroupDiv = Html::rawElement(
226                'div',
227                [ 'class' => 'tpt-add-new-group hidden' ],
228                "$newGroupNameLabel $newGroupName<br />" .
229                "$newGroupDescriptionLabel $newGroupDescription<br />" .
230                "$languageSelector <br />"
231                . $saveButton
232                . $closeButton
233            );
234            $out->addHTML( $newGroupDiv );
235        }
236
237        $out->addHTML( Html::openElement( 'div', [ 'class' => 'mw-tpa-groups' ] ) );
238        foreach ( $aggregates as $group ) {
239            $out->addHTML( $this->showAggregateGroup( $group ) );
240        }
241        $out->addHTML( Html::closeElement( 'div' ) );
242    }
243
244    private function listSubgroups( string $groupId, array $subGroupIds ): string {
245        $id = $this->htmlIdForGroup( $groupId, 'mw-tpa-grouplist-' );
246        $out = Html::openElement( 'ol', [ 'id' => $id ] );
247
248        // Get the respective groups and sort them
249        $subgroups = MessageGroups::getGroupsById( $subGroupIds );
250        '@phan-var WikiPageMessageGroup[] $subgroups';
251        uasort( $subgroups, [ MessageGroups::class, 'groupLabelSort' ] );
252
253        // Avoid potentially thousands of separate database queries
254        $lb = $this->linkBatchFactory->newLinkBatch();
255        foreach ( $subgroups as $group ) {
256            $lb->addObj( $group->getTitle() );
257        }
258        $lb->setCaller( __METHOD__ );
259        $lb->execute();
260
261        // Add missing invalid group ids back, not returned by getGroupsById
262        foreach ( $subGroupIds as $id ) {
263            if ( !isset( $subgroups[$id] ) ) {
264                $subgroups[$id] = null;
265            }
266        }
267
268        foreach ( $subgroups as $id => $group ) {
269            $remove = '';
270            if ( $this->hasPermission ) {
271                $remove = Html::element(
272                    'span',
273                    [ 'class' => 'tp-aggregate-remove-button', 'data-groupid' => $id ]
274                );
275            }
276
277            if ( $group ) {
278                $text = $this->getLinkRenderer()->makeKnownLink( $group->getTitle() );
279                $note = htmlspecialchars( MessageGroups::getPriority( $id ) );
280            } else {
281                $text = htmlspecialchars( $id );
282                $note = $this->msg( 'tpt-aggregategroup-invalid-group' )->escaped();
283            }
284
285            $out .= Html::rawElement( 'li', [], "$text$remove $note" );
286        }
287        $out .= Html::closeElement( 'ol' );
288
289        return $out;
290    }
291
292    private function htmlIdForGroup( string $groupId, string $prefix = '' ): string {
293        $id = sha1( $groupId );
294        $id = substr( $id, 5, 8 );
295
296        return $prefix . $id;
297    }
298
299    private function getGroupToggleIcon( string $targetElementId, bool $shouldExpand ): string {
300        if ( $shouldExpand ) {
301            $title = $this->msg( 'tpt-aggregategroup-collapse-group' )->plain();
302        } else {
303            $title = $this->msg( 'tpt-aggregategroup-expand-group' )->plain();
304        }
305
306        return Html::rawElement(
307            'button',
308            [
309                'type' => 'button',
310                'title' => $title,
311                'class' => 'js-tp-toggle-groups tp-toggle-group-icon',
312                'aria-expanded' => $shouldExpand ? 'true' : 'false',
313                'aria-controls' => $targetElementId
314            ]
315        );
316    }
317
318    private function getLanguageSelector( string $action, string $languageToSelect ): string {
319        if ( $this->languageSelector == null ) {
320            // This should be set according to UI language
321            $this->languageSelector = Utilities::getLanguageSelector(
322                $this->getContext()->getLanguage()->getCode(),
323                '-'
324            );
325        }
326
327        $this->languageSelector->setAttribute( 'class', "tp-aggregategroup-$action-source-language" );
328        $this->languageSelector->setDefault( $languageToSelect );
329        $selector = $this->languageSelector->getHTML();
330
331        $languageSelectorLabel = $this->msg( 'tpt-aggregategroup-select-source-language' )->escaped();
332        return $languageSelectorLabel . $selector;
333    }
334}