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