PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.0.7
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.0.7
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 / classes / WpMatomo / User / Sync.php
matomo / classes / WpMatomo / User Last commit date
Sync.php 2 years ago
Sync.php
385 lines
1 <?php
2 /**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 * @package matomo
8 */
9
10 namespace WpMatomo\User;
11
12 use Exception;
13 use Piwik\Access;
14 use Piwik\Access\Role\Admin;
15 use Piwik\Access\Role\View;
16 use Piwik\Access\Role\Write;
17 use Piwik\Auth\Password;
18 use Piwik\Common;
19 use Piwik\Date;
20 use Piwik\Plugin;
21 use Piwik\Plugins\LanguagesManager\API;
22 use Piwik\Plugins\UsersManager;
23 use Piwik\Plugins\UsersManager\Model;
24 use WP_User;
25 use WpMatomo\Bootstrap;
26 use WpMatomo\Capabilities;
27 use WpMatomo\Logger;
28 use WpMatomo\ScheduledTasks;
29 use WpMatomo\Site;
30 use WpMatomo\User;
31
32 if ( ! defined( 'ABSPATH' ) ) {
33 exit; // if accessed directly
34 }
35
36 class Sync {
37
38 /**
39 * actually allowed is 100 characters...
40 * but we do -5 to have some room to append `wp_`.$login.XYZ if needed
41 */
42 const MAX_USER_NAME_LENGTH = 95;
43
44 /**
45 * @var Logger
46 */
47 private $logger;
48
49 public function __construct() {
50 $this->logger = new Logger();
51 }
52
53 public function register_hooks() {
54 add_action( 'add_user_role', [ $this, 'sync_current_users_1000' ], $prio = 10, $args = 0 );
55 add_action( 'remove_user_role', [ $this, 'sync_current_users_1000' ], $prio = 10, $args = 0 );
56 add_action( 'add_user_to_blog', [ $this, 'sync_current_users_1000' ], $prio = 10, $args = 0 );
57 add_action( 'remove_user_from_blog', [ $this, 'sync_current_users_1000' ], $prio = 10, $args = 0 );
58 add_action( 'user_register', [ $this, 'sync_current_users_1000' ], $prio = 10, $args = 0 );
59 add_action( 'update_option_WPLANG', [ $this, 'on_site_language_change' ], $prio = 10, $args = 0 );
60 add_action( 'profile_update', [ $this, 'sync_maybe_background' ], $prio = 10, $args = 0 );
61 }
62
63 public function sync_maybe_background() {
64 global $pagenow;
65 if ( is_admin() && 'users.php' === $pagenow ) {
66 // eg for profile update we don't want to sync directly see #365 as it could cause issues with other plugins
67 // if they eg alter `get_users` option
68 wp_schedule_single_event( time() + 5, ScheduledTasks::EVENT_SYNC );
69 } else {
70 $this->sync_current_users_1000();
71 }
72 }
73
74 public function on_site_language_change() {
75 unset( $GLOBALS['locale'] ); // same thing that's done after saving in options.php
76
77 $this->sync_current_users_1000();
78 }
79
80 public function sync_all() {
81 if ( function_exists( 'is_multisite' ) && is_multisite() ) {
82 foreach ( get_sites() as $site ) {
83 if ( 1 === (int) $site->deleted ) {
84 continue;
85 }
86
87 switch_to_blog( $site->blog_id );
88
89 $idsite = Site::get_matomo_site_id( $site->blog_id );
90
91 try {
92 if ( $idsite ) {
93 $users = $this->get_users( [ 'blog_id' => $site->blog_id ] );
94 $this->sync_users( $users, $idsite );
95 }
96 } catch ( Exception $e ) {
97 // we don't want to rethrow exception otherwise some other blogs might never sync
98 $this->logger->log_exception( 'user_sync ', $e );
99 }
100
101 restore_current_blog();
102 }
103 } else {
104 $this->sync_current_users();
105 }
106 }
107
108 private function get_users( $options = [] ) {
109 /** @var WP_User[] $users */
110 $users = get_users( $options );
111
112 $current_user = wp_get_current_user();
113 if ( ! empty( $current_user ) && ! empty( $current_user->user_login ) ) {
114 // refs https://github.com/matomo-org/matomo-for-wordpress/issues/365
115 // some other plugins may under circumstances overwrite the get_users query and not return all users
116 // as a result we would delete some users in the matomo users table. this way we make sure at least the current
117 // user will be added and not deleted even if the list of users is not complete
118 $found = false;
119 foreach ( $users as $user ) {
120 if ( $user->user_login === $current_user->user_login ) {
121 $found = true;
122 break;
123 }
124 }
125 if ( ! $found ) {
126 $users[] = $current_user;
127 }
128 }
129
130 if ( is_multisite() ) {
131 $super_admins = get_super_admins();
132 if ( ! empty( $super_admins ) ) {
133 foreach ( $super_admins as $super_admin ) {
134 $found = false;
135 foreach ( $users as $user ) {
136 if ( $user->user_login === $super_admin ) {
137 $found = true;
138 break;
139 }
140 }
141 if ( ! $found ) {
142 $user = get_user_by( 'login', $super_admin );
143 if ( ! empty( $user ) ) {
144 $users[] = $user;
145 }
146 }
147 }
148 }
149 }
150
151 return $users;
152 }
153
154 public function sync_current_users() {
155 $idsite = Site::get_matomo_site_id( get_current_blog_id() );
156 if ( $idsite ) {
157 $users = $this->get_users();
158 $this->sync_users( $users, $idsite );
159 }
160 }
161
162 /**
163 * similar method to sync_current_users which synchronise on the fly only if we have less than 1000 users.
164 * Otherwise it will be done by a background task
165 *
166 * @return void
167 * @see https://github.com/matomo-org/matomo-for-wordpress/issues/460
168 * @see Sync::sync_current_users()
169 */
170 public function sync_current_users_1000() {
171 if ( ! is_plugin_active( 'matomo/matomo.php' ) ) {
172 // @see https://github.com/matomo-org/matomo-for-wordpress/issues/577
173 return;
174 }
175 $idsite = Site::get_matomo_site_id( get_current_blog_id() );
176 if ( $idsite ) {
177 $num_users = count_users();
178 $num_users = $num_users['total_users'];
179 if ( $num_users < 1000 ) {
180 $users = $this->get_users();
181 $this->sync_users( $users, $idsite );
182 }
183 }
184 }
185
186 /**
187 * Sync all users. Make sure to always pass all sites that exist within a given site... you cannot just sync an individual
188 * user... we would delete all other users
189 *
190 * @param WP_User[] $users
191 * @param int|string $idsite
192 */
193 protected function sync_users( $users, $idsite ) {
194 Bootstrap::do_bootstrap();
195
196 $this->logger->log( 'Matomo will now sync ' . count( $users ) . ' users' );
197
198 $super_users = [];
199 $logins_with_some_view_access = [ 'anonmyous' ]; // may or may not exist... we don't want to delete this user though
200 $user_model = new Model();
201
202 // need to make sure we recreate new instance later with latest dependencies in case they changed
203 API::unsetInstance();
204
205 foreach ( $users as $user ) {
206 $user_id = $user->ID;
207
208 // todo if we used transactions we could commit it after a possibly new access has been added
209 // to prevent UI preventing randomly saying no access between deleting and adding access
210
211 $mapped_matomo_login = User::get_matomo_user_login( $user_id );
212
213 $matomo_login = null;
214
215 if ( user_can( $user, Capabilities::KEY_SUPERUSER ) ) {
216 $matomo_login = $this->ensure_user_exists( $user );
217 $super_users[ $matomo_login ] = $user;
218 $logins_with_some_view_access[] = $matomo_login;
219 } elseif ( user_can( $user, Capabilities::KEY_ADMIN ) ) {
220 $matomo_login = $this->ensure_user_exists( $user );
221 $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] );
222 $user_model->addUserAccess( $matomo_login, Admin::ID, [ $idsite ] );
223 $user_model->setSuperUserAccess( $matomo_login, false );
224 $logins_with_some_view_access[] = $matomo_login;
225 } elseif ( user_can( $user, Capabilities::KEY_WRITE ) ) {
226 $matomo_login = $this->ensure_user_exists( $user );
227 $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] );
228 $user_model->addUserAccess( $matomo_login, Write::ID, [ $idsite ] );
229 $user_model->setSuperUserAccess( $matomo_login, false );
230 $logins_with_some_view_access[] = $matomo_login;
231 } elseif ( user_can( $user, Capabilities::KEY_VIEW ) ) {
232 $matomo_login = $this->ensure_user_exists( $user );
233 $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] );
234 $user_model->addUserAccess( $matomo_login, View::ID, [ $idsite ] );
235 $user_model->setSuperUserAccess( $matomo_login, false );
236 $logins_with_some_view_access[] = $matomo_login;
237 } elseif ( $mapped_matomo_login ) {
238 $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] );
239 }
240
241 if ( $matomo_login ) {
242 $locale = get_user_locale( $user->ID );
243 $locale_dash = Common::mb_strtolower( str_replace( '_', '-', $locale ) );
244 $parts = [];
245 if ( $locale && in_array( $locale_dash, [ 'zh-cn', 'zh-tw', 'pt-br', 'es-ar' ], true ) ) {
246 $parts = [ $locale_dash ];
247 } elseif ( ! empty( $locale ) && is_string( $locale ) ) {
248 $parts = explode( '_', $locale );
249 }
250
251 if ( ! empty( $parts[0] ) ) {
252 $lang = $parts[0];
253 if ( Plugin\Manager::getInstance()->isPluginActivated( 'LanguagesManager' )
254 && Plugin\Manager::getInstance()->isPluginInstalled( 'LanguagesManager' )
255 && API::getInstance()->isLanguageAvailable( $lang ) ) {
256 $user_lang_model = new \Piwik\Plugins\LanguagesManager\Model();
257 $user_lang_model->setLanguageForUser( $matomo_login, $lang );
258 }
259 }
260 }
261 // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
262 if ( 1 != $idsite ) {
263 // only needed if the actual site is not the default site... makes sure when they click in Matomo
264 // UI on "Dashboard" that the correct site is being opened by default
265 // eg if the linked site is actually idSite=2.
266 Access::doAsSuperUser(
267 function () use ( $matomo_login, &$idsite ) {
268 try {
269 UsersManager\API::unsetInstance();
270 // we need to unset the instance to make sure it fetches the
271 // up to date dependencies eg current plugin manager etc
272
273 UsersManager\API::getInstance()->setUserPreference(
274 $matomo_login,
275 UsersManager\API::PREFERENCE_DEFAULT_REPORT,
276 $idsite
277 );
278 //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
279 } catch ( Exception $e ) {
280 // ignore any error for now
281 }
282 }
283 );
284 }
285 }
286
287 foreach ( $super_users as $matomo_login => $user ) {
288 $user_model->setSuperUserAccess( $matomo_login, true );
289 }
290
291 $logins_with_some_view_access = array_unique( $logins_with_some_view_access );
292 $all_users = $user_model->getUsers( [] );
293 foreach ( $all_users as $all_user ) {
294 if ( ! in_array( $all_user['login'], $logins_with_some_view_access, true )
295 && ! empty( $all_user['login'] ) ) {
296 Access::doAsSuperUser(
297 function () use ( $user_model, $all_user ) {
298 $user_model->deleteUserOnly( $all_user['login'] );
299 $user_model->deleteUserOptions( $all_user['login'] );
300 $user_model->deleteUserAccess( $all_user['login'] );
301 }
302 );
303 }
304 }
305 }
306
307 /**
308 * @param WP_User $wp_user
309 */
310 protected function ensure_user_exists( $wp_user ) {
311 $user_model = new Model();
312 $user_id = $wp_user->ID;
313 $login = $wp_user->user_login;
314
315 $matomo_user_login = User::get_matomo_user_login( $user_id );
316 $user_in_matomo = null;
317
318 if ( $matomo_user_login ) {
319 $user_in_matomo = $user_model->getUser( $matomo_user_login );
320 } else {
321 $user_by_email = $user_model->getUserByEmail( $wp_user->user_email );
322
323 // the user was deleted without matomo being notified. delete user so we can recreate it
324 // below.
325 //
326 // note: it's also possible there are multiple users with the same email address,
327 // but this is currently unsupported in matomo so we don't take that into consideration.
328 if ( $user_by_email ) {
329 $this->logger->log_exception(
330 'user_sync',
331 new \Exception(
332 'Syncing user with email identical to a user already synced in Matomo. ' .
333 'This means there are multiple WP users with the same email, which Matomo ' .
334 'does not support, or something has deleted the WP option mapping WP user ' .
335 'to Matomo user. Assuming this is a new user to sync and deleting existing user ' .
336 'preferences and options.'
337 )
338 );
339
340 $user_model->deleteUser( $user_by_email['login'] );
341 }
342
343 // wp usernames may include whitespace etc
344 $login = preg_replace( '/[^A-Za-zÄäÖöÜüß0-9_.@+-]+/D', '_', $login );
345 $login = substr( $login, 0, self::MAX_USER_NAME_LENGTH );
346
347 if ( ! $user_model->getUser( $login ) ) {
348 // username is available...
349 $matomo_user_login = $login;
350 } else {
351 // this username seems taken... lets create another one
352
353 $index = 0;
354 do {
355 if ( ! $index ) {
356 $matomo_user_login = 'wp_' . $login;
357 } else {
358 $matomo_user_login = 'wp_' . $login . $index;
359 }
360
361 $index ++;
362 } while ( $user_model->getUser( $matomo_user_login ) );
363 }
364 }
365
366 if ( ! $matomo_user_login || empty( $user_in_matomo ) ) {
367 $this->logger->log( 'Matomo is now creating a user forUserId ' . $user_id . ' with matomo login ' . $matomo_user_login );
368
369 $now = Date::now()->getDatetime();
370 $password = new Password();
371 // we generate some random password since log in using matomo won't be happening anyway
372 $password = $password->hash( $login . $now . Common::getRandomString( 200 ) . microtime( true ) . Common::generateUniqId() );
373
374 $user_model->addUser( $matomo_user_login, $password, $wp_user->user_email, $now );
375
376 User::map_matomo_user_login( $user_id, $matomo_user_login );
377 } elseif ( $user_in_matomo['email'] !== $wp_user->user_email ) {
378 $this->logger->log( 'Matomo is now updating the email for wpUserID ' . $user_id . ' matomo login ' . $matomo_user_login );
379 $user_model->updateUserFields( $matomo_user_login, [ 'email' => $wp_user->user_email ] );
380 }
381
382 return $matomo_user_login;
383 }
384 }
385