PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.10.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.10.1
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 / Installer.php
matomo / classes / WpMatomo Last commit date
Admin 1 month ago Commands 2 years ago Db 1 year ago Ecommerce 1 month ago Report 2 months ago Site 2 months ago TrackingCode 4 months ago Updater 4 years ago User 2 months ago Workarounds 2 years ago WpStatistics 5 months ago views 1 month ago AIBotTracking.php 4 months ago API.php 2 months ago Access.php 4 years ago AjaxTracker.php 4 months ago Annotations.php 2 months ago Bootstrap.php 10 months ago Capabilities.php 2 months ago Compatibility.php 2 months ago Email.php 2 years ago ErrorNotice.php 1 month ago Feature.php 2 months ago Installer.php 1 month ago Logger.php 1 year ago OptOut.php 1 month ago Paths.php 5 months ago PluginActionLinks.php 2 months ago PluginAdminOverrides.php 2 months ago PluginInit.php 2 months ago PrivacyBadge.php 4 years ago RedirectOnActivation.php 2 months ago Referral.php 2 months ago Roles.php 2 months ago ScheduledTasks.php 2 months ago Settings.php 4 months ago Site.php 3 years ago TrackingCode.php 2 months ago Uninstaller.php 5 months ago Updater.php 10 months ago User.php 4 years ago
Installer.php
568 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;
11
12 use Exception;
13 use Piwik\Cache;
14 use Piwik\Common;
15 use Piwik\Config;
16 use Piwik\Container\ContainerDoesNotExistException;
17 use Piwik\Container\StaticContainer;
18 use Piwik\DbHelper;
19 use Piwik\Exception\NotYetInstalledException;
20 use Piwik\Plugin\API as PluginApi;
21 use Piwik\Plugin\Manager;
22 use Piwik\Plugins\SitesManager\Model;
23 use Piwik\SettingsPiwik;
24 use Piwik\Singleton;
25 use WpMatomo\Site\Sync;
26
27 if ( ! defined( 'ABSPATH' ) ) {
28 exit; // if accessed directly
29 }
30
31 class Installer {
32 const OPTION_NAME_INSTALL_DATE = 'matomo-install-date';
33 const OPTION_NAME_INSTALL_VERSION = 'matomo-install-version';
34
35 const DEFAULT_DB_CHARSET = 'utf8';
36 const DEFAULT_DB_COLLATE = '';
37
38 /**
39 * @var Settings
40 */
41 private $settings;
42
43 /**
44 * @var Logger
45 */
46 private $logger;
47
48 public function __construct( Settings $settings ) {
49 $this->settings = $settings;
50 $this->logger = new Logger();
51 }
52
53 public static function is_file_not_exists_failure( \Exception $ex ) {
54 return preg_match( '/no such file or directory/i', $ex->getMessage() );
55 }
56
57 public function register_hooks() {
58 add_action( 'activate_matomo/matomo.php', [ $this, 'install' ] ); // if activate_plugin is invoked with the path to the plugin entrypoint
59 add_action( 'activate_matomo', [ $this, 'install' ] ); // if activate_plugin is invoked with the plugin slug
60 }
61
62 public function looks_like_it_is_installed() {
63 $paths = new Paths();
64 $config_file = $paths->get_config_ini_path();
65
66 $config_dir = dirname( $config_file );
67 if ( ! is_dir( $config_dir ) ) {
68 wp_mkdir_p( $config_dir );
69 }
70
71 if ( ! file_exists( $config_file ) ) {
72 return false;
73 }
74
75 if ( ! $this->is_current_instance_installed() ) {
76 return false;
77 }
78
79 return true;
80 }
81
82 public static function is_intalled() {
83 try {
84 Bootstrap::do_bootstrap();
85
86 return SettingsPiwik::isMatomoInstalled();
87 } catch ( NotYetInstalledException $e ) {
88 // not yet installed.... we will need to install it
89 return false;
90 } catch ( \Zend_Db_Statement_Exception $e ) {
91 // not yet installed.... we will need to install it
92 return false;
93 }
94 }
95
96 public function can_be_installed() {
97 $paths = new Paths();
98 $upload_dir = $paths->get_upload_base_dir();
99
100 return is_writable( $upload_dir ) || is_writable( dirname( $upload_dir ) );
101 }
102
103 public function install() {
104 if ( ! $this->can_be_installed() ) {
105 return false;
106 }
107
108 try {
109 // prevent session related errors during install making it more stable
110 if ( ! defined( 'PIWIK_ENABLE_SESSION_START' ) ) {
111 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
112 define( 'PIWIK_ENABLE_SESSION_START', false );
113 }
114
115 Bootstrap::bootstrap_environment();
116
117 if ( ! SettingsPiwik::isMatomoInstalled() || ! $this->looks_like_it_is_installed() ) {
118 throw new NotYetInstalledException( 'Not yet installed' );
119 }
120
121 return false;
122 } catch ( NotYetInstalledException $e ) {
123 $this->logger->log( 'Matomo is not yet installed... installing now' );
124
125 if ( $this->is_install_in_progress() ) {
126 return false;
127 }
128
129 $this->mark_install_started();
130
131 $db_info = $this->create_db();
132 $this->create_config( $db_info );
133
134 $this->install_plugins_one_at_a_time();
135
136 $this->update_components();
137
138 update_option( self::OPTION_NAME_INSTALL_DATE, time() );
139 $plugin_data = get_plugin_data( MATOMO_ANALYTICS_FILE, $markup = false, $translate = false );
140 if ( ! empty( $plugin_data['Version'] ) ) {
141 update_option( self::OPTION_NAME_INSTALL_VERSION, $plugin_data['Version'] );
142 }
143
144 $this->create_website();
145 $this->create_user(); // we sync users as early as possible to make sure things are set up correctly
146 $this->install_tracker();
147
148 if ( ! $this->is_environment_set_up() ) {
149 try {
150 $this->logger->log( 'Matomo will now init the environment' );
151 $environment = new \Piwik\Application\Environment( null, Bootstrap::get_extra_di_definitions() );
152 $environment->init();
153 } catch ( Exception $e ) {
154 $this->logger->log( 'Ignoring error environment init' );
155 $this->logger->log_exception( 'install_env_init', $e );
156 }
157 }
158
159 try {
160 // should load and install plugins
161 $this->logger->log( 'Matomo will now init the front controller and install plugins etc' );
162 \Piwik\FrontController::unsetInstance(); // make sure we're loading the latest instance
163 $controller = \Piwik\FrontController::getInstance();
164 $controller->init();
165 } catch ( Exception $e ) {
166 $this->logger->log( 'Ignoring error frontcontroller init' );
167 $this->logger->log_exception( 'install_front_init', $e );
168 }
169
170 try {
171 // sync user now again after installing plugins...
172 // before eg the users_language table would not have been available yet
173 $this->create_user();
174 } catch ( Exception $e ) {
175 $this->logger->log_exception( 'install_create_user', $e );
176 }
177
178 try {
179 // update plugins if there are any
180 $this->update_components();
181 } catch ( Exception $e ) {
182 $this->logger->log_exception( 'install_update_comp', $e );
183 }
184
185 $this->logger->log( 'Recording version and url' );
186
187 DbHelper::recordInstallVersion();
188
189 $this->set_matomo_url();
190
191 $this->logger->log( 'Emptying some caches' );
192
193 Singleton::clearAll();
194 PluginApi::unsetAllInstances();
195 try {
196 Cache::flushAll();
197 } catch ( \Exception $ex ) {
198 if ( ! self::is_file_not_exists_failure( $ex ) ) { // ignore errors that involve a directory not existing
199 throw $ex;
200 }
201 }
202
203 $this->logger->log( 'Matomo install finished' );
204
205 $this->mark_matomo_installed();
206
207 // we're scheduling another update in case there are some dimensions to be updated or anything
208 // it is possible that because the plugins need to be reloaded etc that those updates are not executed right
209 // away but need an actual reload and cache clearance etc
210 wp_schedule_single_event( time() + 30, ScheduledTasks::EVENT_UPDATE );
211
212 // to set up geoip in the background later... don't want this to influence the install
213 $sync_config = new Sync\SyncConfig( $this->settings );
214 $tasks = new ScheduledTasks( $this->settings, $sync_config );
215 $last_geoip_update_run_time = $tasks->get_last_time_before_cron( ScheduledTasks::EVENT_GEOIP );
216 if ( empty( $last_geoip_update_run_time ) ) {
217 wp_schedule_single_event( time() + 35, ScheduledTasks::EVENT_GEOIP );
218 }
219
220 // in case something fails with website or user creation
221 // also to set up all the other users
222 wp_schedule_single_event( time() + 45, ScheduledTasks::EVENT_SYNC );
223 }
224
225 return true;
226 }
227
228 public function set_matomo_url() {
229 // note that the full url might not be possible to be set if the cron is executed on cli and it maybe doesn't have
230 // the host or if a plugin overwrites the constant of WP_PLUGIN_URL which is used in plugins_url() to not include domain
231 // see https://www.google.com/url?q=https://wordpress.org/support/topic/no-metrics-showing/%23topic-14362043-replies&source=gmail&ust=1620409922890000&usg=AFQjCNHyzG5-9v0A8bjg8aLVVbYSWxkTxg
232
233 $matomo_url = SettingsPiwik::getPiwikUrl();
234 $plugins_url = plugins_url( 'app', MATOMO_ANALYTICS_FILE );
235 $plugins_url = rtrim( $plugins_url, '/' ) . '/';
236 // need to make sure to update plugins url if it changes eg if installed somewhere else or domain changes
237
238 if ( $matomo_url
239 && $plugins_url === $matomo_url
240 && wp_parse_url( $matomo_url, PHP_URL_SCHEME )
241 && wp_parse_url( $matomo_url, PHP_URL_HOST )
242 ) {
243 // if currently no scheme or host is set then we'll make sure to overwrite it
244 return;
245 }
246
247 if ( ! $plugins_url ) {
248 return;
249 }
250
251 $has_host = wp_parse_url( $plugins_url, PHP_URL_HOST );
252
253 if ( ! $has_host ) {
254 return;
255 }
256
257 $has_scheme = wp_parse_url( $plugins_url, PHP_URL_SCHEME );
258
259 if ( ! $has_scheme ) {
260 return;
261 }
262
263 SettingsPiwik::overwritePiwikUrl( $plugins_url );
264 }
265
266 private function install_tracker() {
267 $this->logger->log( 'Matomo is now installing the tracker' );
268 // making sure the tracker will be created in the wp uploads directory
269 $updater = StaticContainer::get( 'Piwik\Plugins\CustomJsTracker\TrackerUpdater' );
270 $updater->update();
271 }
272
273 private function create_db() {
274 $this->logger->log( 'Matomo will now create the database' );
275
276 try {
277 $db_infos = self::get_db_infos();
278 $config = Config::getInstance();
279 if ( isset( $config ) ) {
280 $db_infos = array_merge( $config->database, $db_infos );
281 }
282 $config->database = $db_infos;
283
284 DbHelper::checkDatabaseVersion();
285 } catch ( Exception $e ) {
286 $message = sprintf( 'Database info detection failed with %s in %s:%s.', $e->getMessage(), $e->getFile(), $e->getLine() );
287 throw new Exception( $message, $e->getCode(), $e );
288 }
289
290 $tables_installed = DbHelper::getTablesInstalled();
291 if ( count( $tables_installed ) > 0 ) {
292 // todo define behaviour... might need to ask user how to proceed... but ideally we add check to
293 // see if all tables are there and if so, reuse them...
294 return $db_infos;
295 }
296 DbHelper::createTables();
297 DbHelper::createAnonymousUser();
298
299 return $db_infos;
300 }
301
302 private function create_config( $db_info ) {
303 $this->logger->log( 'Matomo is now creating the config' );
304 $home_url = home_url();
305 $domain = wp_parse_url( $home_url, PHP_URL_HOST );
306 if ( $domain ) {
307 $port = wp_parse_url( $home_url, PHP_URL_PORT );
308 if ( $port ) {
309 $domain .= ':' . $port;
310 }
311 } else {
312 $domain = $home_url;
313 }
314 $general = [
315 'trusted_hosts' => [ $domain ],
316 'salt' => Common::generateUniqId(),
317 ];
318 $config = Config::getInstance();
319 $path = $config->getLocalPath();
320 if ( ! is_dir( dirname( $path ) ) ) {
321 wp_mkdir_p( dirname( $path ) );
322 }
323 $db_default = [];
324 $general_default = [];
325 if ( $config->database ) {
326 $db_default = $config->database;
327 }
328 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
329 if ( $config->General ) {
330 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
331 $general_default = $config->General;
332 }
333 $config->database = array_merge( $db_default, $db_info );
334 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
335 $config->General = array_merge( $general_default, $general );
336 $config->forceSave();
337
338 $mode = 0664;
339 if ( ! chmod( $config->getLocalPath(), $mode ) ) {
340 $this->logger->log( "Can't chmod " . $config->getLocalPath() );
341 }
342 }
343
344 private function create_website() {
345 $sync = new Sync( $this->settings );
346
347 return $sync->sync_current_site();
348 }
349
350 private function create_user() {
351 $sync = new User\Sync();
352
353 $sync->sync_current_users();
354 }
355
356 /**
357 * @param array $default params
358 *
359 * @return array
360 */
361 public static function get_db_infos( $default = [] ) {
362 global $wpdb;
363
364 $socket = '';
365 $host_data = null;
366 $host = null;
367 $port = 3306;
368 if ( method_exists( $wpdb, 'parse_db_host' ) ) {
369 // WP 4.9+
370 $host_data = $wpdb->parse_db_host( DB_HOST );
371 if ( $host_data ) {
372 list( $host, $port, $socket, $is_ipv6 ) = $host_data;
373 if ( ! $port && ! $socket ) {
374 $port = 3306;
375 }
376 }
377 }
378
379 if ( ! $host_data || ! $host ) {
380 // WP 4.8 and older
381 // in case DB credentials change in WordPress, we need to apply these changes here as well on demand
382 $host_parts = explode( ':', DB_HOST );
383 $host = $host_parts[0];
384 if ( count( $host_parts ) === 2 && is_numeric( $host_parts[1] ) ) {
385 $port = $host_parts[1];
386 } else {
387 $port = 3306;
388 }
389 }
390
391 $charset = $wpdb->charset ? $wpdb->charset : self::DEFAULT_DB_CHARSET;
392 if ( defined( 'MATOMO_DB_CHARSET' ) && MATOMO_DB_CHARSET ) {
393 $charset = MATOMO_DB_CHARSET;
394 }
395
396 $collation = $wpdb->collate ? $wpdb->collate : self::DEFAULT_DB_COLLATE;
397 if ( defined( 'MATOMO_DB_COLLATE' ) && MATOMO_DB_COLLATE ) {
398 $collation = MATOMO_DB_COLLATE;
399 }
400
401 $database = [
402 'host' => $host,
403 'port' => $port,
404 'username' => DB_USER,
405 'password' => DB_PASSWORD,
406 'dbname' => DB_NAME,
407 'charset' => $charset,
408 'collation' => $collation,
409 'tables_prefix' => $wpdb->prefix . MATOMO_DATABASE_PREFIX,
410 'adapter' => 'WordPress',
411 ];
412 if ( ! empty( $socket ) ) {
413 $database['unix_socket'] = $socket;
414 }
415 $database = array_merge( $default, $database );
416
417 return $database;
418 }
419
420 private function update_components() {
421 $this->logger->log( 'Matomo will now trigger an update' );
422 Updater::unlock(); // make sure the update can be executed
423 $updater = new Updater( $this->settings );
424 $updater->update();
425 }
426
427 /**
428 * public for tests
429 *
430 * @return bool
431 */
432 public function is_current_instance_installed() {
433 $installed_components = $this->settings->get_option( Settings::INSTANCE_COMPONENTS_INSTALLED );
434 if ( empty( $installed_components ) ) {
435 $installed_components = '[]';
436 }
437 $installed_components = json_decode( $installed_components, true );
438
439 if ( empty( $installed_components['core'] ) ) {
440 return false;
441 }
442
443 // NOTE: this doesn't handle core plugins, but since they are always present during an install, we
444 // shouldn't need to
445 $plugin_files = isset( $GLOBALS['MATOMO_PLUGIN_FILES'] ) ? $GLOBALS['MATOMO_PLUGIN_FILES'] : [];
446 $plugin_files = is_array( $plugin_files ) ? $plugin_files : [];
447
448 foreach ( $plugin_files as $file ) {
449 $plugin_name = basename( dirname( $file ) );
450 if ( 'matomo' === $plugin_name ) {
451 continue;
452 }
453
454 if ( empty( $installed_components[ $plugin_name ] ) ) {
455 return false;
456 }
457 }
458
459 return true;
460 }
461
462 /**
463 * public for tests
464 *
465 * @return void
466 */
467 public function mark_matomo_installed() {
468 $installed = $this->settings->get_option( Settings::INSTANCE_COMPONENTS_INSTALLED );
469 if ( empty( $installed ) ) {
470 $installed = '[]';
471 }
472 $installed = json_decode( $installed, true );
473
474 $installed['core'] = 1;
475 foreach ( Config::getInstance()->PluginsInstalled['PluginsInstalled'] as $plugin_name ) {
476 $installed[ $plugin_name ] = 1;
477 }
478
479 $this->settings->set_option( Settings::INSTANCE_COMPONENTS_INSTALLED, wp_json_encode( $installed ) );
480 $this->settings->save();
481
482 $option_name = Settings::OPTION_PREFIX . 'install-start-time';
483 delete_option( $option_name );
484 }
485
486 private function mark_install_started() {
487 $option_name = Settings::OPTION_PREFIX . 'install-start-time';
488 update_option( $option_name, time() );
489 }
490
491 private function is_install_in_progress() {
492 $five_minutes = 5 * 60;
493
494 $option_name = Settings::OPTION_PREFIX . 'install-start-time';
495 $start_time = get_option( $option_name );
496
497 // install is in progress if there is no last start time, or the last start time is before
498 // five minutes ago (we assume it failed in this case)
499 return ! empty( $start_time ) && $start_time >= time() - $five_minutes;
500 }
501
502 /**
503 * Install all plugins including core and non-core plugins. Non-core plugins
504 * are installed one at a time. Uninstalled plugins will not be loaded
505 * when each non-core plugin is installed.
506 *
507 * This works around the core bug where exceptions can be thrown when an
508 * uninstalled plugin, which is loaded while another plugin is being installed,
509 * handles the "plugin installed" event.
510 *
511 * In a standalone Matomo, this likely won't be an issue, as multiple non-core
512 * plugins are not usually installed at the same time. In Matomo for WordPress,
513 * this can happen as a matter of course in Multi Site installs.
514 *
515 * If a user creates a new WordPress site with multiple non-core plugins installed,
516 * by default the Matomo install process will try to install all of them at once,
517 * causing an error.
518 *
519 * @return void
520 */
521 private function install_plugins_one_at_a_time() {
522 Config::getInstance()->PluginsInstalled = [ 'PluginsInstalled' => [] ];
523
524 $plugin_names = array_map(
525 function ( $path ) {
526 return basename( dirname( $path ) );
527 },
528 $GLOBALS['MATOMO_PLUGIN_FILES']
529 );
530 $non_core_plugins = array_filter(
531 $plugin_names,
532 function ( $name ) {
533 return 'matomo' !== $name;
534 }
535 );
536
537 // unload plugins since plugin instances may be holding out of date information
538 $plugin_manager = Manager::getInstance();
539 $plugin_manager->unloadPlugins();
540 $plugin_manager->loadActivatedPlugins();
541
542 // first, install core plugins without non-core plugins loaded
543 foreach ( $non_core_plugins as $plugin ) {
544 $plugin_manager->unloadPlugin( $plugin );
545 }
546
547 $plugin_manager->installLoadedPlugins();
548
549 // then for every non-core plugin, install one at a time
550 foreach ( $non_core_plugins as $plugin ) {
551 $plugin_manager->loadPlugin( $plugin );
552 $plugin_manager->installLoadedPlugins();
553 }
554
555 // reload activated plugins just in case something didn't go right above
556 $plugin_manager->loadActivatedPlugins();
557 }
558
559 private function is_environment_set_up() {
560 try {
561 StaticContainer::getContainer();
562 return true;
563 } catch ( ContainerDoesNotExistException $ex ) {
564 return false;
565 }
566 }
567 }
568