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