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