Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.97% covered (warning)
73.97%
54 / 73
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
LocalUserOptionsStore
73.97% covered (warning)
73.97%
54 / 73
50.00% covered (danger)
50.00%
2 / 4
23.71
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
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 fetchBatchForUserNames
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 store
95.00% covered (success)
95.00%
38 / 40
0.00% covered (danger)
0.00%
0 / 1
10
1<?php
2
3namespace MediaWiki\User\Options;
4
5use MediaWiki\HookContainer\HookRunner;
6use MediaWiki\User\UserIdentity;
7use Wikimedia\Rdbms\DBAccessObjectUtils;
8use Wikimedia\Rdbms\IConnectionProvider;
9use Wikimedia\Rdbms\IDBAccessObject;
10
11class LocalUserOptionsStore implements UserOptionsStore {
12    private IConnectionProvider $dbProvider;
13    private HookRunner $hookRunner;
14
15    /** @var array[] Cached options for each user, by user ID */
16    private array $optionsFromDb;
17
18    public function __construct( IConnectionProvider $dbProvider, HookRunner $hookRunner ) {
19        $this->dbProvider = $dbProvider;
20        $this->hookRunner = $hookRunner;
21    }
22
23    public function fetch(
24        UserIdentity $user,
25        int $recency
26    ): array {
27        // In core, only users with local accounts may have preferences
28        if ( !$user->getId() ) {
29            return [];
30        }
31
32        $dbr = DBAccessObjectUtils::getDBFromRecency( $this->dbProvider, $recency );
33        $res = $dbr->newSelectQueryBuilder()
34            ->select( [ 'up_property', 'up_value' ] )
35            ->from( 'user_properties' )
36            ->where( [ 'up_user' => $user->getId() ] )
37            ->recency( $recency )
38            ->caller( __METHOD__ )->fetchResultSet();
39
40        $options = [];
41        foreach ( $res as $row ) {
42            $options[$row->up_property] = (string)$row->up_value;
43        }
44
45        $this->optionsFromDb[$user->getId()] = $options;
46        return $options;
47    }
48
49    /** @inheritDoc */
50    public function fetchBatchForUserNames( array $keys, array $userNames ) {
51        if ( !$keys || !$userNames ) {
52            return [];
53        }
54
55        $options = [];
56        $res = $this->dbProvider->getReplicaDatabase()
57            ->newSelectQueryBuilder()
58            ->select( [ 'user_name', 'up_property', 'up_value' ] )
59            ->from( 'user_properties' )
60            ->join( 'user', null, 'user_id=up_user' )
61            ->where( [
62                'up_property' => $keys,
63                'user_name' => $userNames
64            ] )
65            ->caller( __METHOD__ )
66            ->fetchResultSet();
67        foreach ( $res as $row ) {
68            $options[$row->up_property][$row->user_name] = (string)$row->up_value;
69        }
70        return $options;
71    }
72
73    /** @inheritDoc */
74    public function store( UserIdentity $user, array $updates ) {
75        // In core, only users with local accounts may have preferences
76        if ( !$user->getId() ) {
77            return false;
78        }
79
80        $oldOptions = $this->optionsFromDb[ $user->getId() ]
81            ?? $this->fetch( $user, IDBAccessObject::READ_LATEST );
82        $newOptions = $oldOptions;
83        $keysToDelete = [];
84        $rowsToInsert = [];
85        foreach ( $updates as $key => $value ) {
86            if ( !UserOptionsManager::isValueEqual(
87                $value, $oldOptions[$key] ?? null )
88            ) {
89                // Update by deleting and reinserting
90                if ( array_key_exists( $key, $oldOptions ) ) {
91                    $keysToDelete[] = $key;
92                    unset( $newOptions[$key] );
93                }
94                if ( $value !== null ) {
95                    $truncValue = mb_strcut( $value, 0,
96                        UserOptionsManager::MAX_BYTES_OPTION_VALUE );
97                    $rowsToInsert[] = [
98                        'up_user' => $user->getId(),
99                        'up_property' => $key,
100                        'up_value' => $truncValue,
101                    ];
102                    $newOptions[$key] = $truncValue;
103                }
104            }
105        }
106        if ( !count( $keysToDelete ) && !count( $rowsToInsert ) ) {
107            // Nothing to do
108            return false;
109        }
110
111        // Do the DELETE
112        $dbw = $this->dbProvider->getPrimaryDatabase();
113        if ( $keysToDelete ) {
114            $dbw->newDeleteQueryBuilder()
115                ->deleteFrom( 'user_properties' )
116                ->where( [ 'up_user' => $user->getId() ] )
117                ->andWhere( [ 'up_property' => $keysToDelete ] )
118                ->caller( __METHOD__ )->execute();
119        }
120        if ( $rowsToInsert ) {
121            // Insert the new preference rows
122            $dbw->newInsertQueryBuilder()
123                ->insertInto( 'user_properties' )
124                ->ignore()
125                ->rows( $rowsToInsert )
126                ->caller( __METHOD__ )->execute();
127        }
128
129        // Update cache
130        $this->optionsFromDb[$user->getId()] = $newOptions;
131
132        $this->hookRunner->onLocalUserOptionsStoreSave( $user, $oldOptions, $newOptions );
133
134        return true;
135    }
136
137}