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