Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.01% covered (warning)
59.01%
203 / 344
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ConsumerSubmitControl
59.01% covered (warning)
59.01%
203 / 344
57.14% covered (warning)
57.14%
4 / 7
634.46
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRequiredFields
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
1 / 1
1
 checkBasePermissions
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
9.36
 processAction
46.18% covered (danger)
46.18%
115 / 249
0.00% covered (danger)
0.00%
0 / 1
927.45
 getLogTitle
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 makeLogEntry
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
1
 notify
81.25% covered (warning)
81.25%
13 / 16
0.00% covered (danger)
0.00%
0 / 1
4.11
1<?php
2
3namespace MediaWiki\Extension\OAuth\Control;
4
5use Exception;
6use LogicException;
7use MediaWiki\Context\IContextSource;
8use MediaWiki\Extension\Notifications\Model\Event;
9use MediaWiki\Extension\OAuth\Backend\Consumer;
10use MediaWiki\Extension\OAuth\Backend\ConsumerAcceptance;
11use MediaWiki\Extension\OAuth\Backend\MWOAuthDataStore;
12use MediaWiki\Extension\OAuth\Backend\Utils;
13use MediaWiki\Extension\OAuth\Entity\ClientEntity;
14use MediaWiki\Extension\OAuth\OAuthServices;
15use MediaWiki\Json\FormatJson;
16use MediaWiki\Logger\LoggerFactory;
17use MediaWiki\Logging\ManualLogEntry;
18use MediaWiki\MediaWikiServices;
19use MediaWiki\Registration\ExtensionRegistry;
20use MediaWiki\SpecialPage\SpecialPage;
21use MediaWiki\Status\Status;
22use MediaWiki\Title\Title;
23use MediaWiki\User\User;
24use MediaWiki\Utils\MWCryptRand;
25use MediaWiki\WikiMap\WikiMap;
26use Wikimedia\Rdbms\IDatabase;
27use Wikimedia\Rdbms\SelectQueryBuilder;
28
29/**
30 * (c) Aaron Schulz 2013, GPL
31 *
32 * @license GPL-2.0-or-later
33 */
34
35/**
36 * This handles the core logic of approving/disabling consumers
37 * from using particular user accounts
38 *
39 * This control can only be used on the management wiki
40 *
41 * @todo improve error messages
42 */
43class ConsumerSubmitControl extends SubmitControl {
44    /**
45     * Names of the actions that can be performed on a consumer. These are the same as the
46     * options in getRequiredFields().
47     * @var string[]
48     */
49    public static $actions = [ 'propose', 'update', 'approve', 'reject', 'disable', 'reenable' ];
50
51    /** @var IDatabase */
52    protected $dbw;
53
54    /**
55     * @param IContextSource $context
56     * @param array $params
57     * @param IDatabase $dbw Result of Utils::getOAuthDB( DB_PRIMARY )
58     */
59    public function __construct( IContextSource $context, array $params, IDatabase $dbw ) {
60        parent::__construct( $context, $params );
61        $this->dbw = $dbw;
62    }
63
64    /** @inheritDoc */
65    protected function getRequiredFields() {
66        $expectedConsumerFields = [
67            // list of Consumer properties which appear as fields on the proposal form
68            Consumer::FIELD_NAME,
69            Consumer::FIELD_VERSION,
70            Consumer::FIELD_OAUTH_VERSION,
71            Consumer::FIELD_CALLBACK_URL,
72            Consumer::FIELD_DESCRIPTION,
73            Consumer::FIELD_EMAIL,
74            Consumer::FIELD_WIKI,
75            Consumer::FIELD_OAUTH2_GRANT_TYPES,
76            Consumer::FIELD_GRANTS,
77            Consumer::FIELD_RESTRICTIONS,
78            Consumer::FIELD_RSA_KEY,
79            // FIXME DEVELOPER_AGREEMENT is omitted because the form uses a different field name
80        ];
81        $validator = new ConsumerValidator();
82        $validatorCallbacks = $validator->getValidatorCallbacks();
83        $validateRsaKey = $validatorCallbacks[Consumer::FIELD_RSA_KEY];
84        $validateRestrictions = $validatorCallbacks[Consumer::FIELD_RESTRICTIONS];
85        $validateDeveloperAgreement = $validatorCallbacks[Consumer::FIELD_DEVELOPER_AGREEMENT];
86        $validatorCallbacks = array_intersect_key( $validatorCallbacks,
87            array_fill_keys( $expectedConsumerFields, true ) );
88
89        $suppress = [ 'suppress' => '/^[01]$/' ];
90        $base = [
91            'consumerKey'  => '/^[0-9a-f]{32}$/',
92            'reason'       => '/^.{0,255}$/',
93            'changeToken'  => '/^[0-9a-f]{40}$/',
94        ];
95
96        return [
97            // Proposer (application owner) actions:
98            'propose' => $validatorCallbacks + [
99                'granttype' => '/^(authonly|authonlyprivate|normal)$/',
100                'agreement' => $validateDeveloperAgreement,
101            ],
102            'update' => array_merge( $base, [
103                'restrictions' => $validateRestrictions,
104                'rsaKey' => $validateRsaKey,
105                'resetSecret' => static function ( $s ) {
106                    return is_bool( $s );
107                },
108            ] ),
109            // Approver (OAuth admin) actions:
110            'approve'     => $base,
111            'reject'      => array_merge( $base, $suppress ),
112            'disable'     => array_merge( $base, $suppress ),
113            'reenable'    => $base,
114        ];
115    }
116
117    /** @inheritDoc */
118    protected function checkBasePermissions() {
119        global $wgBlockDisablesLogin;
120        $user = $this->getUser();
121        $readOnlyMode = MediaWikiServices::getInstance()->getReadOnlyMode();
122        if ( !$user->getId() ) {
123            return $this->failure( 'not_logged_in', 'badaccess-group0' );
124        } elseif ( $user->isLocked() || ( $wgBlockDisablesLogin && $user->getBlock() ) ) {
125            return $this->failure( 'user_blocked', 'badaccess-group0' );
126        } elseif ( $readOnlyMode->isReadOnly() ) {
127            return $this->failure( 'readonly', 'readonlytext', $readOnlyMode->getReason() );
128        } elseif ( !Utils::isCentralWiki() ) {
129            // This logs consumer changes to the local logging table on the central wiki
130            throw new LogicException( "This can only be used from the OAuth management wiki." );
131        }
132        return $this->success();
133    }
134
135    /** @inheritDoc */
136    protected function processAction( $action ): Status {
137        $context = $this->getContext();
138        // proposer or admin
139        $user = $this->getUser();
140        $dbw = $this->dbw;
141
142        $centralUserId = Utils::getCentralIdFromLocalUser( $user );
143        if ( !$centralUserId ) {
144            return $this->failure( 'permission_denied', 'badaccess-group0' );
145        }
146
147        $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
148
149        switch ( $action ) {
150            case 'propose':
151                if ( !$permissionManager->userHasRight( $user, 'mwoauthproposeconsumer' ) ) {
152                    return $this->failure( 'permission_denied', 'badaccess-group0' );
153                } elseif ( !$user->isEmailConfirmed() ) {
154                    return $this->failure( 'email_not_confirmed', 'mwoauth-consumer-email-unconfirmed' );
155                } elseif ( $user->getEmail() !== $this->vals['email'] ) {
156                    // @TODO: allow any email and don't set emailAuthenticated below
157                    return $this->failure( 'email_mismatched', 'mwoauth-consumer-email-mismatched' );
158                }
159
160                if ( Consumer::newFromNameVersionUser(
161                    $dbw, $this->vals['name'], $this->vals['version'], $centralUserId
162                ) ) {
163                    return $this->failure( 'consumer_exists', 'mwoauth-consumer-alreadyexists' );
164                }
165
166                $wikiNames = Utils::getAllWikiNames();
167                $dbKey = array_search( $this->vals['wiki'], $wikiNames );
168                if ( $dbKey !== false ) {
169                    $this->vals['wiki'] = $dbKey;
170                }
171
172                $curVer = $dbw->newSelectQueryBuilder()
173                    ->select( 'oarc_version' )
174                    ->from( 'oauth_registered_consumer' )
175                    ->where( [ 'oarc_name' => $this->vals['name'], 'oarc_user_id' => $centralUserId ] )
176                    ->orderBy( 'oarc_registration', SelectQueryBuilder::SORT_DESC )
177                    ->forUpdate()
178                    ->caller( __METHOD__ )
179                    ->fetchField();
180                if ( $curVer !== false && version_compare( $curVer, $this->vals['version'], '>=' ) ) {
181                    return $this->failure( 'consumer_exists',
182                        'mwoauth-consumer-alreadyexistsversion', $curVer );
183                }
184
185                // Handle owner-only mode
186                if ( $this->vals['ownerOnly'] ) {
187                    $this->vals['callbackUrl'] = SpecialPage::getTitleFor( 'OAuth', 'verified' )
188                        ->getLocalURL();
189                    $this->vals['callbackIsPrefix'] = '';
190                    $stage = Consumer::STAGE_APPROVED;
191                } else {
192                    $stage = Consumer::STAGE_PROPOSED;
193                }
194
195                // Handle grant types
196                $grants = [];
197                switch ( $this->vals['granttype'] ) {
198                    case 'authonly':
199                        $grants = [ 'mwoauth-authonly' ];
200                        break;
201                    case 'authonlyprivate':
202                        $grants = [ 'mwoauth-authonlyprivate' ];
203                        break;
204                    case 'normal':
205                        $grants = array_unique( array_merge(
206                            // implied grants
207                            MediaWikiServices::getInstance()
208                                ->getGrantsInfo()
209                                ->getHiddenGrants(),
210                            FormatJson::decode( $this->vals['grants'], true )
211                        ) );
212                        break;
213                }
214
215                $now = wfTimestampNow();
216                $cmr = Consumer::newFromArray(
217                    [
218                        'id'                 => null,
219                        'consumerKey'        => MWCryptRand::generateHex( 32 ),
220                        'userId'             => $centralUserId,
221                        'email'              => $user->getEmail(),
222                        'emailAuthenticated' => $now,
223                        'developerAgreement' => 1,
224                        'secretKey'          => MWCryptRand::generateHex( 32 ),
225                        'registration'       => $now,
226                        'stage'              => $stage,
227                        'stageTimestamp'     => $now,
228                        'grants'             => $grants,
229                        'restrictions'       => $this->vals['restrictions'],
230                        'deleted'            => 0
231                    ] + $this->vals
232                );
233
234                $logAction = 'propose';
235                $oauthServices = OAuthServices::wrap( MediaWikiServices::getInstance() );
236                $workflow = $oauthServices->getWorkflow();
237                $autoApproved = $workflow->consumerCanBeAutoApproved( $cmr );
238                if ( $cmr->getOwnerOnly() ) {
239                    // FIXME the stage is set a few dozen lines earlier - should simplify this
240                    $logAction = 'create-owner-only';
241                } elseif ( $autoApproved ) {
242                    $cmr->setField( 'stage', Consumer::STAGE_APPROVED );
243                    $logAction = 'propose-autoapproved';
244                }
245
246                $cmr->save( $dbw );
247                $this->makeLogEntry( Utils::getCentralWikiDB(), $cmr, $logAction, $user, $this->vals['description'] );
248                if ( !$cmr->getOwnerOnly() && !$autoApproved ) {
249                    // Notify admins if the consumer needs to be approved.
250                    if ( $cmr->getStage() === Consumer::STAGE_PROPOSED ) {
251                        $this->notify( $cmr, $user, $action, '' );
252                    }
253                }
254
255                // If it's owner-only, automatically accept it for the user too.
256                $accessToken = null;
257                if ( $cmr->getOwnerOnly() ) {
258                    $accessToken = MWOAuthDataStore::newToken();
259                    $cmra = ConsumerAcceptance::newFromArray( [
260                        'id'           => null,
261                        'wiki'         => $cmr->getWiki(),
262                        'userId'       => $centralUserId,
263                        'consumerId'   => $cmr->getId(),
264                        'accessToken'  => $accessToken->key,
265                        'accessSecret' => $accessToken->secret,
266                        'grants'       => $cmr->getGrants(),
267                        'accepted'     => $now,
268                        'oauth_version' => $cmr->getOAuthVersion()
269                    ] );
270                    $cmra->save( $dbw );
271                    if ( $cmr instanceof ClientEntity ) {
272                        // OAuth2 client
273                        try {
274                            $accessToken = $cmr->getOwnerOnlyAccessToken( $cmra );
275                        } catch ( Exception $ex ) {
276                            return $this->failure(
277                                'unable_to_retrieve_access_token',
278                                'mwoauth-oauth2-unable-to-retrieve-access-token',
279                                $ex->getMessage()
280                            );
281                        }
282                    }
283                }
284
285                return $this->success( [ 'consumer' => $cmr, 'accessToken' => $accessToken ] );
286            case 'update':
287                if ( !$permissionManager->userHasRight( $user, 'mwoauthupdateownconsumer' ) ) {
288                    return $this->failure( 'permission_denied', 'badaccess-group0' );
289                }
290
291                $cmr = Consumer::newFromKey( $dbw, $this->vals['consumerKey'] );
292                if ( !$cmr ) {
293                    return $this->failure( 'invalid_consumer_key', 'mwoauth-invalid-consumer-key' );
294                } elseif ( $cmr->getUserId() !== $centralUserId ) {
295                    return $this->failure( 'permission_denied', 'badaccess-group0' );
296                } elseif (
297                    $cmr->getStage() !== Consumer::STAGE_APPROVED
298                    && $cmr->getStage() !== Consumer::STAGE_PROPOSED
299                ) {
300                    return $this->failure( 'permission_denied', 'badaccess-group0' );
301                } elseif ( $cmr->getDeleted()
302                    && !$permissionManager->userHasRight( $user, 'mwoauthsuppress' ) ) {
303                    return $this->failure( 'permission_denied', 'badaccess-group0' );
304                } elseif ( !$cmr->checkChangeToken( $context, $this->vals['changeToken'] ) ) {
305                    return $this->failure( 'change_conflict', 'mwoauth-consumer-conflict' );
306                }
307
308                $cmr->setFields( [
309                    'rsaKey'       => $this->vals['rsaKey'],
310                    'restrictions' => $this->vals['restrictions'],
311                    'secretKey'    => $this->vals['resetSecret']
312                        ? MWCryptRand::generateHex( 32 )
313                        : $cmr->getSecretKey(),
314                ] );
315
316                // Log if something actually changed
317                if ( $cmr->save( $dbw ) ) {
318                    $this->makeLogEntry( Utils::getCentralWikiDB(), $cmr, $action, $user, $this->vals['reason'] );
319                    $this->notify( $cmr, $user, $action, $this->vals['reason'] );
320                }
321
322                $accessToken = null;
323                if ( $cmr->getOwnerOnly() && $this->vals['resetSecret'] ) {
324                    $cmra = $cmr->getCurrentAuthorization( $user, WikiMap::getCurrentWikiId() );
325                    $accessToken = MWOAuthDataStore::newToken();
326                    $fields = [
327                        'wiki'         => $cmr->getWiki(),
328                        'userId'       => $centralUserId,
329                        'consumerId'   => $cmr->getId(),
330                        'accessSecret' => $accessToken->secret,
331                        'grants'       => $cmr->getGrants(),
332                    ];
333
334                    if ( $cmra ) {
335                        $accessToken->key = $cmra->getAccessToken();
336                        $cmra->setFields( $fields );
337                    } else {
338                        $cmra = ConsumerAcceptance::newFromArray( $fields + [
339                            'id'           => null,
340                            'accessToken'  => $accessToken->key,
341                            'accepted'     => wfTimestampNow(),
342                        ] );
343                    }
344                    $cmra->save( $dbw );
345                    if ( $cmr instanceof ClientEntity ) {
346                        $accessToken = $cmr->getOwnerOnlyAccessToken( $cmra, true );
347                    }
348                }
349
350                return $this->success( [ 'consumer' => $cmr, 'accessToken' => $accessToken ] );
351            case 'approve':
352                if ( !$permissionManager->userHasRight( $user, 'mwoauthmanageconsumer' ) ) {
353                    return $this->failure( 'permission_denied', 'badaccess-group0' );
354                }
355
356                $cmr = Consumer::newFromKey( $dbw, $this->vals['consumerKey'] );
357                if ( !$cmr ) {
358                    return $this->failure( 'invalid_consumer_key', 'mwoauth-invalid-consumer-key' );
359                } elseif ( !in_array( $cmr->getStage(), [
360                    Consumer::STAGE_PROPOSED,
361                    Consumer::STAGE_EXPIRED,
362                    Consumer::STAGE_REJECTED,
363                ] ) ) {
364                    return $this->failure( 'not_proposed', 'mwoauth-consumer-not-proposed' );
365                } elseif ( $cmr->getDeleted() && !$permissionManager->userHasRight( $user, 'mwoauthsuppress' ) ) {
366                    return $this->failure( 'permission_denied', 'badaccess-group0' );
367                } elseif ( !$cmr->checkChangeToken( $context, $this->vals['changeToken'] ) ) {
368                    return $this->failure( 'change_conflict', 'mwoauth-consumer-conflict' );
369                }
370
371                $cmr->setFields( [
372                    'stage'          => Consumer::STAGE_APPROVED,
373                    'stageTimestamp' => wfTimestampNow(),
374                    'deleted'        => 0 ] );
375
376                // Log if something actually changed
377                if ( $cmr->save( $dbw ) ) {
378                    $this->makeLogEntry( Utils::getCentralWikiDB(), $cmr, $action, $user, $this->vals['reason'] );
379                    $this->notify( $cmr, $user, $action, $this->vals['reason'] );
380                }
381
382                return $this->success( $cmr );
383            case 'reject':
384                if ( !$permissionManager->userHasRight( $user, 'mwoauthmanageconsumer' ) ) {
385                    return $this->failure( 'permission_denied', 'badaccess-group0' );
386                }
387
388                $cmr = Consumer::newFromKey( $dbw, $this->vals['consumerKey'] );
389                if ( !$cmr ) {
390                    return $this->failure( 'invalid_consumer_key', 'mwoauth-invalid-consumer-key' );
391                } elseif ( $cmr->getStage() !== Consumer::STAGE_PROPOSED ) {
392                    return $this->failure( 'not_proposed', 'mwoauth-consumer-not-proposed' );
393                } elseif ( $cmr->getDeleted() && !$permissionManager->userHasRight( $user, 'mwoauthsuppress' ) ) {
394                    return $this->failure( 'permission_denied', 'badaccess-group0' );
395                } elseif ( $this->vals['suppress'] && !$permissionManager->userHasRight( $user, 'mwoauthsuppress' ) ) {
396                    return $this->failure( 'permission_denied', 'badaccess-group0' );
397                } elseif ( !$cmr->checkChangeToken( $context, $this->vals['changeToken'] ) ) {
398                    return $this->failure( 'change_conflict', 'mwoauth-consumer-conflict' );
399                }
400
401                $cmr->setFields( [
402                    'stage'          => Consumer::STAGE_REJECTED,
403                    'stageTimestamp' => wfTimestampNow(),
404                    'deleted'        => $this->vals['suppress'] ] );
405
406                // Log if something actually changed
407                if ( $cmr->save( $dbw ) ) {
408                    $this->makeLogEntry( Utils::getCentralWikiDB(), $cmr, $action, $user, $this->vals['reason'] );
409                    $this->notify( $cmr, $user, $action, $this->vals['reason'] );
410                }
411
412                return $this->success( $cmr );
413            case 'disable':
414                if ( !$permissionManager->userHasRight( $user, 'mwoauthmanageconsumer' ) ) {
415                    return $this->failure( 'permission_denied', 'badaccess-group0' );
416                } elseif ( $this->vals['suppress'] && !$permissionManager->userHasRight( $user, 'mwoauthsuppress' ) ) {
417                    return $this->failure( 'permission_denied', 'badaccess-group0' );
418                }
419
420                $cmr = Consumer::newFromKey( $dbw, $this->vals['consumerKey'] );
421                if ( !$cmr ) {
422                    return $this->failure( 'invalid_consumer_key', 'mwoauth-invalid-consumer-key' );
423                } elseif ( $cmr->getStage() !== Consumer::STAGE_APPROVED
424                && $cmr->getDeleted() == $this->vals['suppress']
425                ) {
426                    return $this->failure( 'not_approved', 'mwoauth-consumer-not-approved' );
427                } elseif ( $cmr->getDeleted() && !$permissionManager->userHasRight( $user, 'mwoauthsuppress' ) ) {
428                    return $this->failure( 'permission_denied', 'badaccess-group0' );
429                } elseif ( !$cmr->checkChangeToken( $context, $this->vals['changeToken'] ) ) {
430                    return $this->failure( 'change_conflict', 'mwoauth-consumer-conflict' );
431                }
432
433                $cmr->setFields( [
434                    'stage'          => Consumer::STAGE_DISABLED,
435                    'stageTimestamp' => wfTimestampNow(),
436                    'deleted'        => $this->vals['suppress'] ] );
437
438                // Log if something actually changed
439                if ( $cmr->save( $dbw ) ) {
440                    $this->makeLogEntry( Utils::getCentralWikiDB(), $cmr, $action, $user, $this->vals['reason'] );
441                    $this->notify( $cmr, $user, $action, $this->vals['reason'] );
442                }
443
444                return $this->success( $cmr );
445            case 'reenable':
446                if ( !$permissionManager->userHasRight( $user, 'mwoauthmanageconsumer' ) ) {
447                    return $this->failure( 'permission_denied', 'badaccess-group0' );
448                }
449
450                $cmr = Consumer::newFromKey( $dbw, $this->vals['consumerKey'] );
451                if ( !$cmr ) {
452                    return $this->failure( 'invalid_consumer_key', 'mwoauth-invalid-consumer-key' );
453                } elseif ( $cmr->getStage() !== Consumer::STAGE_DISABLED ) {
454                    return $this->failure( 'not_disabled', 'mwoauth-consumer-not-disabled' );
455                } elseif ( $cmr->getDeleted() && !$permissionManager->userHasRight( $user, 'mwoauthsuppress' ) ) {
456                    return $this->failure( 'permission_denied', 'badaccess-group0' );
457                } elseif ( !$cmr->checkChangeToken( $context, $this->vals['changeToken'] ) ) {
458                    return $this->failure( 'change_conflict', 'mwoauth-consumer-conflict' );
459                }
460
461                $cmr->setFields( [
462                    'stage'          => Consumer::STAGE_APPROVED,
463                    'stageTimestamp' => wfTimestampNow(),
464                    'deleted'        => 0 ] );
465
466                // Log if something actually changed
467                if ( $cmr->save( $dbw ) ) {
468                    $this->makeLogEntry( Utils::getCentralWikiDB(), $cmr, $action, $user, $this->vals['reason'] );
469                    $this->notify( $cmr, $user, $action, $this->vals['reason'] );
470                }
471
472                return $this->success( $cmr );
473        }
474    }
475
476    /**
477     * @param IDatabase $db
478     * @param int $userId
479     * @return Title
480     */
481    protected function getLogTitle( IDatabase $db, $userId ) {
482        $name = Utils::getCentralUserNameFromId( $userId );
483        return Title::makeTitleSafe( NS_USER, $name );
484    }
485
486    /**
487     * @param IDatabase $dbw
488     * @param Consumer $cmr
489     * @param string $action
490     * @param User $performer
491     * @param string $comment
492     */
493    protected function makeLogEntry(
494        $dbw, Consumer $cmr, $action, User $performer, $comment
495    ) {
496        $logEntry = new ManualLogEntry( 'mwoauthconsumer', $action );
497        $logEntry->setPerformer( $performer );
498        $target = $this->getLogTitle( $dbw, $cmr->getUserId() );
499        $logEntry->setTarget( $target );
500        $logEntry->setComment( $comment );
501        $logEntry->setParameters( [ '4:consumer' => $cmr->getConsumerKey() ] );
502        $logEntry->setRelations( [
503            'OAuthConsumer' => [ $cmr->getConsumerKey() ]
504        ] );
505        $logEntry->insert( $dbw );
506
507        LoggerFactory::getInstance( 'OAuth' )->info(
508            '{user} performed action {action} on consumer {consumer}', [
509                'action' => $action,
510                'user' => $performer->getName(),
511                'consumer' => $cmr->getConsumerKey(),
512                'target' => $target->getText(),
513                'comment' => $comment,
514                'clientip' => $this->getContext()->getRequest()->getIP(),
515            ]
516        );
517    }
518
519    /**
520     * @param Consumer $cmr Consumer which was the subject of the action
521     * @param User $user User who performed the action
522     * @param string $actionType
523     * @param string $comment
524     */
525    protected function notify( $cmr, $user, $actionType, $comment ) {
526        if ( !in_array( $actionType, self::$actions, true ) ) {
527            throw new LogicException( "Invalid action type: $actionType" );
528        } elseif ( !ExtensionRegistry::getInstance()->isLoaded( 'Echo' ) ) {
529            return;
530        } elseif ( !Utils::isCentralWiki() ) {
531            # sanity; should never get here on a replica wiki
532            return;
533        }
534
535        Event::create( [
536            'type' => 'oauth-app-' . $actionType,
537            'agent' => $user,
538            'extra' => [
539                'action' => $actionType,
540                'app-key' => $cmr->getConsumerKey(),
541                'owner-id' => $cmr->getUserId(),
542                'comment' => $comment,
543            ],
544        ] );
545    }
546}