Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 99
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ManageTranslatorSandboxSpecialPage
0.00% covered (danger)
0.00%
0 / 99
0.00% covered (danger)
0.00%
0 / 11
306
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
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 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 showPage
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 makeFilter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeSearchBox
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 makeList
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
30
 makeRequestItem
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 getHumanTimestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 translatorRequestSort
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\TranslatorSandbox;
5
6use FormatJson;
7use MediaWiki\Config\ServiceOptions;
8use MediaWiki\Html\Html;
9use MediaWiki\User\UserOptionsLookup;
10use MWTimestamp;
11use Sanitizer;
12use SpecialPage;
13use User;
14
15/**
16 * Special page for managing sandboxed users.
17 *
18 * @author Niklas Laxström
19 * @author Amir E. Aharoni
20 * @license GPL-2.0-or-later
21 * @ingroup SpecialPage TranslateSpecialPage
22 */
23class ManageTranslatorSandboxSpecialPage extends SpecialPage {
24    /** @var TranslationStashReader */
25    private $stash;
26    /** @var UserOptionsLookup */
27    private $userOptionsLookup;
28    private TranslateSandbox $translateSandbox;
29
30    public const CONSTRUCTOR_OPTIONS = [
31        'TranslateUseSandbox',
32    ];
33
34    public function __construct(
35        TranslationStashReader $stash,
36        UserOptionsLookup $userOptionsLookup,
37        TranslateSandbox $translateSandbox,
38        ServiceOptions $options
39    ) {
40        $this->stash = $stash;
41        $this->userOptionsLookup = $userOptionsLookup;
42        $this->translateSandbox = $translateSandbox;
43
44        parent::__construct(
45            'ManageTranslatorSandbox',
46            'translate-sandboxmanage',
47            $options->get( 'TranslateUseSandbox' )
48        );
49    }
50
51    public function doesWrites() {
52        return true;
53    }
54
55    protected function getGroupName() {
56        return 'translation';
57    }
58
59    public function execute( $params ) {
60        $this->setHeaders();
61        $this->checkPermissions();
62        $out = $this->getOutput();
63        $out->addModuleStyles(
64            [
65                'ext.translate.special.managetranslatorsandbox.styles',
66                'mediawiki.ui.button',
67                'jquery.uls.grid',
68            ]
69        );
70        $out->addModules( 'ext.translate.special.managetranslatorsandbox' );
71
72        $this->showPage();
73    }
74
75    /** Generates the whole page html and appends it to output */
76    private function showPage(): void {
77        $out = $this->getOutput();
78
79        $nojs = Html::errorBox(
80            $this->msg( 'tux-nojs' )->escaped(),
81            '',
82            'tux-nojs'
83        );
84        $out->addHTML( $nojs );
85
86        $out->addHTML(
87            <<<HTML
88                <div class="grid tsb-container">
89                    <div class="row">
90                        <div class="nine columns pane filter">{$this->makeFilter()}</div>
91                        <div class="three columns pane search">{$this->makeSearchBox()}</div>
92                    </div>
93                    <div class="row tsb-body">
94                        <div class="four columns pane requests">
95                            {$this->makeList()}
96                            <div class="request-footer">
97                                <span class="selected-counter">
98                                    {$this->msg( 'tsb-selected-count' )->numParams( 0 )->escaped()}
99                                </span>
100                                \u{00A0}
101                                <a href="#" class="older-requests-indicator"></a>
102                            </div>
103                        </div>
104                        <div class="eight columns pane details"></div>
105                    </div>
106                </div>
107                HTML
108        );
109    }
110
111    private function makeFilter(): string {
112        return $this->msg( 'tsb-filter-pending' )->escaped();
113    }
114
115    private function makeSearchBox(): string {
116        return <<<HTML
117            <input class="request-filter-box right"
118                placeholder="{$this->msg( 'tsb-search-requests' )->escaped()}" type="search" />
119            HTML;
120    }
121
122    private function makeList(): string {
123        $items = [];
124        $requests = [];
125        $users = $this->translateSandbox->getUsers();
126
127        /** @var User $user */
128        foreach ( $users as $user ) {
129            $reminders = $this->userOptionsLookup->getOption( $user, 'translate-sandbox-reminders' );
130            $reminders = $reminders ? explode( '|', $reminders ) : [];
131            $remindersCount = count( $reminders );
132            if ( $remindersCount ) {
133                $lastReminderTimestamp = new MWTimestamp( end( $reminders ) );
134                $lastReminderAgo = htmlspecialchars(
135                    $this->getHumanTimestamp( $lastReminderTimestamp )
136                );
137            } else {
138                $lastReminderAgo = '';
139            }
140
141            $requests[] = [
142                'username' => $user->getName(),
143                'email' => $user->getEmail(),
144                'gender' => $this->userOptionsLookup->getOption( $user, 'gender' ),
145                'registrationdate' => $user->getRegistration(),
146                'translations' => count( $this->stash->getTranslations( $user ) ),
147                'languagepreferences' => FormatJson::decode(
148                    $this->userOptionsLookup->getOption( $user, 'translate-sandbox' )
149                ),
150                'userid' => $user->getId(),
151                'reminderscount' => $remindersCount,
152                'lastreminder' => $lastReminderAgo,
153            ];
154        }
155
156        // Sort the requests based on translations and registration date
157        usort( $requests, [ $this, 'translatorRequestSort' ] );
158
159        foreach ( $requests as $request ) {
160            $items[] = $this->makeRequestItem( $request );
161        }
162
163        $requestsList = implode( "\n", $items );
164
165        return <<<HTML
166            <div class="row request-header">
167                <div class="four columns">
168                    <button class="language-selector unselected">
169                        {$this->msg( 'tsb-all-languages-button-label' )->escaped()}
170                    </button>
171                </div>
172                <div class="five columns request-count"></div>
173                <div class="three columns text-center">
174                    <input class="request-selector-all" name="request" type="checkbox" />
175                </div>
176            </div>
177            <div class="requests-list">
178                {$requestsList}
179            </div>
180            HTML;
181    }
182
183    private function makeRequestItem( array $request ): string {
184        $requestdataEnc = htmlspecialchars( FormatJson::encode( $request ) );
185        $nameEnc = htmlspecialchars( $request['username'] );
186        $nameEncForId =
187            htmlspecialchars(
188                Sanitizer::escapeIdForAttribute( 'tsb-request-' . $request['username'] )
189            );
190        $emailEnc = htmlspecialchars( $request['email'] );
191        $countEnc = htmlspecialchars( (string)$request['translations'] );
192        $timestamp = new MWTimestamp( $request['registrationdate'] );
193        $agoEnc = htmlspecialchars( $this->getHumanTimestamp( $timestamp ) );
194
195        return <<<HTML
196            <div class="row request" data-data="$requestdataEnc" id="$nameEncForId">
197                <div class="two columns amount">
198                    <div class="translation-count">$countEnc</div>
199                </div>
200                <div class="seven columns request-info">
201                    <div class="row username">$nameEnc</div>
202                    <div class="row email" dir="ltr">$emailEnc</div>
203                </div>
204                <div class="three columns approval text-center">
205                    <input class="row request-selector" name="request" type="checkbox" />
206                    <div class="row signup-age">$agoEnc</div>
207                </div>
208            </div>
209            HTML;
210    }
211
212    private function getHumanTimestamp( MWTimestamp $ts ): string {
213        return $this->getLanguage()->getHumanTimestamp( $ts, null, $this->getUser() );
214    }
215
216    /**
217     * Sorts groups by descending order of number of translations,
218     * registration date and username
219     */
220    private function translatorRequestSort( array $a, array $b ): int {
221        return $b['translations'] <=> $a['translations']
222            ?: $b['registrationdate'] <=> $a['registrationdate']
223                ?: strnatcasecmp( $a['username'], $b['username'] );
224    }
225}