Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
47.46% covered (danger)
47.46%
28 / 59
20.00% covered (danger)
20.00%
1 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
GlobalUserOptionsStore
47.46% covered (danger)
47.46%
28 / 59
20.00% covered (danger)
20.00%
1 / 5
42.43
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 fetch
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 fetchBatchForUserNames
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 store
68.18% covered (warning)
68.18%
15 / 22
0.00% covered (danger)
0.00%
0 / 1
5.81
 getStorage
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
1<?php
2
3namespace GlobalPreferences;
4
5use GlobalPreferences\Services\GlobalPreferencesConnectionProvider;
6use GlobalPreferences\Services\GlobalPreferencesHookRunner;
7use MediaWiki\Logger\LoggerFactory;
8use MediaWiki\User\CentralId\CentralIdLookup;
9use MediaWiki\User\Options\UserOptionsStore;
10use MediaWiki\User\UserIdentity;
11use Psr\Log\LoggerInterface;
12use Wikimedia\Rdbms\DBAccessObjectUtils;
13use Wikimedia\Rdbms\IDBAccessObject;
14
15/**
16 * An interface which allows core to update pre-existing global preferences
17 */
18class GlobalUserOptionsStore implements UserOptionsStore {
19
20    private CentralIdLookup $centralIdLookup;
21    private LoggerInterface $logger;
22    private GlobalPreferencesConnectionProvider $globalDbProvider;
23    private GlobalPreferencesHookRunner $globalPreferencesHookRunner;
24
25    public function __construct(
26        CentralIdLookup $centralIdLookup,
27        GlobalPreferencesConnectionProvider $globalDbProvider,
28        GlobalPreferencesHookRunner $globalPreferencesHookRunner
29    ) {
30        $this->centralIdLookup = $centralIdLookup;
31        $this->globalDbProvider = $globalDbProvider;
32        $this->globalPreferencesHookRunner = $globalPreferencesHookRunner;
33        $this->logger = LoggerFactory::getInstance( 'preferences' );
34    }
35
36    /**
37     * @param UserIdentity $user
38     * @param int $recency
39     * @return array|string[]
40     */
41    public function fetch( UserIdentity $user, int $recency ) {
42        $storage = $this->getStorage( $user );
43        if ( !$storage ) {
44            return [];
45        }
46        if ( DBAccessObjectUtils::hasFlags( $recency, IDBAccessObject::READ_LATEST ) ) {
47            $dbType = DB_PRIMARY;
48        } else {
49            $dbType = DB_REPLICA;
50        }
51        return $storage->loadFromDB( $dbType, $recency );
52    }
53
54    /**
55     * @param array $keys
56     * @param array $userNames
57     * @return array
58     */
59    public function fetchBatchForUserNames( array $keys, array $userNames ) {
60        $idsByName = $this->centralIdLookup->lookupOwnedUserNames(
61            array_fill_keys( $userNames, 0 ) );
62        $idsByName = array_filter( $idsByName );
63        if ( !$idsByName ) {
64            return [];
65        }
66        $res = $this->globalDbProvider->getReplicaDatabase()
67            ->newSelectQueryBuilder()
68            ->select( [ 'gp_user', 'gp_property', 'gp_value' ] )
69            ->from( 'global_preferences' )
70            ->where( [
71                'gp_user' => array_values( $idsByName ),
72                'gp_property' => $keys,
73            ] )
74            ->caller( __METHOD__ )
75            ->fetchResultSet();
76
77        $namesById = array_flip( $idsByName );
78        $options = [];
79        foreach ( $res as $row ) {
80            $name = $namesById[$row->gp_user];
81            $options[$row->gp_property][$name] = (string)$row->gp_value;
82        }
83        return $options;
84    }
85
86    /**
87     * @param UserIdentity $user
88     * @param array $updates
89     * @return bool
90     */
91    public function store( UserIdentity $user, array $updates ) {
92        $storage = $this->getStorage( $user );
93        if ( !$storage ) {
94            $this->logger->warning(
95                'Unable to store preference for non-global user "{userName}"',
96                [ 'userName' => $user->getName() ]
97            );
98            return false;
99        }
100        $oldPreferences = $storage->load();
101        $newPreferences = $oldPreferences;
102
103        $replacements = [];
104        $deletions = [];
105        foreach ( $updates as $key => $value ) {
106            if ( $value === null ) {
107                $deletions[] = $key;
108                unset( $newPreferences[$key] );
109            } else {
110                $replacements[$key] = $value;
111                $newPreferences[$key] = $value;
112            }
113        }
114
115        $storage->replaceAndDelete( $replacements, $deletions );
116
117        // Run the set preferences hook here because global preferences may be set by the local options API
118        // and we still want hook handlers to know about this.
119        $this->globalPreferencesHookRunner->onGlobalPreferencesSetGlobalPreferences(
120            $user, $oldPreferences, $newPreferences
121        );
122
123        return $replacements || $deletions;
124    }
125
126    private function getStorage( UserIdentity $user ): ?Storage {
127        // Avoid CentralIdLookup::isOwned() since it has a slow worst case
128        $userName = $user->getName();
129        $id = $this->centralIdLookup->lookupOwnedUserNames( [ $userName => 0 ] )[ $userName ];
130        if ( !$id ) {
131            return null;
132        }
133        return new Storage( $id );
134    }
135}