Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 11
702
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onRegistration
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
12
 onExtensionFunctions
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 onChangeTagCanCreate
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 onMergeAccountFromTo
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 doUserIdMerge
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 onListDefinedTags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onChangeTagsListActive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUsedConsumerTags
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
72
 onSetupAfterCache
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onApiRsdServiceApis
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\OAuth\Backend;
4
5use MediaWiki\Api\Hook\ApiRsdServiceApisHook;
6use MediaWiki\ChangeTags\Hook\ChangeTagCanCreateHook;
7use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
8use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
9use MediaWiki\Extension\OAuth\Frontend\OAuthLogFormatter;
10use MediaWiki\Hook\SetupAfterCacheHook;
11use MediaWiki\Status\Status;
12use MediaWiki\Storage\NameTableAccessException;
13use MediaWiki\Storage\NameTableStore;
14use MediaWiki\User\User;
15use MediaWiki\WikiMap\WikiMap;
16use Wikimedia\Rdbms\IConnectionProvider;
17
18/**
19 * Class containing hooked functions for an OAuth environment
20 */
21class Hooks implements
22    ApiRsdServiceApisHook,
23    ChangeTagsListActiveHook,
24    ChangeTagCanCreateHook,
25    ListDefinedTagsHook,
26    SetupAfterCacheHook
27{
28
29    /** @var NameTableStore */
30    private $changeTagDefStore;
31
32    /** @var IConnectionProvider */
33    private $connectionProvider;
34
35    /**
36     * @param NameTableStore $changeTagDefStore
37     * @param IConnectionProvider $connectionProvider
38     */
39    public function __construct( NameTableStore $changeTagDefStore, IConnectionProvider $connectionProvider
40    ) {
41        $this->changeTagDefStore = $changeTagDefStore;
42        $this->connectionProvider = $connectionProvider;
43    }
44
45    /**
46     * Called right after configuration variables have been set.
47     */
48    public static function onRegistration() {
49        global $wgOAuth2PrivateKey, $wgOAuth2PublicKey;
50
51        // Set $wgOAuth2PrivateKey and $wgOAuth2PublicKey for Wikimedia Jenkins, PHPUnit.
52        if ( defined( 'MW_PHPUNIT_TEST' ) || defined( 'MW_QUIBBLE_CI' ) ) {
53            $wgOAuth2PrivateKey = <<<EOK
54-----BEGIN RSA PRIVATE KEY-----
55MIIBOwIBAAJBAMBGXQYJ2lXzLuQkRlWoqYJvSnNGfRvPBUVsbHfFPyCr8i6jBPcO
56vtMLFMRAaq4quRDFgQ7YQLvKTqjpN+bo7RECAwEAAQJBAKP3XTzZCihhyYskpBZI
57TsW8wnCrm+UrFgOuApHg04S3oeUXpNApxxGy+EX0aBsVoPBuisyBjiJDIFssdgJa
58IwECIQDuMipv8QOzA9qJPPpXZCQQN6znXjSE3jZhrBH879SDBQIhAM6lgY0lWB0N
59lhQZWtM8jRcxtJUFrApEizE6WFxj/LedAiEAyINgaAVqiMror3iugNyi4ygLHGWY
60LnVlMAmKxvMZYQUCIAYTeb6ztWaNSrdmk3QYmLFw5bVoCEn4//q/k2+MBRdFAiA2
61MJWJuom6IpoP0UrM/gJbwGxwgZymb4jL+sKFoIqGmA==
62-----END RSA PRIVATE KEY-----
63EOK;
64            $wgOAuth2PublicKey = <<<EOK
65-----BEGIN PUBLIC KEY-----
66MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAMBGXQYJ2lXzLuQkRlWoqYJvSnNGfRvP
67BUVsbHfFPyCr8i6jBPcOvtMLFMRAaq4quRDFgQ7YQLvKTqjpN+bo7RECAwEAAQ==
68-----END PUBLIC KEY-----
69EOK;
70        }
71    }
72
73    public static function onExtensionFunctions() {
74        global $wgLogTypes, $wgLogNames,
75            $wgLogHeaders, $wgLogActionsHandlers, $wgActionFilteredLogs;
76
77        if ( Utils::isCentralWiki() ) {
78            $wgLogTypes[] = 'mwoauthconsumer';
79            $wgLogNames['mwoauthconsumer'] = 'mwoauthconsumer-consumer-logpage';
80            $wgLogHeaders['mwoauthconsumer'] = 'mwoauthconsumer-consumer-logpagetext';
81            $wgLogActionsHandlers['mwoauthconsumer/*'] = [
82                'class' => OAuthLogFormatter::class,
83                'services' => [
84                    'LinkRenderer',
85                    'TitleFactory',
86                    'UserEditTracker',
87                ],
88            ];
89            $wgActionFilteredLogs['mwoauthconsumer'] = [
90                'approve' => [ 'approve' ],
91                'create-owner-only' => [ 'create-owner-only' ],
92                'disable' => [ 'disable' ],
93                'propose' => [ 'propose' ],
94                'propose-autoapproved' => [ 'propose-autoapproved' ],
95                'reenable' => [ 'reenable' ],
96                'reject' => [ 'reject' ],
97                'update' => [ 'update' ],
98            ];
99        }
100    }
101
102    /**
103     * Reserve change tags that look like an OAuth change tag.
104     *
105     * @param string $tag
106     * @param User|null $user
107     * @param Status &$status
108     */
109    public function onChangeTagCanCreate( $tag, $user, &$status ) {
110        if ( Utils::isReservedTagName( $tag ) ) {
111            $status->fatal( 'mwoauth-tag-reserved' );
112        }
113    }
114
115    public static function onMergeAccountFromTo( User $oUser, User $nUser ) {
116        global $wgMWOAuthSharedUserIDs;
117
118        if ( !$wgMWOAuthSharedUserIDs ) {
119            $oldid = $oUser->getId();
120            $newid = $nUser->getId();
121            if ( $oldid && $newid ) {
122                self::doUserIdMerge( $oldid, $newid );
123            }
124        }
125
126        return true;
127    }
128
129    protected static function doUserIdMerge( $oldid, $newid ) {
130        $dbw = Utils::getCentralDB( DB_PRIMARY );
131        // Merge any consumers register to this user
132        $dbw->newUpdateQueryBuilder()
133            ->update( 'oauth_registered_consumer' )
134            ->set( [ 'oarc_user_id' => $newid ] )
135            ->where( [ 'oarc_user_id' => $oldid ] )
136            ->caller( __METHOD__ )
137            ->execute();
138        // Delete any acceptance tokens by the old user ID
139        $dbw->newDeleteQueryBuilder()
140            ->deleteFrom( 'oauth_accepted_consumer' )
141            ->where( [ 'oaac_user_id' => $oldid ] )
142            ->caller( __METHOD__ )
143            ->execute();
144    }
145
146    public function onListDefinedTags( &$tags ) {
147        return $this->getUsedConsumerTags( false, $tags );
148    }
149
150    public function onChangeTagsListActive( &$tags ) {
151        return $this->getUsedConsumerTags( true, $tags );
152    }
153
154    /**
155     * List tags that should show as defined/active on Special:Tags
156     *
157     * Handles both the ChangeTagsListActive and ListDefinedTags hooks. Only
158     * lists those tags that are actually in use on the local wiki, to avoid
159     * flooding Special:Tags with tags for consumers that will never be making
160     * logged actions.
161     *
162     * @param bool $activeOnly true for ChangeTagsListActive, false for ListDefinedTags
163     * @param array &$tags
164     * @return bool
165     */
166    private function getUsedConsumerTags( $activeOnly, &$tags ) {
167        // Step 1: Get the list of (active) consumers' tags for this wiki
168        $db = Utils::getCentralDB( DB_REPLICA );
169        $conds = [
170            $db->expr( 'oarc_wiki', '=', [ '*', WikiMap::getCurrentWikiId() ] ),
171            'oarc_deleted' => 0,
172        ];
173        if ( $activeOnly ) {
174            $conds[] = $db->expr( 'oarc_stage', '=', [ Consumer::STAGE_APPROVED, Consumer::STAGE_PROPOSED ] );
175        }
176        $res = $db->newSelectQueryBuilder()
177            ->select( 'oarc_id' )
178            ->from( 'oauth_registered_consumer' )
179            ->where( $conds )
180            ->caller( __METHOD__ )
181            ->fetchResultSet();
182        $allTags = [];
183        foreach ( $res as $row ) {
184            $allTags[] = Utils::getTagName( $row->oarc_id );
185        }
186
187        // Step 2: Return only those that are in use.
188        $tagIds = [];
189        foreach ( $allTags as $tag ) {
190            try {
191                $tagIds[] = $this->changeTagDefStore->getId( $tag );
192            } catch ( NameTableAccessException $ex ) {
193                continue;
194            }
195        }
196        if ( $tagIds === [] ) {
197            // Nothing to add, return
198            return true;
199        }
200        $conditions = [ 'ct_tag_id' => $tagIds ];
201        $field = 'ct_tag_id';
202
203        if ( $allTags ) {
204            $db = $this->connectionProvider->getReplicaDatabase();
205
206            $res = $db->newSelectQueryBuilder()
207                ->select( $field )
208                ->distinct()
209                ->from( 'change_tag' )
210                ->where( $conditions )
211                ->caller( __METHOD__ )
212                ->fetchResultSet();
213            foreach ( $res as $row ) {
214                $tags[] = $this->changeTagDefStore->getName( intval( $row->ct_tag_id ) );
215            }
216        }
217
218        return true;
219    }
220
221    public function onSetupAfterCache() {
222        global $wgMWOAuthCentralWiki, $wgMWOAuthSharedUserIDs;
223
224        if ( $wgMWOAuthCentralWiki === false ) {
225            // Treat each wiki as its own "central wiki" as there is no actual one
226            $wgMWOAuthCentralWiki = WikiMap::getCurrentWikiId();
227        } else {
228            // There is actually a central wiki, requiring global user IDs via hook
229            $wgMWOAuthSharedUserIDs = true;
230        }
231    }
232
233    public function onApiRsdServiceApis( &$apis ) {
234        $apis['MediaWiki']['settings']['OAuth'] = true;
235    }
236}