PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.14.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.14.2
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 3 years ago
Sync.php
352 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( 'profile_update', [ $this, 'sync_maybe_background' ], $prio = 10, $args = 0 );
60 }
61
62 public function sync_maybe_background() {
63 global $pagenow;
64 if ( is_admin() && 'users.php' === $pagenow ) {
65 // eg for profile update we don't want to sync directly see #365 as it could cause issues with other plugins
66 // if they eg alter `get_users` option
67 wp_schedule_single_event( time() + 5, ScheduledTasks::EVENT_SYNC );
68 } else {
69 $this->sync_current_users_1000();
70 }
71 }
72
73 public function sync_all() {
74 if ( function_exists( 'is_multisite' ) && is_multisite() ) {
75 foreach ( get_sites() as $site ) {
76 switch_to_blog( $site->blog_id );
77
78 $idsite = Site::get_matomo_site_id( $site->blog_id );
79
80 try {
81 if ( $idsite ) {
82 $users = $this->get_users( [ 'blog_id' => $site->blog_id ] );
83 $this->sync_users( $users, $idsite );
84 }
85 } catch ( Exception $e ) {
86 // we don't want to rethrow exception otherwise some other blogs might never sync
87 $this->logger->log_exception( 'user_sync ', $e );
88 }
89
90 restore_current_blog();
91 }
92 } else {
93 $this->sync_current_users();
94 }
95 }
96
97 private function get_users( $options = [] ) {
98 /** @var WP_User[] $users */
99 $users = get_users( $options );
100
101 $current_user = wp_get_current_user();
102 if ( ! empty( $current_user ) && ! empty( $current_user->user_login ) ) {
103 // refs https://github.com/matomo-org/wp-matomo/issues/365
104 // some other plugins may under circumstances overwrite the get_users query and not return all users
105 // as a result we would delete some users in the matomo users table. this way we make sure at least the current
106 // user will be added and not deleted even if the list of users is not complete
107 $found = false;
108 foreach ( $users as $user ) {
109 if ( $user->user_login === $current_user->user_login ) {
110 $found = true;
111 break;
112 }
113 }
114 if ( ! $found ) {
115 $users[] = $current_user;
116 }
117 }
118
119 if ( is_multisite() ) {
120 $super_admins = get_super_admins();
121 if ( ! empty( $super_admins ) ) {
122 foreach ( $super_admins as $super_admin ) {
123 $found = false;
124 foreach ( $users as $user ) {
125 if ( $user->user_login === $super_admin ) {
126 $found = true;
127 break;
128 }
129 }
130 if ( ! $found ) {
131 $user = get_user_by( 'login', $super_admin );
132 if ( ! empty( $user ) ) {
133 $users[] = $user;
134 }
135 }
136 }
137 }
138 }
139
140 return $users;
141 }
142
143 public function sync_current_users() {
144 $idsite = Site::get_matomo_site_id( get_current_blog_id() );
145 if ( $idsite ) {
146 $users = $this->get_users();
147 $this->sync_users( $users, $idsite );
148 }
149 }
150
151 /**
152 * similar method to sync_current_users which synchronise on the fly only if we have less than 1000 users.
153 * Otherwise it will be done by a background task
154 *
155 * @return void
156 * @see https://github.com/matomo-org/matomo-for-wordpress/issues/460
157 * @see Sync::sync_current_users()
158 */
159 public function sync_current_users_1000() {
160 if ( ! is_plugin_active( 'matomo/matomo.php' ) ) {
161 // @see https://github.com/matomo-org/matomo-for-wordpress/issues/577
162 return;
163 }
164 $idsite = Site::get_matomo_site_id( get_current_blog_id() );
165 if ( $idsite ) {
166 $num_users = count_users();
167 $num_users = $num_users['total_users'];
168 if ( $num_users < 1000 ) {
169 $users = $this->get_users();
170 $this->sync_users( $users, $idsite );
171 }
172 }
173 }
174
175 /**
176 * Sync all users. Make sure to always pass all sites that exist within a given site... you cannot just sync an individual
177 * user... we would delete all other users
178 *
179 * @param WP_User[] $users
180 * @param $idsite
181 */
182 protected function sync_users( $users, $idsite ) {
183 Bootstrap::do_bootstrap();
184
185 $this->logger->log( 'Matomo will now sync ' . count( $users ) . ' users' );
186
187 $super_users = [];
188 $logins_with_some_view_access = [ 'anonmyous' ]; // may or may not exist... we don't want to delete this user though
189 $user_model = new Model();
190
191 // need to make sure we recreate new instance later with latest dependencies in case they changed
192 API::unsetInstance();
193
194 foreach ( $users as $user ) {
195 $user_id = $user->ID;
196
197 // todo if we used transactions we could commit it after a possibly new access has been added
198 // to prevent UI preventing randomly saying no access between deleting and adding access
199
200 $mapped_matomo_login = User::get_matomo_user_login( $user_id );
201
202 $matomo_login = null;
203
204 if ( user_can( $user, Capabilities::KEY_SUPERUSER ) ) {
205 $matomo_login = $this->ensure_user_exists( $user );
206 $super_users[ $matomo_login ] = $user;
207 $logins_with_some_view_access[] = $matomo_login;
208 } elseif ( user_can( $user, Capabilities::KEY_ADMIN ) ) {
209 $matomo_login = $this->ensure_user_exists( $user );
210 $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] );
211 $user_model->addUserAccess( $matomo_login, Admin::ID, [ $idsite ] );
212 $user_model->setSuperUserAccess( $matomo_login, false );
213 $logins_with_some_view_access[] = $matomo_login;
214 } elseif ( user_can( $user, Capabilities::KEY_WRITE ) ) {
215 $matomo_login = $this->ensure_user_exists( $user );
216 $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] );
217 $user_model->addUserAccess( $matomo_login, Write::ID, [ $idsite ] );
218 $user_model->setSuperUserAccess( $matomo_login, false );
219 $logins_with_some_view_access[] = $matomo_login;
220 } elseif ( user_can( $user, Capabilities::KEY_VIEW ) ) {
221 $matomo_login = $this->ensure_user_exists( $user );
222 $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] );
223 $user_model->addUserAccess( $matomo_login, View::ID, [ $idsite ] );
224 $user_model->setSuperUserAccess( $matomo_login, false );
225 $logins_with_some_view_access[] = $matomo_login;
226 } elseif ( $mapped_matomo_login ) {
227 $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] );
228 }
229
230 if ( $matomo_login ) {
231 $locale = get_user_locale( $user->ID );
232 $locale_dash = Common::mb_strtolower( str_replace( '_', '-', $locale ) );
233 $parts = [];
234 if ( $locale && in_array( $locale_dash, [ 'zh-cn', 'zh-tw', 'pt-br', 'es-ar' ], true ) ) {
235 $parts = [ $locale_dash ];
236 } elseif ( ! empty( $locale ) && is_string( $locale ) ) {
237 $parts = explode( '_', $locale );
238 }
239
240 if ( ! empty( $parts[0] ) ) {
241 $lang = $parts[0];
242 if ( Plugin\Manager::getInstance()->isPluginActivated( 'LanguagesManager' )
243 && Plugin\Manager::getInstance()->isPluginInstalled( 'LanguagesManager' )
244 && API::getInstance()->isLanguageAvailable( $lang ) ) {
245 $user_lang_model = new \Piwik\Plugins\LanguagesManager\Model();
246 $user_lang_model->setLanguageForUser( $matomo_login, $lang );
247 }
248 }
249 }
250 // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
251 if ( 1 != $idsite ) {
252 // only needed if the actual site is not the default site... makes sure when they click in Matomo
253 // UI on "Dashboard" that the correct site is being opened by default
254 // eg if the linked site is actually idSite=2.
255 Access::doAsSuperUser(
256 function () use ( $matomo_login, &$idsite ) {
257 try {
258 UsersManager\API::unsetInstance();
259 // we need to unset the instance to make sure it fetches the
260 // up to date dependencies eg current plugin manager etc
261
262 UsersManager\API::getInstance()->setUserPreference(
263 $matomo_login,
264 UsersManager\API::PREFERENCE_DEFAULT_REPORT,
265 $idsite
266 );
267 //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
268 } catch ( Exception $e ) {
269 // ignore any error for now
270 }
271 }
272 );
273 }
274 }
275
276 foreach ( $super_users as $matomo_login => $user ) {
277 $user_model->setSuperUserAccess( $matomo_login, true );
278 }
279
280 $logins_with_some_view_access = array_unique( $logins_with_some_view_access );
281 $all_users = $user_model->getUsers( [] );
282 foreach ( $all_users as $all_user ) {
283 if ( ! in_array( $all_user['login'], $logins_with_some_view_access, true )
284 && ! empty( $all_user['login'] ) ) {
285 Access::doAsSuperUser(
286 function () use ( $user_model, $all_user ) {
287 $user_model->deleteUserOnly( $all_user['login'] );
288 $user_model->deleteUserOptions( $all_user['login'] );
289 $user_model->deleteUserAccess( $all_user['login'] );
290 }
291 );
292 }
293 }
294 }
295
296 /**
297 * @param WP_User $wp_user
298 */
299 protected function ensure_user_exists( $wp_user ) {
300 $user_model = new Model();
301 $user_id = $wp_user->ID;
302 $login = $wp_user->user_login;
303
304 $matomo_user_login = User::get_matomo_user_login( $user_id );
305 $user_in_matomo = null;
306
307 if ( $matomo_user_login ) {
308 $user_in_matomo = $user_model->getUser( $matomo_user_login );
309 } else {
310 // wp usernames may include whitespace etc
311 $login = preg_replace( '/[^A-Za-zÄäÖöÜüß0-9_.@+-]+/D', '_', $login );
312 $login = substr( $login, 0, self::MAX_USER_NAME_LENGTH );
313
314 if ( ! $user_model->getUser( $login ) ) {
315 // username is available...
316 $matomo_user_login = $login;
317 } else {
318 // this username seems taken... lets create another one
319
320 $index = 0;
321 do {
322 if ( ! $index ) {
323 $matomo_user_login = 'wp_' . $login;
324 } else {
325 $matomo_user_login = 'wp_' . $login . $index;
326 }
327
328 $index ++;
329 } while ( $user_model->getUser( $matomo_user_login ) );
330 }
331 }
332
333 if ( ! $matomo_user_login || empty( $user_in_matomo ) ) {
334 $this->logger->log( 'Matomo is now creating a user forUserId ' . $user_id . ' with matomo login ' . $matomo_user_login );
335
336 $now = Date::now()->getDatetime();
337 $password = new Password();
338 // we generate some random password since log in using matomo won't be happening anyway
339 $password = $password->hash( $login . $now . Common::getRandomString( 200 ) . microtime( true ) . Common::generateUniqId() );
340
341 $user_model->addUser( $matomo_user_login, $password, $wp_user->user_email, $now );
342
343 User::map_matomo_user_login( $user_id, $matomo_user_login );
344 } elseif ( $user_in_matomo['email'] !== $wp_user->user_email ) {
345 $this->logger->log( 'Matomo is now updating the email for wpUserID ' . $user_id . ' matomo login ' . $matomo_user_login );
346 $user_model->updateUserFields( $matomo_user_login, [ 'email' => $wp_user->user_email ] );
347 }
348
349 return $matomo_user_login;
350 }
351 }
352