PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / trunk
Matomo Analytics – Powerful, Privacy-First Insights for WordPress vtrunk
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Settings / Storage / LegacyUserSettingsMigration.php
matomo / app / core / Settings / Storage Last commit date
Backend 1 month ago Factory.php 3 months ago LegacyUserSettingsMigration.php 1 month ago Storage.php 1 year ago UserScopedSettingsAccessManager.php 1 month ago
LegacyUserSettingsMigration.php
254 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik\Settings\Storage;
10
11 use Piwik\Common;
12 use Piwik\Container\StaticContainer;
13 use Piwik\Db;
14 use Piwik\Option;
15 use Piwik\Piwik;
16 use Piwik\Plugins\MobileMessaging\MobileMessaging;
17 use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
18 class LegacyUserSettingsMigration
19 {
20 /**
21 * @var UserScopedSettingsAccessManager
22 */
23 private $store;
24 public function __construct(?\Piwik\Settings\Storage\UserScopedSettingsAccessManager $store = null)
25 {
26 if ($store === null) {
27 $store = new \Piwik\Settings\Storage\UserScopedSettingsAccessManager();
28 }
29 $this->store = $store;
30 }
31 /**
32 * @return array<string, int>
33 */
34 public function migrate() : array
35 {
36 $knownLogins = $this->getKnownLogins();
37 return ['mobileMessaging' => $this->migrateMobileMessagingSettings($knownLogins), 'feedback' => $this->migrateFeedbackSettings($knownLogins), 'professionalServices' => $this->migrateProfessionalServicesSettings($knownLogins), 'usersManagerPreferences' => $this->migrateUsersManagerPreferences($knownLogins)];
38 }
39 /**
40 * @param array<string, bool> $knownLogins
41 */
42 private function migrateMobileMessagingSettings(array $knownLogins) : int
43 {
44 $migratedCount = 0;
45 $suffix = MobileMessaging::USER_SETTINGS_POSTFIX_OPTION;
46 $legacySettings = Option::getLike('%' . $suffix);
47 foreach ($legacySettings as $optionName => $optionValue) {
48 if (!str_ends_with($optionName, $suffix)) {
49 continue;
50 }
51 $login = substr($optionName, 0, -strlen($suffix));
52 if ($login !== '' && !isset($knownLogins[$login])) {
53 Option::delete($optionName);
54 continue;
55 }
56 $existingSettings = $this->getMobileMessagingSettings($login);
57 if (empty($existingSettings) && is_string($optionValue)) {
58 $decoded = json_decode($optionValue, \true);
59 if (is_array($decoded)) {
60 $this->setMobileMessagingSettings($login, $decoded);
61 ++$migratedCount;
62 }
63 }
64 Option::delete($optionName);
65 }
66 return $migratedCount;
67 }
68 /**
69 * @param array<string, bool> $knownLogins
70 */
71 private function migrateFeedbackSettings(array $knownLogins) : int
72 {
73 $migratedCount = 0;
74 $prefix = 'Feedback.nextFeedbackReminder.';
75 $legacySettings = Option::getLike($prefix . '%');
76 foreach ($legacySettings as $optionName => $optionValue) {
77 if (!str_starts_with($optionName, $prefix)) {
78 continue;
79 }
80 $login = substr($optionName, strlen($prefix));
81 if ($login !== '' && isset($knownLogins[$login]) && !$this->hasSetting('Feedback', $login, 'nextFeedbackReminder')) {
82 $this->store->set('Feedback', $login, 'nextFeedbackReminder', $optionValue);
83 ++$migratedCount;
84 }
85 Option::delete($optionName);
86 }
87 return $migratedCount;
88 }
89 /**
90 * @param array<string, bool> $knownLogins
91 */
92 private function migrateProfessionalServicesSettings(array $knownLogins) : int
93 {
94 $migratedCount = 0;
95 $prefix = 'ProfessionalServices.DismissedWidget.';
96 $legacySettings = Option::getLike($prefix . '%');
97 $legacyByLogin = [];
98 $knownLoginNames = array_keys($knownLogins);
99 // sort logins by length to avoid partial collisions
100 usort($knownLoginNames, function ($a, $b) {
101 return strlen($b) <=> strlen($a);
102 });
103 foreach ($legacySettings as $optionName => $optionValue) {
104 if (!str_starts_with($optionName, $prefix)) {
105 continue;
106 }
107 $payload = substr($optionName, strlen($prefix));
108 $matchedLogin = null;
109 $matchedWidget = null;
110 foreach ($knownLoginNames as $candidateLogin) {
111 if (empty($candidateLogin)) {
112 continue;
113 }
114 $suffix = '.' . $candidateLogin;
115 if (!str_ends_with($payload, $suffix)) {
116 continue;
117 }
118 $widgetName = substr($payload, 0, strlen($suffix) * -1);
119 if (empty($widgetName)) {
120 continue;
121 }
122 $matchedLogin = $candidateLogin;
123 $matchedWidget = $widgetName;
124 break;
125 }
126 if (empty($matchedLogin)) {
127 continue;
128 }
129 $legacyByLogin[$matchedLogin][$matchedWidget] = (int) $optionValue;
130 }
131 foreach ($legacyByLogin as $login => $legacyWidgets) {
132 $dismissedWidgets = $this->store->get('ProfessionalServices', $login, 'dismissedWidgets', []);
133 if (!is_array($dismissedWidgets)) {
134 $dismissedWidgets = [];
135 }
136 $hasChanges = \false;
137 foreach ($legacyWidgets as $widgetName => $optionValue) {
138 if (!isset($dismissedWidgets[$widgetName])) {
139 $dismissedWidgets[$widgetName] = $optionValue;
140 $hasChanges = \true;
141 }
142 }
143 if ($hasChanges) {
144 $this->store->set('ProfessionalServices', $login, 'dismissedWidgets', $dismissedWidgets);
145 ++$migratedCount;
146 }
147 }
148 Option::deleteLike('ProfessionalServices.DismissedWidget.%');
149 return $migratedCount;
150 }
151 /**
152 * @param array<string, bool> $knownLogins
153 */
154 private function migrateUsersManagerPreferences(array $knownLogins) : int
155 {
156 $migratedCount = 0;
157 $preferenceNames = $this->getSupportedPreferenceNames();
158 foreach ($preferenceNames as $preferenceName) {
159 $suffix = UsersManagerAPI::OPTION_NAME_PREFERENCE_SEPARATOR . $preferenceName;
160 $legacySettings = Option::getLike('%' . $suffix);
161 foreach ($legacySettings as $optionName => $optionValue) {
162 if (!str_ends_with($optionName, $suffix)) {
163 continue;
164 }
165 $login = substr($optionName, 0, -strlen($suffix));
166 if ($login === '' || !$this->isLikelyValidLogin($login)) {
167 continue;
168 }
169 if (isset($knownLogins[$login]) && !$this->hasSetting('UsersManager', $login, $preferenceName)) {
170 $this->store->set('UsersManager', $login, $preferenceName, $optionValue);
171 ++$migratedCount;
172 }
173 /**
174 * @deprecated Should be removed with Matomo 6, LoginLdap should be
175 * updated to not rely on Option storage for this setting
176 */
177 if ($preferenceName === 'isLDAPUser' && isset($knownLogins[$login])) {
178 // Keep legacy option key for LoginLdap submodule compatibility.
179 continue;
180 }
181 Option::delete($optionName);
182 }
183 }
184 return $migratedCount;
185 }
186 /**
187 * @return array<string, bool>
188 */
189 private function getKnownLogins() : array
190 {
191 $logins = ['anonymous' => \true];
192 $rows = Db::fetchAll('SELECT login FROM `' . Common::prefixTable('user') . '`');
193 foreach ($rows as $row) {
194 if (!empty($row['login'])) {
195 $logins[$row['login']] = \true;
196 }
197 }
198 return $logins;
199 }
200 /**
201 * @return string[]
202 */
203 private function getSupportedPreferenceNames() : array
204 {
205 $preferenceNames = [UsersManagerAPI::PREFERENCE_DEFAULT_REPORT, UsersManagerAPI::PREFERENCE_DEFAULT_REPORT_DATE, 'isLDAPUser', 'hideSegmentDefinitionChangeMessage'];
206 $customPreferenceNames = StaticContainer::get('usersmanager.user_preference_names');
207 if (!is_array($customPreferenceNames)) {
208 $customPreferenceNames = [];
209 }
210 $customPreferenceNames = array_values(array_filter($customPreferenceNames, 'is_string'));
211 return array_values(array_unique(array_merge($preferenceNames, $customPreferenceNames)));
212 }
213 private function isLikelyValidLogin(string $login) : bool
214 {
215 try {
216 Piwik::checkValidLoginString($login);
217 return \true;
218 } catch (\Exception $exception) {
219 return \false;
220 }
221 }
222 private function hasSetting(string $pluginName, string $userLogin, string $settingName) : bool
223 {
224 $settings = $this->store->getAll($pluginName, $userLogin);
225 return array_key_exists($settingName, $settings);
226 }
227 /**
228 * @return array<string, mixed>
229 */
230 private function getMobileMessagingSettings(string $userLogin) : array
231 {
232 if ($userLogin === '') {
233 $settings = $this->getFactory()->getPluginStorage('MobileMessaging', $userLogin)->getBackend()->load();
234 return is_array($settings) ? $settings : [];
235 }
236 return $this->store->getAll('MobileMessaging', $userLogin);
237 }
238 /**
239 * @param array<string, mixed> $settings
240 */
241 private function setMobileMessagingSettings(string $userLogin, array $settings) : void
242 {
243 if ($userLogin === '') {
244 $this->getFactory()->getPluginStorage('MobileMessaging', $userLogin)->getBackend()->save($settings);
245 return;
246 }
247 $this->store->setAll('MobileMessaging', $userLogin, $settings);
248 }
249 private function getFactory() : \Piwik\Settings\Storage\Factory
250 {
251 return StaticContainer::get(\Piwik\Settings\Storage\Factory::class);
252 }
253 }
254