Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 205
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMWOAuthManageMyGrants
0.00% covered (danger)
0.00%
0 / 205
0.00% covered (danger)
0.00%
0 / 8
992
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 / 21
0.00% covered (danger)
0.00%
0 / 1
72
 addSubtitleLinks
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 handleConsumerForm
0.00% covered (danger)
0.00%
0 / 109
0.00% covered (danger)
0.00%
0 / 1
156
 showConsumerList
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 formatRow
0.00% covered (danger)
0.00%
0 / 44
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
5use MediaWiki\Context\IContextSource;
6use MediaWiki\Exception\ErrorPageError;
7use MediaWiki\Exception\PermissionsError;
8use MediaWiki\Extension\OAuth\Backend\Consumer;
9use MediaWiki\Extension\OAuth\Backend\ConsumerAcceptance;
10use MediaWiki\Extension\OAuth\Backend\Utils;
11use MediaWiki\Extension\OAuth\Control\ConsumerAcceptanceAccessControl;
12use MediaWiki\Extension\OAuth\Control\ConsumerAcceptanceSubmitControl;
13use MediaWiki\Extension\OAuth\Control\ConsumerAccessControl;
14use MediaWiki\Extension\OAuth\Control\SubmitControl;
15use MediaWiki\Extension\OAuth\Frontend\Pagers\ManageMyGrantsPager;
16use MediaWiki\Extension\OAuth\Frontend\UIUtils;
17use MediaWiki\HTMLForm\HTMLForm;
18use MediaWiki\Json\FormatJson;
19use MediaWiki\Permissions\GrantsInfo;
20use MediaWiki\Permissions\GrantsLocalization;
21use MediaWiki\Permissions\PermissionManager;
22use MediaWiki\SpecialPage\SpecialPage;
23use MediaWiki\Status\Status;
24use stdClass;
25use Wikimedia\Rdbms\IDatabase;
26
27/**
28 * (c) Aaron Schulz 2013, GPL
29 *
30 * @license GPL-2.0-or-later
31 */
32
33/**
34 * Special page for listing consumers this user granted access to and
35 * for manage the specific grants given or revoking access for the consumer
36 */
37class SpecialMWOAuthManageMyGrants extends SpecialPage {
38    public function __construct(
39        private readonly GrantsInfo $grantsInfo,
40        private readonly GrantsLocalization $grantsLocalization,
41        private readonly PermissionManager $permissionManager,
42    ) {
43        parent::__construct( 'OAuthManageMyGrants', 'mwoauthmanagemygrants' );
44    }
45
46    /** @inheritDoc */
47    public function doesWrites() {
48        return true;
49    }
50
51    /** @inheritDoc */
52    public function execute( $par ) {
53        $this->setHeaders();
54        $this->getOutput()->disallowUserJs();
55        $this->addHelpLink( 'Help:OAuth' );
56        $this->requireNamedUser( 'mwoauth-available-only-to-registered' );
57
58        $user = $this->getUser();
59        if ( !$this->permissionManager->userHasRight( $user, 'mwoauthmanagemygrants' ) ) {
60            throw new PermissionsError( 'mwoauthmanagemygrants' );
61        }
62
63        // Format is Special:OAuthManageMyGrants[/list|/manage/<accesstoken>]
64        $navigation = $par !== null ? explode( '/', $par ) : [];
65        $typeKey = $navigation[0] ?? null;
66        $acceptanceId = $navigation[1] ?? null;
67
68        if ( $this->getConfig()->get( 'MWOAuthReadOnly' )
69                && in_array( $typeKey, [ 'update', 'revoke' ] )
70        ) {
71            throw new ErrorPageError( 'mwoauth-error', 'mwoauth-db-readonly' );
72        }
73
74        switch ( $typeKey ) {
75            case 'update':
76            case 'revoke':
77                $this->handleConsumerForm( $acceptanceId ?? 0, $typeKey );
78                break;
79            default:
80                $this->showConsumerList();
81                break;
82        }
83
84        $this->addSubtitleLinks( $acceptanceId );
85
86        $this->getOutput()->addModuleStyles( 'ext.MWOAuth.styles' );
87    }
88
89    /**
90     * Show navigation links
91     *
92     * @param string|null $acceptanceId
93     * @return void
94     */
95    protected function addSubtitleLinks( $acceptanceId ) {
96        $listLinks = [];
97
98        if ( $acceptanceId ) {
99            $dbr = Utils::getOAuthDB( DB_REPLICA );
100            $cmraAc = ConsumerAcceptance::newFromId( $dbr, (int)$acceptanceId );
101            $listLinks[] = $this->getLinkRenderer()->makeKnownLink(
102                $this->getPageTitle(),
103                $this->msg( 'mwoauthmanagemygrants-showlist' )->text()
104            );
105
106            if ( $cmraAc ) {
107                $cmrAc = Consumer::newFromId( $dbr, $cmraAc->getConsumerId() );
108                $consumerKey = $cmrAc->getConsumerKey();
109                $listLinks[] = $this->getLinkRenderer()->makeKnownLink(
110                    SpecialPage::getTitleFor( 'OAuthListConsumers', "view/$consumerKey" ),
111                    $this->msg( 'mwoauthconsumer-application-view' )->text()
112                );
113            }
114        } else {
115            $listLinks[] = $this->msg( 'mwoauthmanagemygrants-showlist' )->escaped();
116        }
117
118        $linkHtml = $this->getLanguage()->pipeList( $listLinks );
119
120        $this->getOutput()->setSubtitle(
121            "<strong>" . $this->msg( 'mwoauthmanagemygrants-navigation' )->escaped() .
122            "</strong> [{$linkHtml}]" );
123    }
124
125    /**
126     * Show the form to approve/reject/disable/re-enable consumers
127     *
128     * @param int $acceptanceId
129     * @param string $type One of (update,revoke)
130     * @throws PermissionsError
131     */
132    protected function handleConsumerForm( $acceptanceId, $type ) {
133        $user = $this->getUser();
134        $dbr = Utils::getOAuthDB( DB_REPLICA );
135
136        $centralUserId = Utils::getCentralIdFromLocalUser( $user );
137        if ( !$centralUserId ) {
138            $this->getOutput()->addWikiMsg( 'badaccess-group0' );
139            return;
140        }
141
142        $cmraAc = ConsumerAcceptanceAccessControl::wrap(
143            ConsumerAcceptance::newFromId( $dbr, $acceptanceId ), $this->getContext() );
144        if ( !$cmraAc || $cmraAc->getUserId() !== $centralUserId ) {
145            $this->getOutput()->addHTML( $this->msg( 'mwoauth-invalid-access-token' )->escaped() );
146            return;
147        }
148
149        $cmrAc = ConsumerAccessControl::wrap(
150            Consumer::newFromId( $dbr, $cmraAc->getConsumerId() ), $this->getContext() );
151        if ( $cmrAc->getDeleted()
152            && !$this->permissionManager->userHasRight( $user, 'mwoauthviewsuppressed' ) ) {
153            throw new PermissionsError( 'mwoauthviewsuppressed' );
154        }
155
156        $action = '';
157        if ( $this->getRequest()->getCheck( 'renounce' ) ) {
158            $action = 'renounce';
159        } elseif ( $this->getRequest()->getCheck( 'update' ) ) {
160            $action = 'update';
161        }
162
163        $data = [ 'action' => $action ];
164        $control = new ConsumerAcceptanceSubmitControl(
165            $this->getContext(), $data, $dbr, $cmraAc->getDAO()->getOAuthVersion()
166        );
167        $form = HTMLForm::factory( 'ooui',
168            $control->registerValidators( [
169                'info' => [
170                    'type' => 'info',
171                    'raw' => true,
172                    'default' => UIUtils::generateInfoTable( [
173                        'mwoauth-consumer-name' => $cmrAc->getNameAndVersion(),
174                        'mwoauth-consumer-user' => $cmrAc->getUserName(),
175                        'mwoauth-consumer-description' => $cmrAc->getDescription(),
176                        'mwoauthmanagemygrants-wikiallowed' => $cmraAc->getWikiName(),
177                    ], $this->getContext() ),
178                ],
179                'grants'  => [
180                    'type' => 'checkmatrix',
181                    'label-message' => 'mwoauthmanagemygrants-applicablegrantsallowed',
182                    'columns' => [
183                        $this->msg( 'mwoauthmanagemygrants-grantaccept' )->escaped() => 'grant'
184                    ],
185                    'rows' => array_combine(
186                        array_map( [ $this->grantsLocalization, 'getGrantsLink' ], $cmrAc->getGrants() ),
187                        $cmrAc->getGrants()
188                    ),
189                    'default' => array_map(
190                        static function ( $g ) {
191                            return "grant-$g";
192                        },
193                        $cmraAc->getGrants()
194                    ),
195                    'tooltips-html' => [
196                        $this->grantsLocalization->getGrantsLink( 'basic' ) =>
197                            $this->msg( 'mwoauthmanagemygrants-basic-tooltip' )->parse(),
198                        $this->grantsLocalization->getGrantsLink( 'mwoauth-authonly' ) =>
199                            $this->msg( 'mwoauthmanagemygrants-authonly-tooltip' )->parse(),
200                        $this->grantsLocalization->getGrantsLink( 'mwoauth-authonlyprivate' ) =>
201                            $this->msg( 'mwoauthmanagemygrants-authonly-tooltip' )->parse(),
202                    ],
203                    'force-options-on' => array_map(
204                        static function ( $g ) {
205                            return "grant-$g";
206                        },
207                        ( $type === 'revoke' )
208                            ? array_merge( $this->grantsInfo->getValidGrants(), SubmitControl::getIrrevocableGrants() )
209                            : SubmitControl::getIrrevocableGrants()
210                    ),
211                    'validation-callback' => null
212                ],
213                'acceptanceId' => [
214                    'type' => 'hidden',
215                    'default' => $cmraAc->getId(),
216                ]
217            ] ),
218            $this->getContext()
219        );
220        $form->setSubmitCallback(
221            static function ( array $data, IContextSource $context ) use ( $action, $cmraAc ) {
222                $data['action'] = $action;
223                // adapt form to controller
224                $data['grants'] = FormatJson::encode(
225                    preg_replace( '/^grant-/', '', $data['grants'] )
226                );
227
228                $dbw = Utils::getOAuthDB( DB_PRIMARY );
229                $control = new ConsumerAcceptanceSubmitControl(
230                    $context, $data, $dbw, $cmraAc->getDAO()->getOAuthVersion()
231                );
232                return $control->submit();
233            }
234        );
235
236        $form->setWrapperLegendMsg( 'mwoauthmanagemygrants-confirm-legend' );
237        $form->suppressDefaultSubmit();
238        if ( $type === 'revoke' ) {
239            $form->addButton( [
240                'name' => 'renounce',
241                'value' => $this->msg( 'mwoauthmanagemygrants-renounce' )->text(),
242                'flags' => [ 'primary', 'destructive' ],
243            ] );
244        } else {
245            $form->addButton( [
246                'name' => 'update',
247                'value' => $this->msg( 'mwoauthmanagemygrants-update' )->text(),
248                'flags' => [ 'primary', 'progressive' ],
249            ] );
250        }
251        $form->addPreHtml(
252            $this->msg( "mwoauthmanagemygrants-$type-text" )->parseAsBlock() );
253
254        $status = $form->show();
255        if ( $status instanceof Status && $status->isOK() ) {
256            // Messages: mwoauthmanagemygrants-success-update, mwoauthmanagemygrants-success-renounce
257            $this->getOutput()->addWikiMsg( "mwoauthmanagemygrants-success-$action" );
258        }
259    }
260
261    /**
262     * Show a paged list of consumers with links to details
263     *
264     * @return void
265     */
266    protected function showConsumerList() {
267        $this->getOutput()->addWikiMsg( 'mwoauthmanagemygrants-text' );
268
269        $centralUserId = Utils::getCentralIdFromLocalUser( $this->getUser() );
270        $pager = new ManageMyGrantsPager( $this, [], $centralUserId );
271        if ( $pager->getNumRows() ) {
272            $this->getOutput()->addHTML( $pager->getNavigationBar() );
273            $this->getOutput()->addHTML( $pager->getBody() );
274            $this->getOutput()->addHTML( $pager->getNavigationBar() );
275        } else {
276            $this->getOutput()->addWikiMsg( "mwoauthmanagemygrants-none" );
277        }
278    }
279
280    /**
281     * @param IDatabase $db
282     * @param stdClass $row
283     * @return string
284     */
285    public function formatRow( IDatabase $db, $row ) {
286        $cmrAc = ConsumerAccessControl::wrap(
287            Consumer::newFromRow( $db, $row ), $this->getContext() );
288        $cmraAc = ConsumerAcceptanceAccessControl::wrap(
289            ConsumerAcceptance::newFromRow( $db, $row ), $this->getContext() );
290
291        $linkRenderer = $this->getLinkRenderer();
292
293        $links = [];
294        if ( array_diff( $cmrAc->getGrants(), SubmitControl::getIrrevocableGrants() ) ) {
295            $links[] = $linkRenderer->makeKnownLink(
296                $this->getPageTitle( 'update/' . $cmraAc->getId() ),
297                $this->msg( 'mwoauthmanagemygrants-review' )->text()
298            );
299        }
300        $links[] = $linkRenderer->makeKnownLink(
301            $this->getPageTitle( 'revoke/' . $cmraAc->getId() ),
302            $this->msg( 'mwoauthmanagemygrants-revoke' )->text()
303        );
304        $reviewLinks = $this->getLanguage()->pipeList( $links );
305
306        $encName = $cmrAc->escapeForHtml( $cmrAc->getNameAndVersion() );
307
308        $r = '<li class="mw-mwoauthmanagemygrants-list-item">';
309        $r .= "<strong dir='ltr'>{$encName}</strong> (<strong>$reviewLinks</strong>)";
310        $data = [
311            'mwoauthmanagemygrants-user' => $cmrAc->getUserName(),
312            'mwoauthmanagemygrants-wikiallowed' => $cmraAc->getWikiName(),
313        ];
314
315        foreach ( $data as $msg => $val ) {
316            $r .= '<p>' . $this->msg( $msg )->escaped() . ' ' . $cmrAc->escapeForHtml( $val ) . '</p>';
317        }
318
319        $editsLink = $linkRenderer->makeKnownLink(
320            SpecialPage::getTitleFor( 'Contributions', $this->getUser()->getName() ),
321            $this->msg( 'mwoauthmanagemygrants-editslink', $this->getUser()->getName() )->text(),
322            [],
323            [ 'tagfilter' => Utils::getTagName( $cmrAc->getId() ) ]
324        );
325        $r .= '<p>' . $editsLink . '</p>';
326        $actionsLink = $linkRenderer->makeKnownLink(
327            SpecialPage::getTitleFor( 'Log' ),
328            $this->msg( 'mwoauthmanagemygrants-actionslink', $this->getUser()->getName() )->text(),
329            [],
330            [
331                'user' => $this->getUser()->getName(),
332                'tagfilter' => Utils::getTagName( $cmrAc->getId() ),
333            ]
334        );
335        $r .= '<p>' . $actionsLink . '</p>';
336
337        $r .= '</li>';
338
339        return $r;
340    }
341
342    /** @inheritDoc */
343    protected function getGroupName() {
344        return 'login';
345    }
346}