PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.8.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.8.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 / Installer.php
matomo / classes / WpMatomo Last commit date
Admin 2 months ago Commands 2 years ago Db 1 year ago Ecommerce 3 months 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 4 years 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 2 months ago Feature.php 2 months ago Installer.php 5 months ago Logger.php 1 year ago OptOut.php 2 months 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
556 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 $sync_config = new Sync\SyncConfig( $this->settings );
211 $tasks = new ScheduledTasks( $this->settings, $sync_config );
212 $last_geoip_update_run_time = $tasks->get_last_time_before_cron( ScheduledTasks::EVENT_GEOIP );
213 if ( empty( $last_geoip_update_run_time ) ) {
214 wp_schedule_single_event( time() + 35, ScheduledTasks::EVENT_GEOIP );
215 }
216
217 // in case something fails with website or user creation
218 // also to set up all the other users
219 wp_schedule_single_event( time() + 45, ScheduledTasks::EVENT_SYNC );
220 }
221
222 return true;
223 }
224
225 public function set_matomo_url() {
226 // 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
227 // the host or if a plugin overwrites the constant of WP_PLUGIN_URL which is used in plugins_url() to not include domain
228 // 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
229
230 $matomo_url = SettingsPiwik::getPiwikUrl();
231 $plugins_url = plugins_url( 'app', MATOMO_ANALYTICS_FILE );
232 $plugins_url = rtrim( $plugins_url, '/' ) . '/';
233 // need to make sure to update plugins url if it changes eg if installed somewhere else or domain changes
234
235 if ( $matomo_url
236 && $plugins_url === $matomo_url
237 && wp_parse_url( $matomo_url, PHP_URL_SCHEME )
238 && wp_parse_url( $matomo_url, PHP_URL_HOST )
239 ) {
240 // if currently no scheme or host is set then we'll make sure to overwrite it
241 return;
242 }
243
244 if ( ! $plugins_url ) {
245 return;
246 }
247
248 $has_host = wp_parse_url( $plugins_url, PHP_URL_HOST );
249
250 if ( ! $has_host ) {
251 return;
252 }
253
254 $has_scheme = wp_parse_url( $plugins_url, PHP_URL_SCHEME );
255
256 if ( ! $has_scheme ) {
257 return;
258 }
259
260 SettingsPiwik::overwritePiwikUrl( $plugins_url );
261 }
262
263 private function install_tracker() {
264 $this->logger->log( 'Matomo is now installing the tracker' );
265 // making sure the tracker will be created in the wp uploads directory
266 $updater = StaticContainer::get( 'Piwik\Plugins\CustomJsTracker\TrackerUpdater' );
267 $updater->update();
268 }
269
270 private function create_db() {
271 $this->logger->log( 'Matomo will now create the database' );
272
273 try {
274 $db_infos = self::get_db_infos();
275 $config = Config::getInstance();
276 if ( isset( $config ) ) {
277 $db_infos = array_merge( $config->database, $db_infos );
278 }
279 $config->database = $db_infos;
280
281 DbHelper::checkDatabaseVersion();
282 } catch ( Exception $e ) {
283 $message = sprintf( 'Database info detection failed with %s in %s:%s.', $e->getMessage(), $e->getFile(), $e->getLine() );
284 throw new Exception( $message, $e->getCode(), $e );
285 }
286
287 $tables_installed = DbHelper::getTablesInstalled();
288 if ( count( $tables_installed ) > 0 ) {
289 // todo define behaviour... might need to ask user how to proceed... but ideally we add check to
290 // see if all tables are there and if so, reuse them...
291 return $db_infos;
292 }
293 DbHelper::createTables();
294 DbHelper::createAnonymousUser();
295
296 return $db_infos;
297 }
298
299 private function create_config( $db_info ) {
300 $this->logger->log( 'Matomo is now creating the config' );
301 $home_url = home_url();
302 $domain = wp_parse_url( $home_url, PHP_URL_HOST );
303 if ( $domain ) {
304 $port = wp_parse_url( $home_url, PHP_URL_PORT );
305 if ( $port ) {
306 $domain .= ':' . $port;
307 }
308 } else {
309 $domain = $home_url;
310 }
311 $general = [
312 'trusted_hosts' => [ $domain ],
313 'salt' => Common::generateUniqId(),
314 ];
315 $config = Config::getInstance();
316 $path = $config->getLocalPath();
317 if ( ! is_dir( dirname( $path ) ) ) {
318 wp_mkdir_p( dirname( $path ) );
319 }
320 $db_default = [];
321 $general_default = [];
322 if ( $config->database ) {
323 $db_default = $config->database;
324 }
325 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
326 if ( $config->General ) {
327 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
328 $general_default = $config->General;
329 }
330 $config->database = array_merge( $db_default, $db_info );
331 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
332 $config->General = array_merge( $general_default, $general );
333 $config->forceSave();
334
335 $mode = 0664;
336 if ( ! chmod( $config->getLocalPath(), $mode ) ) {
337 $this->logger->log( "Can't chmod " . $config->getLocalPath() );
338 }
339 }
340
341 private function create_website() {
342 $sync = new Sync( $this->settings );
343
344 return $sync->sync_current_site();
345 }
346
347 private function create_user() {
348 $sync = new User\Sync();
349
350 $sync->sync_current_users();
351 }
352
353 /**
354 * @param array $default params
355 *
356 * @return array
357 */
358 public static function get_db_infos( $default = [] ) {
359 global $wpdb;
360
361 $socket = '';
362 $host_data = null;
363 $host = null;
364 $port = 3306;
365 if ( method_exists( $wpdb, 'parse_db_host' ) ) {
366 // WP 4.9+
367 $host_data = $wpdb->parse_db_host( DB_HOST );
368 if ( $host_data ) {
369 list( $host, $port, $socket, $is_ipv6 ) = $host_data;
370 if ( ! $port && ! $socket ) {
371 $port = 3306;
372 }
373 }
374 }
375
376 if ( ! $host_data || ! $host ) {
377 // WP 4.8 and older
378 // in case DB credentials change in WordPress, we need to apply these changes here as well on demand
379 $host_parts = explode( ':', DB_HOST );
380 $host = $host_parts[0];
381 if ( count( $host_parts ) === 2 && is_numeric( $host_parts[1] ) ) {
382 $port = $host_parts[1];
383 } else {
384 $port = 3306;
385 }
386 }
387
388 $charset = $wpdb->charset ? $wpdb->charset : self::DEFAULT_DB_CHARSET;
389 if ( defined( 'MATOMO_DB_CHARSET' ) && MATOMO_DB_CHARSET ) {
390 $charset = MATOMO_DB_CHARSET;
391 }
392
393 $collation = $wpdb->collate ? $wpdb->collate : self::DEFAULT_DB_COLLATE;
394 if ( defined( 'MATOMO_DB_COLLATE' ) && MATOMO_DB_COLLATE ) {
395 $collation = MATOMO_DB_COLLATE;
396 }
397
398 $database = [
399 'host' => $host,
400 'port' => $port,
401 'username' => DB_USER,
402 'password' => DB_PASSWORD,
403 'dbname' => DB_NAME,
404 'charset' => $charset,
405 'collation' => $collation,
406 'tables_prefix' => $wpdb->prefix . MATOMO_DATABASE_PREFIX,
407 'adapter' => 'WordPress',
408 ];
409 if ( ! empty( $socket ) ) {
410 $database['unix_socket'] = $socket;
411 }
412 $database = array_merge( $default, $database );
413
414 return $database;
415 }
416
417 private function update_components() {
418 $this->logger->log( 'Matomo will now trigger an update' );
419 Updater::unlock(); // make sure the update can be executed
420 $updater = new Updater( $this->settings );
421 $updater->update();
422 }
423
424 /**
425 * public for tests
426 *
427 * @return bool
428 */
429 public function is_current_instance_installed() {
430 $installed_components = $this->settings->get_option( Settings::INSTANCE_COMPONENTS_INSTALLED );
431 if ( empty( $installed_components ) ) {
432 $installed_components = '[]';
433 }
434 $installed_components = json_decode( $installed_components, true );
435
436 if ( empty( $installed_components['core'] ) ) {
437 return false;
438 }
439
440 // NOTE: this doesn't handle core plugins, but since they are always present during an install, we
441 // shouldn't need to
442 $plugin_files = isset( $GLOBALS['MATOMO_PLUGIN_FILES'] ) ? $GLOBALS['MATOMO_PLUGIN_FILES'] : [];
443 $plugin_files = is_array( $plugin_files ) ? $plugin_files : [];
444
445 foreach ( $plugin_files as $file ) {
446 $plugin_name = basename( dirname( $file ) );
447 if ( 'matomo' === $plugin_name ) {
448 continue;
449 }
450
451 if ( empty( $installed_components[ $plugin_name ] ) ) {
452 return false;
453 }
454 }
455
456 return true;
457 }
458
459 /**
460 * public for tests
461 *
462 * @return void
463 */
464 public function mark_matomo_installed() {
465 $installed = $this->settings->get_option( Settings::INSTANCE_COMPONENTS_INSTALLED );
466 if ( empty( $installed ) ) {
467 $installed = '[]';
468 }
469 $installed = json_decode( $installed, true );
470
471 $installed['core'] = 1;
472 foreach ( Config::getInstance()->PluginsInstalled['PluginsInstalled'] as $plugin_name ) {
473 $installed[ $plugin_name ] = 1;
474 }
475
476 $this->settings->set_option( Settings::INSTANCE_COMPONENTS_INSTALLED, wp_json_encode( $installed ) );
477 $this->settings->save();
478
479 $option_name = Settings::OPTION_PREFIX . 'install-start-time';
480 delete_option( $option_name );
481 }
482
483 private function mark_install_started() {
484 $option_name = Settings::OPTION_PREFIX . 'install-start-time';
485 update_option( $option_name, time() );
486 }
487
488 private function is_install_in_progress() {
489 $five_minutes = 5 * 60;
490
491 $option_name = Settings::OPTION_PREFIX . 'install-start-time';
492 $start_time = get_option( $option_name );
493
494 // install is in progress if there is no last start time, or the last start time is before
495 // five minutes ago (we assume it failed in this case)
496 return ! empty( $start_time ) && $start_time >= time() - $five_minutes;
497 }
498
499 /**
500 * Install all plugins including core and non-core plugins. Non-core plugins
501 * are installed one at a time. Uninstalled plugins will not be loaded
502 * when each non-core plugin is installed.
503 *
504 * This works around the core bug where exceptions can be thrown when an
505 * uninstalled plugin, which is loaded while another plugin is being installed,
506 * handles the "plugin installed" event.
507 *
508 * In a standalone Matomo, this likely won't be an issue, as multiple non-core
509 * plugins are not usually installed at the same time. In Matomo for WordPress,
510 * this can happen as a matter of course in Multi Site installs.
511 *
512 * If a user creates a new WordPress site with multiple non-core plugins installed,
513 * by default the Matomo install process will try to install all of them at once,
514 * causing an error.
515 *
516 * @return void
517 */
518 private function install_plugins_one_at_a_time() {
519 Config::getInstance()->PluginsInstalled = [ 'PluginsInstalled' => [] ];
520
521 $plugin_names = array_map(
522 function ( $path ) {
523 return basename( dirname( $path ) );
524 },
525 $GLOBALS['MATOMO_PLUGIN_FILES']
526 );
527 $non_core_plugins = array_filter(
528 $plugin_names,
529 function ( $name ) {
530 return 'matomo' !== $name;
531 }
532 );
533
534 // unload plugins since plugin instances may be holding out of date information
535 $plugin_manager = Manager::getInstance();
536 $plugin_manager->unloadPlugins();
537 $plugin_manager->loadActivatedPlugins();
538
539 // first, install core plugins without non-core plugins loaded
540 foreach ( $non_core_plugins as $plugin ) {
541 $plugin_manager->unloadPlugin( $plugin );
542 }
543
544 $plugin_manager->installLoadedPlugins();
545
546 // then for every non-core plugin, install one at a time
547 foreach ( $non_core_plugins as $plugin ) {
548 $plugin_manager->loadPlugin( $plugin );
549 $plugin_manager->installLoadedPlugins();
550 }
551
552 // reload activated plugins just in case something didn't go right above
553 $plugin_manager->loadActivatedPlugins();
554 }
555 }
556