Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.47% |
77 / 79 |
|
87.50% |
7 / 8 |
CRAP | |
0.00% |
0 / 1 |
Storage | |
97.47% |
77 / 79 |
|
87.50% |
7 / 8 |
23 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
load | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
loadFromDB | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
save | |
100.00% |
36 / 36 |
|
100.00% |
1 / 1 |
11 | |||
delete | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
getDatabase | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
3.07 | |||
getCache | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCacheKey | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * This file contains only the Storage class. |
4 | * @package GlobalPreferences |
5 | */ |
6 | |
7 | namespace GlobalPreferences; |
8 | |
9 | use MediaWiki\MediaWikiServices; |
10 | use WANObjectCache; |
11 | use Wikimedia\LightweightObjectStore\ExpirationAwareness; |
12 | use Wikimedia\Rdbms\IDatabase; |
13 | |
14 | /** |
15 | * This class handles all database storage of global preferences. |
16 | * @package GlobalPreferences |
17 | */ |
18 | class Storage { |
19 | |
20 | /** The non-prefixed name of the global preferences database table. */ |
21 | public const TABLE_NAME = 'global_preferences'; |
22 | |
23 | /** Update this constant when making incompatible changes to caching */ |
24 | private const CACHE_VERSION = 1; |
25 | |
26 | /** Cache lifetime */ |
27 | private const CACHE_TTL = ExpirationAwareness::TTL_WEEK; |
28 | |
29 | /** Instructs preference loading code to load the preferences from cache directly */ |
30 | public const SKIP_CACHE = true; |
31 | |
32 | /** @var int The global user ID. */ |
33 | protected $userId; |
34 | |
35 | /** |
36 | * Create a new Global Preferences Storage object for a given user. |
37 | * @param int $userId The global user ID. |
38 | */ |
39 | public function __construct( int $userId ) { |
40 | $this->userId = $userId; |
41 | } |
42 | |
43 | /** |
44 | * Get the user's global preferences. |
45 | * |
46 | * @param bool $skipCache Whether the preferences should be loaded strictly from DB |
47 | * @return string[] Keyed by the preference name. |
48 | */ |
49 | public function load( bool $skipCache = false ): array { |
50 | if ( $skipCache ) { |
51 | return $this->loadFromDB(); |
52 | } |
53 | |
54 | $cache = $this->getCache(); |
55 | $key = $this->getCacheKey(); |
56 | |
57 | return $cache->getWithSetCallback( $key, self::CACHE_TTL, function () { |
58 | return $this->loadFromDB(); |
59 | } ); |
60 | } |
61 | |
62 | /** |
63 | * @param int $dbType One of DB_* constants |
64 | * @return string[] |
65 | */ |
66 | protected function loadFromDB( $dbType = DB_REPLICA ): array { |
67 | $dbr = $this->getDatabase( $dbType ); |
68 | $res = $dbr->select( |
69 | static::TABLE_NAME, |
70 | [ 'gp_property', 'gp_value' ], |
71 | [ 'gp_user' => $this->userId ], |
72 | __METHOD__ |
73 | ); |
74 | $preferences = []; |
75 | foreach ( $res as $row ) { |
76 | $preferences[$row->gp_property] = $row->gp_value; |
77 | } |
78 | return $preferences; |
79 | } |
80 | |
81 | /** |
82 | * Save a set of global preferences. All existing preferences will be deleted before the new |
83 | * ones are saved. |
84 | * @param string[] $newPrefs Keyed by the preference name. |
85 | * @param string[] $knownPrefs Only work with the preferences we know about. |
86 | * @param string[] $checkMatricesToClear List of check matrix controls that |
87 | * need their rows purged |
88 | */ |
89 | public function save( array $newPrefs, array $knownPrefs, array $checkMatricesToClear = [] |
90 | ): void { |
91 | $currentPrefs = $this->loadFromDB( DB_PRIMARY ); |
92 | |
93 | // Find records needing an insert or update |
94 | $save = []; |
95 | $delete = []; |
96 | foreach ( $newPrefs as $prop => $value ) { |
97 | if ( $value !== null ) { |
98 | if ( !isset( $currentPrefs[$prop] ) || $currentPrefs[$prop] != $value ) { |
99 | $save[$prop] = $value; |
100 | } |
101 | } else { |
102 | $delete[] = $prop; |
103 | } |
104 | } |
105 | |
106 | // Assemble the records to save |
107 | $rows = []; |
108 | foreach ( $save as $prop => $value ) { |
109 | $rows[] = [ |
110 | 'gp_user' => $this->userId, |
111 | 'gp_property' => $prop, |
112 | 'gp_value' => $value, |
113 | ]; |
114 | } |
115 | // Save |
116 | if ( $rows ) { |
117 | $dbw = $this->getDatabase( DB_PRIMARY ); |
118 | $dbw->newReplaceQueryBuilder() |
119 | ->replaceInto( static::TABLE_NAME ) |
120 | ->uniqueIndexFields( [ 'gp_user', 'gp_property' ] ) |
121 | ->rows( $rows ) |
122 | ->caller( __METHOD__ ) |
123 | ->execute(); |
124 | } |
125 | |
126 | // Delete unneeded rows |
127 | $keys = array_keys( $currentPrefs ); |
128 | // Only delete prefs present on the local wiki |
129 | $keys = array_intersect( $keys, $knownPrefs ); |
130 | $keys = array_values( array_diff( $keys, array_keys( $newPrefs ) ) ); |
131 | $delete = array_merge( $delete, $keys ); |
132 | |
133 | // And specifically nuke the rows of a deglobalized CheckMatrix |
134 | foreach ( $checkMatricesToClear as $matrix ) { |
135 | foreach ( array_keys( $currentPrefs ) as $pref ) { |
136 | if ( strpos( $pref, $matrix ) === 0 ) { |
137 | $delete[] = $pref; |
138 | } |
139 | } |
140 | } |
141 | $delete = array_unique( $delete ); |
142 | |
143 | if ( $delete ) { |
144 | $this->delete( $delete ); |
145 | } |
146 | |
147 | $key = $this->getCacheKey(); |
148 | // Because we don't have the full preferences, just clear the cache |
149 | $this->getCache()->delete( $key ); |
150 | } |
151 | |
152 | /** |
153 | * Delete all of this user's global preferences. |
154 | * @param string[]|null $knownPrefs Only delete the preferences we know about. |
155 | */ |
156 | public function delete( ?array $knownPrefs = null ): void { |
157 | $db = $this->getDatabase( DB_PRIMARY ); |
158 | $conds = [ 'gp_user' => $this->userId ]; |
159 | if ( $knownPrefs !== null ) { |
160 | $conds['gp_property'] = $knownPrefs; |
161 | } |
162 | $db->newDeleteQueryBuilder() |
163 | ->deleteFrom( static::TABLE_NAME ) |
164 | ->where( $conds ) |
165 | ->caller( __METHOD__ ) |
166 | ->execute(); |
167 | $key = $this->getCacheKey(); |
168 | $this->getCache()->delete( $key ); |
169 | } |
170 | |
171 | /** |
172 | * Get the database object pointing to the Global Preferences database. |
173 | * @param int $type One of the DB_* constants |
174 | * @return IDatabase |
175 | */ |
176 | protected function getDatabase( int $type = DB_REPLICA ): IDatabase { |
177 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
178 | $globalPreferencesDB = (string)$config->get( 'GlobalPreferencesDB' ); |
179 | $sharedDB = (string)$config->get( 'SharedDB' ); |
180 | $lbf = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); |
181 | if ( $globalPreferencesDB != '' ) { |
182 | $domainId = $globalPreferencesDB; |
183 | } elseif ( $sharedDB != '' ) { |
184 | $domainId = $sharedDB; |
185 | } else { |
186 | // local wiki |
187 | $domainId = false; |
188 | } |
189 | |
190 | return $lbf->getMainLB( $domainId )->getConnection( $type, [], $domainId ); |
191 | } |
192 | |
193 | /** |
194 | * @return WANObjectCache |
195 | */ |
196 | protected function getCache(): WANObjectCache { |
197 | return MediaWikiServices::getInstance()->getMainWANObjectCache(); |
198 | } |
199 | |
200 | /** |
201 | * @return string |
202 | */ |
203 | protected function getCacheKey(): string { |
204 | return $this->getCache() |
205 | ->makeGlobalKey( 'globalpreferences', 'prefs', self::CACHE_VERSION, $this->userId ); |
206 | } |
207 | } |