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