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