PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / trunk
Matomo Analytics – Powerful, Privacy-First Insights for WordPress vtrunk
5.13.0 5.12.1 5.12.0 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 / Site / Sync.php
matomo / classes / WpMatomo / Site Last commit date
Sync 4 weeks ago Sync.php 2 days ago
Sync.php
449 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\Site;
11
12 use Exception;
13 use Piwik\Access;
14 use Piwik\Config;
15 use Piwik\Container\StaticContainer;
16 use Piwik\Intl\Data\Provider\CurrencyDataProvider;
17 use Piwik\Plugins\SitesManager;
18 use Piwik\Plugins\SitesManager\Model;
19 use WP_Site;
20 use WpMatomo\Bootstrap;
21 use WpMatomo\Db\Settings as DbSettings;
22 use WpMatomo\Feature;
23 use WpMatomo\Installer;
24 use WpMatomo\Logger;
25 use WpMatomo\Request;
26 use WpMatomo\Settings;
27 use WpMatomo\Site;
28 use WpMatomo\Site\Sync\SyncConfig;
29
30 if ( ! defined( 'ABSPATH' ) ) {
31 exit; // if accessed directly
32 }
33
34 /**
35 * Properties coming from matomo
36 * phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
37 */
38 class Sync extends Feature {
39 const MAX_LENGTH_SITE_NAME = 90;
40
41 const RETURN_TO_SERVICE_ACTIONS = [ 'make_undelete_blog', 'unarchive_blog', 'make_ham_blog' ];
42 const RETURN_TO_SERVICE_PRIORITY = 10;
43
44 /**
45 * @var Logger
46 */
47 private $logger;
48
49 /**
50 * @var Settings
51 */
52 private $settings;
53
54 /**
55 * @var SyncConfig
56 */
57 private $config_sync;
58
59 public function __construct( Settings $settings ) {
60 $this->logger = new Logger();
61 $this->settings = $settings;
62 $this->config_sync = new SyncConfig( $settings );
63 }
64
65 /**
66 * @return bool
67 */
68 private function is_sync_allowed_for_request() {
69 return ! Request::is_frontend();
70 }
71
72 public function register_hooks() {
73 add_action( 'update_option_blogname', [ $this, 'sync_current_site_ignore_error' ] );
74 add_action( 'update_option_home', [ $this, 'sync_current_site_ignore_error' ] );
75 add_action( 'update_option_siteurl', [ $this, 'sync_current_site_ignore_error' ] );
76 add_action( 'update_option_timezone_string', [ $this, 'sync_current_site_ignore_error' ] );
77 add_action( 'matomo_setting_change_track_ecommerce', [ $this, 'sync_current_site_ignore_error' ] );
78 add_action( 'matomo_setting_change_site_currency', [ $this, 'sync_current_site_ignore_error' ] );
79 add_filter( 'wpmu_drop_tables', [ $this, 'on_drop_blog_tables' ], 10, 2 );
80 add_action( 'wp_delete_site', [ $this, 'on_delete_site' ], 10, 1 );
81
82 foreach ( self::RETURN_TO_SERVICE_ACTIONS as $blog_action ) {
83 add_action( $blog_action, [ $this, 'on_blog_returned_to_service' ], self::RETURN_TO_SERVICE_PRIORITY, 1 );
84 }
85 }
86
87 /**
88 * Tests only
89 *
90 * @internal
91 */
92 public function remove_hooks() {
93 remove_action( 'update_option_blogname', [ $this, 'sync_current_site_ignore_error' ] );
94 remove_action( 'update_option_home', [ $this, 'sync_current_site_ignore_error' ] );
95 remove_action( 'update_option_siteurl', [ $this, 'sync_current_site_ignore_error' ] );
96 remove_action( 'update_option_timezone_string', [ $this, 'sync_current_site_ignore_error' ] );
97 remove_action( 'matomo_setting_change_track_ecommerce', [ $this, 'sync_current_site_ignore_error' ] );
98 remove_action( 'matomo_setting_change_site_currency', [ $this, 'sync_current_site_ignore_error' ] );
99 remove_filter( 'wpmu_drop_tables', [ $this, 'on_drop_blog_tables' ], 10 );
100 remove_action( 'wp_delete_site', [ $this, 'on_delete_site' ], 10 );
101
102 foreach ( self::RETURN_TO_SERVICE_ACTIONS as $blog_action ) {
103 remove_action( $blog_action, [ $this, 'on_blog_returned_to_service' ], self::RETURN_TO_SERVICE_PRIORITY );
104 }
105 }
106
107 /**
108 * @param int $blog_id
109 */
110 public function on_blog_returned_to_service( $blog_id ) {
111 if ( ! $this->is_sync_allowed_for_request() ) {
112 return;
113 }
114
115 // only one of the three flags was cleared, and the blog stays out of service while any
116 // of the others is still set
117 if ( Site::is_blog_out_of_service( $blog_id ) ) {
118 return;
119 }
120
121 switch_to_blog( $blog_id );
122
123 $this->sync_current_site_ignore_error();
124
125 restore_current_blog();
126 }
127
128 /**
129 * @param string[] $tables
130 * @param int $blog_id
131 *
132 * @return string[]
133 */
134 public function on_drop_blog_tables( $tables, $blog_id ) {
135 $matomo_tables = ( new DbSettings() )->get_installed_matomo_tables();
136
137 $this->logger->log( sprintf( 'Matomo will drop %s tables of deleted blog %s', count( $matomo_tables ), $blog_id ) );
138
139 return array_merge( (array) $tables, $matomo_tables );
140 }
141
142 /**
143 * remove the matomo site mapping when a site is deleted
144 *
145 * @param WP_Site $old_site
146 */
147 public function on_delete_site( $old_site ) {
148 Site::map_matomo_site_id( $old_site->id, null );
149 }
150
151 public function sync_current_site_ignore_error() {
152 if ( ! $this->is_sync_allowed_for_request() ) {
153 return;
154 }
155
156 if ( ! function_exists( 'is_plugin_active' ) ) {
157 // these hooks are not admin only, so this may run somewhere wp-admin/includes is not loaded
158 require_once ABSPATH . 'wp-admin/includes/plugin.php';
159 }
160
161 if ( ! is_plugin_active( 'matomo/matomo.php' ) ) {
162 // @see https://github.com/matomo-org/matomo-for-wordpress/issues/577
163 return;
164 }
165 try {
166 $this->sync_current_site();
167 } catch ( Exception $e ) {
168 $this->logger->log( 'Ignoring site sync error: ' . $e->getMessage() );
169 $this->logger->log_exception( 'sync_site_ignore', $e );
170 }
171 }
172
173 public function sync_all() {
174 $succeed_all = true;
175
176 Bootstrap::do_bootstrap();
177
178 if ( is_multisite() && function_exists( 'get_sites' ) ) {
179 // number => 0 means no limit. WP_Site_Query defaults to 100, which would silently leave
180 // every blog after that without a Matomo site
181 foreach ( get_sites( [ 'number' => 0 ] ) as $site ) {
182 if ( Site::is_blog_out_of_service( $site ) ) {
183 continue;
184 }
185
186 /** @var WP_Site $site */
187 switch_to_blog( $site->blog_id );
188
189 $success = false;
190
191 try {
192 $installer = new Installer( $this->settings );
193 if ( ! $installer->looks_like_it_is_installed() ) {
194 $this->logger->log( sprintf( 'Matomo was not installed yet for blog: %s installing now.', $site->blog_id ) );
195
196 // prevents error that it wouldn't fully install matomo for a different site as it would think it already did install it etc.
197 // and would otherwise think plugins are already activated etc
198 Bootstrap::set_not_bootstrapped();
199 $config = Config::getInstance();
200 $installed = $config->PluginsInstalled;
201 $installed['PluginsInstalled'] = [];
202 $config->PluginsInstalled = $installed;
203
204 if ( $installer->can_be_installed() ) {
205 $installer->install();
206 } else {
207 $this->logger->log( sprintf( 'Matomo cannot be installed for blog: %s, skipping it.', $site->blog_id ) );
208 continue;
209 }
210 }
211
212 $success = $this->sync_site( $site->blog_id, $site->blogname, $site->siteurl );
213 } catch ( Exception $e ) {
214 $success = false;
215 // we don't want to rethrow exception otherwise some other blogs might never sync
216 $this->logger->log( 'Matomo error syncing site: ' . $e->getMessage() );
217 } finally {
218 restore_current_blog();
219
220 $succeed_all = $succeed_all && $success;
221 }
222 }
223 } else {
224 $success = $this->sync_current_site();
225 $succeed_all = $succeed_all && $success;
226 }
227
228 return $succeed_all;
229 }
230
231 public function sync_current_site() {
232 return $this->sync_site( get_current_blog_id(), get_bloginfo( 'name' ), get_bloginfo( 'url' ) );
233 }
234
235 public function sync_site( $blog_id, $blog_name, $blog_url ) {
236 Bootstrap::do_bootstrap();
237 $this->logger->log( 'Matomo is now syncing blogId ' . $blog_id );
238
239 $idsite = Site::get_matomo_site_id( $blog_id );
240
241 $sites_manager_model = new Model();
242
243 if ( ! is_multisite() ) {
244 // we should have here only one record in the matomo_site table then...
245 $matomo_id_sites = $sites_manager_model->getSitesId();
246 if ( count( $matomo_id_sites ) === 1 ) {
247 $matomo_id_site = (int) $matomo_id_sites[0];
248 if ( empty( $idsite ) ) {
249 // we have one record in the matomo_site table but the mapping does not exist. Force usage of the ID found.
250 $idsite = $matomo_id_site;
251 $this->logger->log( "Can't find the id site in the mapping, but there is already an existing site. Use its ID " . $idsite . ' for blog' );
252 wp_cache_flush();
253 add_site_option( Site::SITE_MAPPING_PREFIX . $blog_id, $idsite );
254 } elseif ( (int) $idsite !== $matomo_id_site ) {
255 // the mapped id in the WP config is different from the id in the matomo_site table: we force usage of the matomo table site ID
256 $idsite = $matomo_id_site;
257 $this->logger->log( 'The id site in the mapping is different from the id site in the matomo table. Force usage of Matomo ID ' . $idsite . ' for blog' );
258 Site::map_matomo_site_id( $blog_id, $idsite );
259 }
260 } elseif ( count( $matomo_id_sites ) > 1 ) {
261 // there are more than one record: we'll have to identify which one has data and which one must be removed from the matomo_site table
262 $this->logger->log( 'There is a problem in your configuration. Please contact support at wordpress@matomo.org' );
263 return false;
264 }
265 }
266
267 if ( empty( $blog_name ) ) {
268 $blog_name = esc_html__( 'Default', 'matomo' );
269 } else {
270 $blog_name = substr( $blog_name, 0, self::MAX_LENGTH_SITE_NAME );
271 }
272
273 $track_ecommerce = (int) $this->settings->get_global_option( 'track_ecommerce' );
274 $site_currency = $this->settings->get_global_option( Settings::SITE_CURRENCY );
275 $detected_timezone = $this->detect_timezone();
276
277 $data_provider = StaticContainer::get( CurrencyDataProvider::class );
278 $valid_currencies = $data_provider->getCurrencyList();
279 if ( ! array_key_exists( $site_currency, $valid_currencies ) ) {
280 $site_currency = 'USD';
281 }
282
283 if ( ! empty( $idsite ) ) {
284 $this->logger->log( 'Matomo site is known for blog (' . $idsite . ')... will update' );
285
286 $site = $sites_manager_model->getSiteFromId( $idsite );
287 if ( ! empty( $site ) ) {
288 // if site has changed then we have to update it
289 if ( $site['name'] !== $blog_name
290 || $site['main_url'] !== $blog_url
291 // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
292 || $site['ecommerce'] != $track_ecommerce
293 || $site['currency'] !== $site_currency
294 || $site['timezone'] !== $detected_timezone ) {
295
296 /** @var WP_Site $site */
297 $params = [
298 'name' => $blog_name,
299 'main_url' => $blog_url,
300 'ecommerce' => $track_ecommerce,
301 'currency' => $site_currency,
302 'timezone' => $detected_timezone,
303 ];
304 $sites_manager_model->updateSite( $params, $idsite );
305
306 // the site details the tracking code was built from changed so invalidate
307 $this->invalidate_current_blogs_tracking_code();
308
309 do_action( 'matomo_site_synced', $idsite, $blog_id );
310 }
311
312 $this->config_sync->sync_config_for_current_site();
313
314 return true;
315 }
316 }
317
318 $this->logger->log( 'Matomo site is not known for blog... will create site' );
319
320 /** @var WP_Site $site */
321 $idsite = null;
322
323 $this->set_enable_sites_admin( 1 );
324
325 Access::doAsSuperUser(
326 function () use ( $blog_name, $blog_url, $detected_timezone, $track_ecommerce, &$idsite, $site_currency ) {
327 SitesManager\API::unsetInstance();
328 // we need to unset the instance to make sure it fetches the
329 // up to date dependencies eg current plugin manager etc
330
331 $idsite = SitesManager\API::getInstance()->addSite(
332 $blog_name,
333 [ $blog_url ],
334 $track_ecommerce,
335 $site_search = null,
336 $search_keyword_parameters = null,
337 $search_category_parameters = null,
338 $excluded_ips = null,
339 $excluded_query_parameters = null,
340 $detected_timezone,
341 $site_currency
342 );
343 }
344 );
345 $this->set_enable_sites_admin( 0 );
346
347 $this->logger->log( 'Matomo created site with ID ' . $idsite . ' for blog' );
348
349 if ( ! is_numeric( $idsite ) || 0 === $idsite || '0' === $idsite ) {
350 $this->logger->log( sprintf( 'Creating the website failed: %s', wp_json_encode( $blog_id ) ) );
351
352 return false;
353 }
354
355 Site::map_matomo_site_id( $blog_id, $idsite );
356
357 $this->config_sync->sync_config_for_current_site();
358
359 // invalidate the tracking code in case this site existed before but was just out of service
360 $this->invalidate_current_blogs_tracking_code();
361
362 do_action( 'matomo_site_synced', $idsite, $blog_id );
363
364 return true;
365 }
366
367 private function invalidate_current_blogs_tracking_code() {
368 // only invalidate if the tracking mode autogenerates the tracking code, and if
369 // the tracking code has been generated once. otherwise there is no reason to invalidate.
370 if ( ! $this->settings->is_tracking_code_autogenerated()
371 || ! $this->settings->get_option( Settings::OPTION_LAST_TRACKING_CODE_UPDATE )
372 ) {
373 return;
374 }
375
376 $this->settings->set_option( Settings::OPTION_LAST_TRACKING_CODE_UPDATE, 0 );
377 $this->settings->save();
378 }
379
380 private function set_enable_sites_admin( $enabled ) {
381 $general = Config::getInstance()->General;
382 $general['enable_sites_admin'] = (int) $enabled;
383 Config::getInstance()->General = $general;
384 }
385
386 private function detect_timezone() {
387 $timezone = get_option( 'timezone_string' );
388
389 if ( $timezone && $this->check_and_try_to_set_default_timezone( $timezone ) ) {
390 return $timezone;
391 }
392
393 // older WordPress
394 $utc_offset = (int) get_option( 'gmt_offset', 0 );
395
396 if ( 0 === $utc_offset ) {
397 return 'UTC';
398 }
399
400 $utc_offset_in_seconds = $utc_offset * 3600;
401 $timezone = timezone_name_from_abbr( '', $utc_offset_in_seconds );
402
403 if ( $timezone && $this->check_and_try_to_set_default_timezone( $timezone ) ) {
404 return $timezone;
405 }
406
407 $dst = (bool) gmdate( 'I' );
408 foreach ( timezone_abbreviations_list() as $abbr ) {
409 foreach ( $abbr as $city ) {
410 if ( $dst === (bool) $city['dst']
411 && $city['timezone_id']
412 && (int) $city['offset'] === $utc_offset_in_seconds ) {
413 return $city['timezone_id'];
414 }
415 }
416 }
417
418 if ( is_numeric( $utc_offset ) ) {
419 if ( $utc_offset > 0 ) {
420 $timezone = 'UTC+' . $utc_offset;
421 } else {
422 $timezone = 'UTC' . $utc_offset;
423 }
424
425 if ( $this->check_and_try_to_set_default_timezone( $timezone ) ) {
426 return $timezone;
427 }
428 }
429
430 return 'UTC';
431 }
432
433 private function check_and_try_to_set_default_timezone( $timezone ) {
434 try {
435 Access::doAsSuperUser(
436 function () use ( $timezone ) {
437 // make sure we're loading the latest instance with all up to date dependencies... mainly needed for tests
438 SitesManager\API::unsetInstance();
439 SitesManager\API::getInstance()->setDefaultTimezone( $timezone );
440 }
441 );
442 } catch ( Exception $e ) {
443 return false;
444 }
445
446 return true;
447 }
448 }
449