Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 329
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMWOAuthManageConsumers
0.00% covered (danger)
0.00%
0 / 329
0.00% covered (danger)
0.00%
0 / 11
3306
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
 doesWrites
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 / 34
0.00% covered (danger)
0.00%
0 / 1
210
 addQueueSubtitleLinks
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 showMainHub
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
20
 handleConsumerForm
0.00% covered (danger)
0.00%
0 / 97
0.00% covered (danger)
0.00%
0 / 1
132
 getInfoTableOptions
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
132
 formatCallbackUrl
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 showConsumerList
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 formatRow
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
12
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\OAuth\Frontend\SpecialPages;
4
5/**
6 * (c) Aaron Schulz 2013, GPL
7 *
8 * @license GPL-2.0-or-later
9 */
10
11use MediaWiki\Context\IContextSource;
12use MediaWiki\Exception\ErrorPageError;
13use MediaWiki\Exception\PermissionsError;
14use MediaWiki\Extension\OAuth\Backend\Consumer;
15use MediaWiki\Extension\OAuth\Backend\Utils;
16use MediaWiki\Extension\OAuth\Control\ConsumerAccessControl;
17use MediaWiki\Extension\OAuth\Control\ConsumerSubmitControl;
18use MediaWiki\Extension\OAuth\Entity\ClientEntity;
19use MediaWiki\Extension\OAuth\Frontend\Pagers\ManageConsumersPager;
20use MediaWiki\Extension\OAuth\Frontend\UIUtils;
21use MediaWiki\Html\Html;
22use MediaWiki\HTMLForm\HTMLForm;
23use MediaWiki\Logging\LogEventsList;
24use MediaWiki\Logging\LogPage;
25use MediaWiki\Permissions\GrantsLocalization;
26use MediaWiki\Permissions\PermissionManager;
27use MediaWiki\SpecialPage\SpecialPage;
28use MediaWiki\Status\Status;
29use MediaWiki\Title\Title;
30use MediaWiki\Utils\UrlUtils;
31use MediaWiki\WikiMap\WikiMap;
32use MWRestrictions;
33use OOUI\HtmlSnippet;
34use stdClass;
35use Wikimedia\Rdbms\IDatabase;
36
37/**
38 * Special page for listing the queue of consumer requests and managing
39 * their approval/rejection and also for listing approved/disabled consumers
40 */
41class SpecialMWOAuthManageConsumers extends SpecialPage {
42    /** @var bool|int An Consumer::STAGE_* constant on queue/list subpages, false otherwise */
43    protected $stage = false;
44    /** @var string A stage key from Consumer::$stageNames */
45    protected $stageKey;
46
47    /**
48     * Stages which are shown in a queue (they are in an actionable state and can form a backlog)
49     * @var int[]
50     */
51    public static $queueStages = [ Consumer::STAGE_PROPOSED,
52        Consumer::STAGE_REJECTED, Consumer::STAGE_EXPIRED ];
53
54    /**
55     * Stages which cannot form a backlog and are shown in a list
56     * @var int[]
57     */
58    public static $listStages = [ Consumer::STAGE_APPROVED,
59        Consumer::STAGE_DISABLED ];
60
61    public function __construct(
62        private readonly GrantsLocalization $grantsLocalization,
63        private readonly PermissionManager $permissionManager,
64        private readonly UrlUtils $urlUtils,
65    ) {
66        parent::__construct( 'OAuthManageConsumers', 'mwoauthmanageconsumer' );
67    }
68
69    /** @inheritDoc */
70    public function doesWrites() {
71        return true;
72    }
73
74    /** @inheritDoc */
75    public function execute( $par ) {
76        $this->setHeaders();
77        $this->getOutput()->disallowUserJs();
78        $this->addHelpLink( 'Help:OAuth' );
79
80        if ( !Utils::isCentralWiki() ) {
81            $this->getOutput()->addWikiMsg( 'mwoauth-consumers-central-wiki' );
82            $wiki = WikiMap::getWiki( Utils::getCentralWiki() ?: WikiMap::getCurrentWikiId() );
83            if ( $wiki ) {
84                $this->getOutput()->addHTML( Html::element( 'a', [
85                    // Cross-wiki, so don't localize
86                    'href' => $wiki->getUrl( 'Special:OAuthManageConsumers' . ( $par !== null ? "/$par" : '' ) ),
87                ], $this->msg( 'mwoauth-consumers-central-wiki-go', $wiki->getDisplayName() )->text() ) );
88            }
89            return;
90        }
91
92        $this->requireNamedUser( 'mwoauth-available-only-to-registered' );
93
94        $user = $this->getUser();
95
96        if ( !$this->permissionManager->userHasRight( $user, 'mwoauthmanageconsumer' ) ) {
97            throw new PermissionsError( 'mwoauthmanageconsumer' );
98        }
99
100        if ( $this->getConfig()->get( 'MWOAuthReadOnly' ) ) {
101            throw new ErrorPageError( 'mwoauth-error', 'mwoauth-db-readonly' );
102        }
103
104        // Format is Special:OAuthManageConsumers[/<stage>|/<consumer key>]
105        // B/C format is Special:OAuthManageConsumers/<stage>/<consumer key>
106        $consumerKey = null;
107        $navigation = $par !== null ? explode( '/', $par ) : [];
108        if ( count( $navigation ) === 2 ) {
109            $this->stage = false;
110            $consumerKey = $navigation[1];
111        } elseif ( count( $navigation ) === 1 && $navigation[0] ) {
112            $this->stage = array_search( $navigation[0], Consumer::$stageNames, true );
113            if ( $this->stage !== false ) {
114                $this->stageKey = $navigation[0];
115            } else {
116                $consumerKey = $navigation[0];
117            }
118        }
119
120        if ( $consumerKey ) {
121            $this->handleConsumerForm( $consumerKey );
122        } elseif ( $this->stage !== false ) {
123            $this->showConsumerList();
124        } else {
125            $this->showMainHub();
126        }
127
128        $this->addQueueSubtitleLinks( $consumerKey );
129
130        $this->getOutput()->addModuleStyles( 'ext.MWOAuth.styles' );
131    }
132
133    /**
134     * Show other sub-queue links. Grey out the current one.
135     * When viewing a request, show them all and a link to current consumer view.
136     *
137     * @param string|null $consumerKey
138     * @return void
139     */
140    protected function addQueueSubtitleLinks( $consumerKey ) {
141        $linkRenderer = $this->getLinkRenderer();
142        $listLinks = [];
143        foreach ( self::$queueStages as $stage ) {
144            $stageKey = Consumer::$stageNames[$stage];
145            if ( $consumerKey || $this->stageKey !== $stageKey ) {
146                $listLinks[] = $linkRenderer->makeKnownLink(
147                    $this->getPageTitle( $stageKey ),
148                    // Messages: mwoauthmanageconsumers-showproposed,
149                    // mwoauthmanageconsumers-showrejected, mwoauthmanageconsumers-showexpired,
150                    $this->msg( 'mwoauthmanageconsumers-show' . $stageKey )->text()
151                );
152            } else {
153                $listLinks[] = $this->msg( 'mwoauthmanageconsumers-show' . $stageKey )->escaped();
154            }
155        }
156
157        if ( $consumerKey ) {
158            $consumerViewLink = "[" . $linkRenderer->makeKnownLink(
159                SpecialPage::getTitleFor( 'OAuthListConsumers', "view/$consumerKey" ),
160                $this->msg( 'mwoauthconsumer-consumer-view' )->text() ) . "]";
161        } else {
162            $consumerViewLink = '';
163        }
164
165        $linkHtml = $this->getLanguage()->pipeList( $listLinks );
166
167        $viewall = $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeKnownLink(
168            $this->getPageTitle(),
169            $this->msg( 'mwoauthmanageconsumers-main' )->text()
170        ) )->escaped();
171
172        $this->getOutput()->setSubtitle(
173            "<strong>" . $this->msg( 'mwoauthmanageconsumers-type' )->escaped() .
174            "</strong> [{$linkHtml}{$consumerViewLink} <strong>{$viewall}</strong>" );
175    }
176
177    /**
178     * Show the links to all the queues and how many requests are in each.
179     * Also show the list of enabled and disabled consumers and how many there are of each.
180     *
181     * @return void
182     */
183    protected function showMainHub() {
184        $keyStageMapQ = array_intersect( array_flip( Consumer::$stageNames ),
185            self::$queueStages );
186        $keyStageMapL = array_intersect( array_flip( Consumer::$stageNames ),
187            self::$listStages );
188
189        $linkRenderer = $this->getLinkRenderer();
190        $out = $this->getOutput();
191
192        $out->addWikiMsg( 'mwoauthmanageconsumers-maintext' );
193
194        $counts = Utils::getConsumerStateCounts( Utils::getOAuthDB( DB_REPLICA ) );
195
196        $out->wrapWikiMsg( "<p><strong>$1</strong></p>", 'mwoauthmanageconsumers-queues' );
197        $out->addHTML( '<ul>' );
198        foreach ( $keyStageMapQ as $stageKey => $stage ) {
199            $tag = ( $stage === Consumer::STAGE_EXPIRED ) ? 'i' : 'b';
200            $out->addHTML(
201                '<li>' .
202                "<$tag>" .
203                $linkRenderer->makeKnownLink(
204                    $this->getPageTitle( $stageKey ),
205                    // Messages: mwoauthmanageconsumers-q-proposed, mwoauthmanageconsumers-q-rejected,
206                    // mwoauthmanageconsumers-q-expired
207                    $this->msg( 'mwoauthmanageconsumers-q-' . $stageKey )->text()
208                ) .
209                "</$tag> [$counts[$stage]]" .
210                '</li>'
211            );
212        }
213        $out->addHTML( '</ul>' );
214
215        $out->wrapWikiMsg( "<p><strong>$1</strong></p>", 'mwoauthmanageconsumers-lists' );
216        $out->addHTML( '<ul>' );
217        foreach ( $keyStageMapL as $stageKey => $stage ) {
218            $out->addHTML(
219                '<li>' .
220                $linkRenderer->makeKnownLink(
221                    $this->getPageTitle( $stageKey ),
222                    // Messages: mwoauthmanageconsumers-l-approved, mwoauthmanageconsumers-l-disabled
223                    $this->msg( 'mwoauthmanageconsumers-l-' . $stageKey )->text()
224                ) .
225                " [$counts[$stage]]" .
226                '</li>'
227            );
228        }
229        $out->addHTML( '</ul>' );
230    }
231
232    /**
233     * Show the form to approve/reject/disable/re-enable consumers
234     *
235     * @param string $consumerKey
236     * @throws PermissionsError
237     */
238    protected function handleConsumerForm( $consumerKey ) {
239        $user = $this->getUser();
240        $dbr = Utils::getOAuthDB( DB_REPLICA );
241        $cmrAc = ConsumerAccessControl::wrap(
242            Consumer::newFromKey( $dbr, $consumerKey ), $this->getContext() );
243
244        if ( !$cmrAc ) {
245            $this->getOutput()->addWikiMsg( 'mwoauth-invalid-consumer-key' );
246            return;
247        } elseif ( $cmrAc->getDeleted()
248            && !$this->permissionManager->userHasRight( $user, 'mwoauthviewsuppressed' ) ) {
249            throw new PermissionsError( 'mwoauthviewsuppressed' );
250        }
251        $startingStage = $cmrAc->getStage();
252        $pending = !in_array( $startingStage, [
253            Consumer::STAGE_APPROVED, Consumer::STAGE_DISABLED ] );
254
255        if ( $pending ) {
256            $opts = [
257                $this->msg( 'mwoauthmanageconsumers-approve' )->escaped() => 'approve',
258                $this->msg( 'mwoauthmanageconsumers-reject' )->escaped()  => 'reject'
259            ];
260            if ( $this->permissionManager->userHasRight( $this->getUser(), 'mwoauthsuppress' ) ) {
261                $msg = $this->msg( 'mwoauthmanageconsumers-rsuppress' )->escaped();
262                $opts["<strong>$msg</strong>"] = 'rsuppress';
263            }
264        } else {
265            $opts = [
266                $this->msg( 'mwoauthmanageconsumers-disable' )->escaped() => 'disable',
267                $this->msg( 'mwoauthmanageconsumers-reenable' )->escaped()  => 'reenable'
268            ];
269            if ( $this->permissionManager->userHasRight( $this->getUser(), 'mwoauthsuppress' ) ) {
270                $msg = $this->msg( 'mwoauthmanageconsumers-dsuppress' )->escaped();
271                $opts["<strong>$msg</strong>"] = 'dsuppress';
272            }
273        }
274
275        $dbw = Utils::getOAuthDB( DB_PRIMARY );
276        $control = new ConsumerSubmitControl( $this->getContext(), [], $dbw );
277        $form = HTMLForm::factory( 'ooui',
278            $control->registerValidators( [
279                'info' => [
280                    'type' => 'info',
281                    'raw' => true,
282                    'default' => UIUtils::generateInfoTable(
283                        $this->getInfoTableOptions( $cmrAc ),
284                        $this->getContext()
285                    ),
286                ],
287                'action' => [
288                    'type' => 'radio',
289                    'label-message' => 'mwoauthmanageconsumers-action',
290                    'required' => true,
291                    'options' => $opts,
292                    // no validate on GET
293                    'default' => '',
294                ],
295                'reason' => [
296                    'type' => 'text',
297                    'label-message' => 'mwoauthmanageconsumers-reason',
298                    'required' => true,
299                ],
300                'consumerKey' => [
301                    'type' => 'hidden',
302                    'default' => $cmrAc->getConsumerKey(),
303                ],
304                'changeToken' => [
305                    'type' => 'hidden',
306                    'default' => $cmrAc->getDAO()->getChangeToken( $this->getContext() ),
307                ],
308            ] ),
309            $this->getContext()
310        );
311        $form->setSubmitCallback(
312            static function ( array $data, IContextSource $context ) use ( $control ) {
313                $data['suppress'] = 0;
314                if ( $data['action'] === 'dsuppress' ) {
315                    $data = [ 'action' => 'disable', 'suppress' => 1 ] + $data;
316                } elseif ( $data['action'] === 'rsuppress' ) {
317                    $data = [ 'action' => 'reject', 'suppress' => 1 ] + $data;
318                }
319                $control->setInputParameters( $data );
320                return $control->submit();
321            }
322        );
323
324        $form->setWrapperLegendMsg( 'mwoauthmanageconsumers-confirm-legend' );
325        $form->setSubmitTextMsg( 'mwoauthmanageconsumers-confirm-submit' );
326        $form->addPreHtml(
327            $this->msg( 'mwoauthmanageconsumers-confirm-text' )->parseAsBlock() );
328
329        $status = $form->show();
330        if ( $status instanceof Status && $status->isOK() ) {
331            /** @var Consumer $cmr */
332            $cmr = $status->value['result'];
333            '@phan-var Consumer $cmr';
334            $oldStageKey = Consumer::$stageNames[$startingStage];
335            $newStageKey = Consumer::$stageNames[$cmr->getStage()];
336            // Messages: mwoauthmanageconsumers-success-approved, mwoauthmanageconsumers-success-rejected,
337            // mwoauthmanageconsumers-success-disabled
338            $this->getOutput()->addWikiMsg( "mwoauthmanageconsumers-success-$newStageKey" );
339            $returnTo = Title::newFromText( 'Special:OAuthManageConsumers/' . $oldStageKey );
340            $this->getOutput()->addReturnTo( $returnTo, [],
341                // Messages: mwoauthmanageconsumers-linkproposed,
342                // mwoauthmanageconsumers-linkrejected, mwoauthmanageconsumers-linkexpired,
343                // mwoauthmanageconsumers-linkapproved, mwoauthmanageconsumers-linkdisabled
344                $this->msg( 'mwoauthmanageconsumers-link' . $oldStageKey )->text() );
345        } else {
346            $out = $this->getOutput();
347            // Show all of the status updates
348            $logPage = new LogPage( 'mwoauthconsumer' );
349            $out->addHTML( Html::element( 'h2', [], $logPage->getName()->text() ) );
350            LogEventsList::showLogExtract( $out, 'mwoauthconsumer', '', '', [
351                'conds' => [
352                    'ls_field' => 'OAuthConsumer',
353                    'ls_value' => $cmrAc->getConsumerKey(),
354                ],
355            ] );
356        }
357    }
358
359    /**
360     * @param ConsumerAccessControl $cmrAc
361     * @return array
362     */
363    protected function getInfoTableOptions( $cmrAc ) {
364        $owner = $cmrAc->getUserName();
365        $lang = $this->getLanguage();
366
367        $link = $this->getLinkRenderer()->makeKnownLink(
368            $title = SpecialPage::getTitleFor( 'OAuthListConsumers' ),
369            $this->msg( 'mwoauthmanageconsumers-search-publisher' )->text(),
370            [],
371            [ 'publisher' => $owner ]
372        );
373        $ownerLink = $cmrAc->escapeForHtml( $owner ) . ' ' .
374            $this->msg( 'parentheses' )->rawParams( $link )->escaped();
375        $ownerOnly = $cmrAc->getDAO()->getOwnerOnly();
376        $restrictions = $cmrAc->getRestrictions();
377
378        $options = [
379            // Messages: mwoauth-consumer-stage-proposed, mwoauth-consumer-stage-rejected,
380            // mwoauth-consumer-stage-expired, mwoauth-consumer-stage-approved,
381            // mwoauth-consumer-stage-disabled
382            'mwoauth-consumer-stage' => $cmrAc->getDeleted()
383                ? $this->msg( 'mwoauth-consumer-stage-suppressed' )
384                : $this->msg( 'mwoauth-consumer-stage-' .
385                    Consumer::$stageNames[$cmrAc->getStage()] ),
386            'mwoauth-consumer-key' => $cmrAc->getConsumerKey(),
387            'mwoauth-consumer-name' => new HtmlSnippet( $cmrAc->get( 'name', function ( $s ) {
388                $link = $this->getLinkRenderer()->makeKnownLink(
389                    SpecialPage::getTitleFor( 'OAuthListConsumers' ),
390                    $this->msg( 'mwoauthmanageconsumers-search-name' )->text(),
391                    [],
392                    [ 'name' => $s ]
393                );
394                return htmlspecialchars( $s ) . ' ' .
395                    $this->msg( 'parentheses' )->rawParams( $link )->escaped();
396            } ) ),
397            'mwoauth-consumer-version' => $cmrAc->getVersion(),
398            'mwoauth-oauth-version' => $cmrAc->getOAuthVersion() === Consumer::OAUTH_VERSION_2
399                ? $this->msg( 'mwoauth-oauth-version-2' )
400                : $this->msg( 'mwoauth-oauth-version-1' ),
401            'mwoauth-consumer-user' => new HtmlSnippet( $ownerLink ),
402            'mwoauth-consumer-description' => $cmrAc->getDescription(),
403            'mwoauth-consumer-owner-only-label' => $ownerOnly ?
404                $this->msg( 'mwoauth-consumer-owner-only', $owner ) : null,
405            'mwoauth-consumer-callbackurl' => $ownerOnly ?
406                null : $this->formatCallbackUrl( $cmrAc ),
407            'mwoauth-consumer-callbackisprefix' => $ownerOnly ?
408                null : ( $cmrAc->getCallbackIsPrefix() ?
409                    $this->msg( 'htmlform-yes' ) : $this->msg( 'htmlform-no' ) ),
410            'mwoauth-consumer-grantsneeded' => $cmrAc->get( 'grants',
411                function ( $grants ) use ( $lang ) {
412                    return $lang->semicolonList( $this->grantsLocalization->getGrantDescriptions( $grants, $lang ) );
413                } ),
414            'mwoauth-consumer-email' => $cmrAc->getEmail(),
415            'mwoauth-consumer-wiki' => $cmrAc->getWiki()
416        ];
417
418        // Add OAuth2 specific parameters
419        if ( $cmrAc->getOAuthVersion() === Consumer::OAUTH_VERSION_2 ) {
420            /** @var ClientEntity $consumer */
421            $consumer = $cmrAc->getDAO();
422            $options += [
423                'mwoauth-oauth2-is-confidential' => $consumer->isConfidential() ?
424                    $this->msg( 'htmlform-yes' ) : $this->msg( 'htmlform-no' ),
425                'mwoauth-oauth2-granttypes' => implode( ', ', array_map( function ( $grant ) {
426                    $map = [
427                        'authorization_code' => 'mwoauth-oauth2-granttype-auth-code',
428                        'refresh_token' => 'mwoauth-oauth2-granttype-refresh-token',
429                        'client_credentials' => 'mwoauth-oauth2-granttype-client-credentials'
430                    ];
431                    return isset( $map[$grant] ) ? $this->msg( $map[$grant] ) : '';
432                }, $consumer->getAllowedGrants() ) )
433            ];
434        }
435
436        // Add optional parameters
437        $options += [
438            'mwoauth-consumer-restrictions-json' => $restrictions instanceof MWRestrictions ?
439                $restrictions->toJson( true ) : $restrictions,
440            'mwoauth-consumer-rsakey' => $cmrAc->getRsaKey(),
441        ];
442
443        return $options;
444    }
445
446    /**
447     * Format a callback URL. Usually this doesn't do anything nontrivial, but it adds a warning
448     * to callback URLs with a special meaning.
449     * @param ConsumerAccessControl $cmrAc
450     * @return HtmlSnippet|string Formatted callback URL, as a plaintext or HTML string
451     */
452    protected function formatCallbackUrl( ConsumerAccessControl $cmrAc ) {
453        $url = $cmrAc->getCallbackUrl();
454        if ( $cmrAc->getDAO()->getCallbackIsPrefix() ) {
455            $urlParts = $this->urlUtils->parse( $cmrAc->getDAO()->getCallbackUrl() );
456            if ( ( $urlParts['port'] ?? null ) === 1 ) {
457                $warning = Html::element( 'span', [ 'class' => 'warning' ],
458                    $this->msg( 'mwoauth-consumer-callbackurl-warning' )->text() );
459                $url = new HtmlSnippet( $url . ' ' . $warning );
460            }
461        }
462        return $url;
463    }
464
465    /**
466     * Show a paged list of consumers with links to details
467     */
468    protected function showConsumerList() {
469        $pager = new ManageConsumersPager( $this, [], $this->stage );
470        if ( $pager->getNumRows() ) {
471            $this->getOutput()->addHTML( $pager->getNavigationBar() );
472            $this->getOutput()->addHTML( $pager->getBody() );
473            $this->getOutput()->addHTML( $pager->getNavigationBar() );
474        } else {
475            // Messages: mwoauthmanageconsumers-none-proposed, mwoauthmanageconsumers-none-rejected,
476            // mwoauthmanageconsumers-none-expired, mwoauthmanageconsumers-none-approved,
477            // mwoauthmanageconsumers-none-disabled
478            $this->getOutput()->addWikiMsg( "mwoauthmanageconsumers-none-{$this->stageKey}" );
479        }
480        # Every 30th view, prune old deleted items
481        if ( mt_rand( 0, 29 ) == 0 ) {
482            Utils::runAutoMaintenance( Utils::getOAuthDB( DB_PRIMARY ) );
483        }
484    }
485
486    /**
487     * @param IDatabase $db
488     * @param stdClass $row
489     * @return string
490     */
491    public function formatRow( IDatabase $db, $row ) {
492        $cmrAc = ConsumerAccessControl::wrap(
493            Consumer::newFromRow( $db, $row ), $this->getContext()
494        );
495
496        $cmrKey = $cmrAc->getConsumerKey();
497        $stageKey = Consumer::$stageNames[$cmrAc->getStage()];
498
499        $link = $this->getLinkRenderer()->makeKnownLink(
500            $this->getPageTitle( $cmrKey ),
501            $this->msg( 'mwoauthmanageconsumers-review' )->text()
502        );
503
504        $time = $this->getLanguage()->timeanddate(
505            wfTimestamp( TS_MW, $cmrAc->getRegistration() ), true );
506
507        $encStageKey = htmlspecialchars( $stageKey );
508        $r = "<li class='mw-mwoauthmanageconsumers-{$encStageKey}'>";
509        $r .= "<span class='mw-mwoauth-stage-icon'></span> ";
510
511        $r .= $time . " (<strong>{$link}</strong>)";
512
513        // Show last log entry (@TODO: title namespace?)
514        // @TODO: inject DB
515        $logHtml = '';
516        LogEventsList::showLogExtract( $logHtml, 'mwoauthconsumer', '', '', [
517            'action' => Consumer::$stageActionNames[$cmrAc->getStage()],
518            'conds'  => [
519                'ls_field' => 'OAuthConsumer',
520                'ls_value' => $cmrAc->getConsumerKey(),
521            ],
522            'lim'    => 1,
523            'flags'  => LogEventsList::NO_EXTRA_USER_LINKS,
524        ] );
525
526        $lang = $this->getLanguage();
527        $data = [
528            'mwoauthmanageconsumers-name' => $cmrAc->escapeForHtml( $cmrAc->getNameAndVersion() ),
529            'mwoauthmanageconsumers-user' => $cmrAc->escapeForHtml( $cmrAc->getUserName() ),
530            'mwoauth-oauth-version' => $cmrAc->escapeForHtml(
531                $cmrAc->getOAuthVersion() === Consumer::OAUTH_VERSION_2 ?
532                $this->msg( 'mwoauth-oauth-version-2' ) :
533                $this->msg( 'mwoauth-oauth-version-1' )
534            ),
535            'mwoauthmanageconsumers-description' => $cmrAc->escapeForHtml(
536                $cmrAc->get( 'description', static function ( $s ) use ( $lang ) {
537                    return $lang->truncateForVisual( $s, 10024 );
538                } )
539            ),
540            'mwoauthmanageconsumers-email' => $cmrAc->escapeForHtml( $cmrAc->getEmail() ),
541            'mwoauthmanageconsumers-consumerkey' => $cmrAc->escapeForHtml( $cmrAc->getConsumerKey() ),
542            'mwoauthmanageconsumers-lastchange' => $logHtml,
543        ];
544
545        $r .= "<table class='mw-mwoauthmanageconsumers-body mw-datatable'>";
546        foreach ( $data as $msg => $encValue ) {
547            $r .= '<tr>' .
548                '<th>' . $this->msg( $msg )->escaped() . '</th>' .
549                '<td width=\'90%\'>' . $encValue . '</td>' .
550                '</tr>';
551        }
552        $r .= '</table>';
553
554        $r .= '</li>';
555
556        return $r;
557    }
558
559    /** @inheritDoc */
560    protected function getGroupName() {
561        return 'users';
562    }
563
564}