PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.1.3
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.1.3
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 / Admin / SystemReport.php
matomo / classes / WpMatomo / Admin Last commit date
TrackingSettings 1 year ago views 1 year ago AccessSettings.php 4 years ago Admin.php 1 year ago AdminSettings.php 2 years ago AdminSettingsInterface.php 6 years ago AdvancedSettings.php 2 years ago Chart.php 4 years ago CookieConsent.php 4 years ago Dashboard.php 4 years ago ExclusionSettings.php 4 years ago GeolocationSettings.php 2 years ago GetStarted.php 4 years ago ImportWpStatistics.php 4 years ago Info.php 4 years ago InvalidIpException.php 4 years ago Marketplace.php 2 years ago MarketplaceSetupWizard.php 1 year ago Menu.php 1 year ago PluginMeasurableSettings.php 2 years ago PrivacySettings.php 4 years ago SafeModeMenu.php 4 years ago Summary.php 4 years ago SystemReport.php 2 years ago TrackingSettings.php 1 year ago
SystemReport.php
2021 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\Admin;
11
12 use Exception;
13 use ITSEC_Modules;
14 use Piwik\CliMulti;
15 use Piwik\CliMulti\CliPhp;
16 use Piwik\Common;
17 use Piwik\Config;
18 use Piwik\Container\StaticContainer;
19 use Piwik\DeviceDetector\DeviceDetectorFactory;
20 use Piwik\Filesystem;
21 use Piwik\Plugin;
22 use Piwik\Plugins\CoreAdminHome\API;
23 use Piwik\Plugins\Diagnostics\Diagnostic\DiagnosticResult;
24 use Piwik\Plugins\Diagnostics\DiagnosticService;
25 use Piwik\Plugins\SitesManager\Model;
26 use Piwik\Plugins\UserCountry\LocationProvider;
27 use Piwik\Plugins\WordPress\WordPress;
28 use Piwik\SettingsPiwik;
29 use Piwik\Tracker\Failures;
30 use Piwik\Version;
31 use WpMatomo;
32 use WpMatomo\Bootstrap;
33 use WpMatomo\Capabilities;
34 use WpMatomo\Installer;
35 use WpMatomo\Logger;
36 use WpMatomo\Paths;
37 use WpMatomo\ScheduledTasks;
38 use WpMatomo\Settings;
39 use WpMatomo\Site;
40 use WpMatomo\Site\Sync as SiteSync;
41 use WpMatomo\Updater;
42 use WpMatomo\User\Sync as UserSync;
43
44 if ( ! defined( 'ABSPATH' ) ) {
45 exit; // if accessed directly
46 }
47
48 /**
49 * error_reporting is required for this page
50 * phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_error_reporting
51 *
52 * We want a real data, not something coming from cache
53 * phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
54 *
55 * This is a report error, so silent the possible errors
56 * phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
57 *
58 * We cannot use parameters of statements as this is the table names we build
59 * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
60 * phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
61 */
62 class SystemReport {
63 const NONCE_NAME = 'matomo_troubleshooting';
64 const TROUBLESHOOT_SYNC_USERS = 'matomo_troubleshooting_action_site_users';
65 const TROUBLESHOOT_SYNC_ALL_USERS = 'matomo_troubleshooting_action_all_users';
66 const TROUBLESHOOT_SYNC_SITE = 'matomo_troubleshooting_action_site';
67 const TROUBLESHOOT_SYNC_ALL_SITES = 'matomo_troubleshooting_action_all_sites';
68 const TROUBLESHOOT_CLEAR_MATOMO_CACHE = 'matomo_troubleshooting_action_clear_matomo_cache';
69 const TROUBLESHOOT_ARCHIVE_NOW = 'matomo_troubleshooting_action_archive_now';
70 const TROUBLESHOOT_UPDATE_GEOIP_DB = 'matomo_troubleshooting_action_update_geoipdb';
71 const TROUBLESHOOT_CLEAR_LOGS = 'matomo_troubleshooting_action_clear_logs';
72 const TROUBLESHOOT_RUN_UPDATER = 'matomo_troubleshooting_action_run_updater';
73
74 private $not_compatible_plugins = [
75 'minify-html-markup',
76 ];
77
78 private $valid_tabs = [ 'troubleshooting' ];
79
80 /**
81 * @var Settings
82 */
83 private $settings;
84
85 /**
86 * @var Logger
87 */
88 private $logger;
89
90 private $initial_error_reporting = null;
91
92 private $shell_exec_available;
93 /**
94 * @var \WpMatomo\Db\Settings
95 */
96 public $db_settings;
97 /**
98 * @var string the php binary used by Matomo (use get_php_binary() to access)
99 */
100 private $binary;
101
102 /**
103 * @var string
104 */
105 private $path_to_plugin;
106
107 private static $matomo_tables;
108
109 public function __construct( Settings $settings ) {
110 $this->settings = $settings;
111 $this->logger = new Logger();
112 $this->db_settings = new \WpMatomo\Db\Settings();
113 $this->shell_exec_available = function_exists( 'shell_exec' );
114 $this->path_to_plugin = $this->get_abs_path_to_plugin();
115 }
116
117 public function get_not_compatible_plugins() {
118 return $this->not_compatible_plugins;
119 }
120
121 private function execute_troubleshoot_if_needed() {
122 if ( ! empty( $_POST )
123 && is_admin()
124 && check_admin_referer( self::NONCE_NAME )
125 && current_user_can( Capabilities::KEY_SUPERUSER )
126 ) {
127 if ( ! empty( $_POST[ self::TROUBLESHOOT_ARCHIVE_NOW ] ) ) {
128 Bootstrap::do_bootstrap();
129 $scheduled_tasks = new ScheduledTasks( $this->settings );
130
131 if ( ! defined( 'PIWIK_ARCHIVE_NO_TRUNCATE' ) ) {
132 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
133 define( 'PIWIK_ARCHIVE_NO_TRUNCATE', 1 ); // when triggering it manually, we prefer the full error message
134 }
135
136 try {
137 // force invalidation of archive to ensure it actually will rearchive the data
138 $site = new Site();
139 $idsite = $site->get_current_matomo_site_id();
140 if ( $idsite ) {
141 $timezone = \Piwik\Site::getTimezoneFor( $idsite );
142 $now_string = \Piwik\Date::factory( 'now', $timezone )->toString();
143 foreach ( [ 'day' ] as $period ) {
144 API::getInstance()->invalidateArchivedReports( $idsite, $now_string, $period, false, false );
145 }
146 }
147 } catch ( Exception $e ) {
148 $this->logger->log_exception( 'archive_invalidate', $e );
149 }
150
151 try {
152 $errors = $scheduled_tasks->archive( true, false );
153 } catch ( Exception $e ) {
154 echo '<div class="error"><p>' . esc_html__( 'Matomo Archive Error', 'matomo' ) . ': ' . esc_html( matomo_anonymize_value( $e->getMessage() . ' =>' . $this->logger->get_readable_trace( $e ) ) ) . '</p></div>';
155 throw $e;
156 }
157
158 if ( ! empty( $errors ) ) {
159 echo '<div class="notice notice-warning"><p>Matomo Archive Warnings: ';
160 foreach ( $errors as $error ) {
161 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
162 echo nl2br( esc_html( matomo_anonymize_value( var_export( $error, 1 ) ) ) );
163 echo '<br/>';
164 }
165 echo '</p></div>';
166 } else {
167 echo '<div class="notice notice-success"><p>' . esc_html__( 'Matomo Archiving completed successfully!', 'matomo' ) . '</p></div>';
168 }
169 }
170
171 if ( ! empty( $_POST[ self::TROUBLESHOOT_CLEAR_MATOMO_CACHE ] ) ) {
172 $paths = new Paths();
173 $paths->clear_cache_dir();
174 // we first delete the cache dir manually just in case there's something
175 // going wrong with matomo and bootstrapping would not even be possible.
176 Bootstrap::do_bootstrap();
177 Filesystem::deleteAllCacheOnUpdate();
178 Updater::unlock();
179 }
180
181 if ( ! empty( $_POST[ self::TROUBLESHOOT_UPDATE_GEOIP_DB ] ) ) {
182 $scheduled_tasks = new ScheduledTasks( $this->settings );
183 $scheduled_tasks->update_geo_ip2_db();
184 }
185
186 if ( ! empty( $_POST[ self::TROUBLESHOOT_CLEAR_LOGS ] ) ) {
187 $this->logger->clear_logged_exceptions();
188 }
189
190 if ( ! $this->settings->is_network_enabled() || ! is_network_admin() ) {
191 if ( ! empty( $_POST[ self::TROUBLESHOOT_SYNC_USERS ] ) ) {
192 $sync = new UserSync();
193 $sync->sync_current_users();
194 }
195 if ( ! empty( $_POST[ self::TROUBLESHOOT_SYNC_SITE ] ) ) {
196 $sync = new SiteSync( $this->settings );
197 $sync->sync_current_site();
198 }
199 if ( ! empty( $_POST[ self::TROUBLESHOOT_RUN_UPDATER ] ) ) {
200 $update_from_version = ! empty( $_POST['matomo_troubleshooting_update_from'] )
201 ? sanitize_text_field( wp_unslash( $_POST['matomo_troubleshooting_update_from'] ) )
202 : null;
203 $update_from_version = trim( $update_from_version );
204
205 if ( ! empty( $update_from_version )
206 && ! preg_match( '/^\d+(?:.\d+)*$/', $update_from_version )
207 ) {
208 echo '<div class="error"><p>' . esc_html__( 'Matomo Update Error', 'matomo' )
209 . ': unrecognized version string "' . esc_html( $update_from_version )
210 . '", ignoring.</p></div>';
211
212 $update_from_version = '';
213 }
214
215 try {
216 Updater::unlock();
217 $sync = new Updater( $this->settings );
218 $sync->update( $update_from_version );
219 } catch ( \Exception $e ) {
220 echo '<div class="error"><p>' . esc_html__( 'Matomo Update Error', 'matomo' ) . ': ' . esc_html( matomo_anonymize_value( $e->getMessage() . ' =>' . $this->logger->get_readable_trace( $e ) ) ) . '</p></div>';
221 }
222 }
223 }
224 if ( $this->settings->is_network_enabled() ) {
225 if ( ! empty( $_POST[ self::TROUBLESHOOT_SYNC_ALL_SITES ] ) ) {
226 $sync = new SiteSync( $this->settings );
227 $sync->sync_all();
228 }
229 if ( ! empty( $_POST[ self::TROUBLESHOOT_SYNC_ALL_USERS ] ) ) {
230 $sync = new UserSync();
231 $sync->sync_all();
232 }
233 }
234 }
235 }
236
237 private function get_error_tables() {
238 $matomo_tables = self::$matomo_tables;
239
240 if ( ! $matomo_tables ) {
241 $matomo_tables = [
242 [
243 'title' => 'Matomo',
244 'rows' => $this->get_matomo_info(),
245 'has_comments' => true,
246 ],
247 [
248 'title' => 'WordPress',
249 'rows' => $this->get_wordpress_info(),
250 'has_comments' => true,
251 ],
252 [
253 'title' => 'WordPress Plugins',
254 'rows' => $this->get_plugins_info(),
255 'has_comments' => true,
256 ],
257 [
258 'title' => 'Server',
259 'rows' => $this->get_server_info(),
260 'has_comments' => true,
261 ],
262 [
263 'title' => 'PHP cli',
264 'rows' => $this->get_phpcli_info(),
265 'has_comments' => true,
266 ],
267 [
268 'title' => 'Database',
269 'rows' => $this->get_db_info(),
270 'has_comments' => true,
271 ],
272 [
273 'title' => 'Browser',
274 'rows' => $this->get_browser_info(),
275 'has_comments' => true,
276 ],
277 ];
278 self::$matomo_tables = $matomo_tables;
279 }
280
281 return $matomo_tables;
282 }
283
284 public function errors_present() {
285 $cache_key = 'matomo_system_report_has_errors';
286 $cache_value = get_site_transient( $cache_key );
287
288 if ( false === $cache_value ) {
289 // pre-record that there were no errors found. in case the system report fails to execute, this will
290 // allow the rest of Matomo for WordPress to continue to still be usable.
291 set_site_transient( $cache_key, 0, WEEK_IN_SECONDS );
292
293 $matomo_tables = $this->get_error_tables();
294
295 $matomo_tables = apply_filters( 'matomo_systemreport_tables', $matomo_tables );
296 $matomo_tables = $this->add_errors_first( $matomo_tables );
297
298 foreach ( $matomo_tables as $report_table ) {
299 foreach ( $report_table['rows'] as $row ) {
300 if ( ! empty( $row['is_error'] ) || ! empty( $row['is_warning'] ) ) {
301 $cache_value = true;
302 }
303 }
304 }
305
306 set_site_transient( $cache_key, (int) $cache_value, WEEK_IN_SECONDS );
307 }
308
309 return 1 === (int) $cache_value;
310 }
311
312 public function show() {
313 $this->execute_troubleshoot_if_needed();
314
315 $settings = $this->settings;
316
317 $matomo_active_tab = '';
318
319 if ( isset( $_GET['tab'] ) ) {
320 $tab = sanitize_text_field( wp_unslash( $_GET['tab'] ) );
321 if ( in_array( $tab, $this->valid_tabs, true ) ) {
322 $matomo_active_tab = $tab;
323 }
324 }
325
326 $matomo_tables = [];
327 if ( empty( $matomo_active_tab ) ) {
328 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting
329 $this->initial_error_reporting = @error_reporting();
330 $matomo_tables = $this->get_error_tables();
331 }
332 $matomo_tables = apply_filters( 'matomo_systemreport_tables', $matomo_tables );
333 $matomo_tables = $this->add_errors_first( $matomo_tables );
334 $matomo_has_warning_and_no_errors = $this->has_only_warnings_no_error( $matomo_tables );
335
336 $matomo_has_exception_logs = $this->logger->get_last_logged_entries();
337
338 include dirname( __FILE__ ) . '/views/systemreport.php';
339 }
340
341 private function has_only_warnings_no_error( $report_tables ) {
342 $has_warning = false;
343 $has_error = false;
344 foreach ( $report_tables as $report_table ) {
345 foreach ( $report_table['rows'] as $row ) {
346 if ( ! empty( $row['is_error'] ) ) {
347 $has_error = true;
348 }
349 if ( ! empty( $row['is_warning'] ) ) {
350 $has_warning = true;
351 }
352 }
353 }
354
355 return $has_warning && ! $has_error;
356 }
357
358 private function add_errors_first( $report_tables ) {
359 $errors = [
360 'title' => 'Errors',
361 'rows' => [],
362 'has_comments' => true,
363 ];
364 foreach ( $report_tables as $report_table ) {
365 foreach ( $report_table['rows'] as $row ) {
366 if ( ! empty( $row['is_error'] ) ) {
367 $errors['rows'][] = $row;
368 }
369 }
370 }
371
372 if ( ! empty( $errors['rows'] ) ) {
373 array_unshift( $report_tables, $errors );
374 }
375
376 return $report_tables;
377 }
378
379 private function check_file_exists_and_writable( $rows, $path_to_check, $title, $required ) {
380 $file_exists = file_exists( $path_to_check );
381 $file_readable = is_readable( $path_to_check );
382 $file_writable = is_writable( $path_to_check );
383 $comment = '"' . $path_to_check . '" ';
384 if ( ! $file_exists ) {
385 $comment .= sprintf( esc_html__( '%s does not exist. ', 'matomo' ), $title );
386 }
387 if ( ! $file_readable ) {
388 $comment .= sprintf( esc_html__( '%s is not readable. ', 'matomo' ), $title );
389 }
390 if ( ! $file_writable ) {
391 $comment .= sprintf( esc_html__( '%s is not writable. ', 'matomo' ), $title );
392 }
393
394 $rows[] = [
395 'name' => sprintf( esc_html__( '%s exists and is writable.', 'matomo' ), $title ),
396 'value' => $file_exists && $file_readable && $file_writable ? esc_html__( 'Yes', 'matomo' ) : esc_html__( 'No', 'matomo' ),
397 'comment' => $comment,
398 'is_error' => $required && ( ! $file_exists || ! $file_readable ),
399 'is_warning' => ! $required && ( ! $file_exists || ! $file_readable ),
400 ];
401
402 return $rows;
403 }
404
405 private function get_phpcli_info() {
406 $rows = [];
407
408 if ( $this->shell_exec_available ) {
409 try {
410 $cli_multi = new CliMulti();
411
412 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
413 $supports_async = $cli_multi->supportsAsync;
414 } catch ( \Exception $ex ) {
415 $rows[] = [
416 'name' => esc_html__( 'PHP CLI configuration', 'matomo' ),
417 'value' => esc_html__( 'Unexpected error', 'matomo' ),
418 'is_warning' => true,
419 'comment' => sprintf( esc_html__( 'Could not detect whether async archiving is enabled: %s', 'matomo' ), $ex->getMessage() ),
420 ];
421 }
422
423 $phpcli_version = $this->get_phpcli_output( '-r "echo phpversion();"' );
424
425 $is_warning = false;
426 $comment = '';
427
428 $advanced_settings_url = home_url( '/wp-admin/admin.php?page=matomo-settings&tab=advanced#matomo[disable_async_archiving]' );
429
430 $is_using_cli_archiving = ! \WpMatomo::is_async_archiving_manually_disabled() && $supports_async;
431
432 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
433 if ( version_compare( $phpcli_version, PHP_VERSION ) < 0 ) {
434 if ( $is_using_cli_archiving ) {
435 $is_warning = true;
436 }
437
438 $comment = sprintf(
439 esc_html__( 'The detected PHP CLI version does not match the PHP web version. To avoid archiving errors, %1$senable archiving via HTTP requests%2$s, or %3$smanually set the path to your PHP CLI executable%4$s to the one for PHP version %5$s.', 'matomo' ),
440 '<a href="' . $advanced_settings_url . '">',
441 '</a>',
442 '<a href="https://matomo.org/faq/how-to-solve-the-error-message-your-php-cli-version-is-not-compatible-with-the-matomo-requirements/" target="_blank" rel="noreferrer noopener">',
443 '</a>',
444 PHP_VERSION
445 );
446 }
447 $rows[] = [
448 'name' => esc_html__( 'PHP CLI Version', 'matomo' ),
449 'value' => $phpcli_version,
450 'comment' => $comment,
451 'is_warning' => $is_warning,
452 ];
453
454 $is_error = false;
455 $value = __( 'ok', 'matomo' );
456 $comment = '';
457 if ( ! intval( $this->get_phpcli_output( '-r "echo extension_loaded(\'mysqli\');"' ) ) ) {
458 $value = __( 'missing', 'matomo' );
459 $is_error = true;
460 $comment = esc_html__( 'Your PHP cli does not load the MySQLi extension. You might have archiving problems in Matomo but also others problems in your WordPress cron tasks. You should enable this extension', 'matomo' );
461 }
462
463 $rows[] = [
464 'name' => esc_html__( 'MySQLi support', 'matomo' ),
465 'value' => $value,
466 'comment' => $comment,
467 'is_error' => $is_using_cli_archiving ? $is_error : false,
468 ];
469
470 if ( $supports_async ) {
471 $this->check_wp_can_be_loaded_in_php_cli( $rows );
472 }
473 }
474
475 return $rows;
476 }
477
478 private function check_wp_can_be_loaded_in_php_cli( &$rows ) {
479 if ( ! $this->binary ) {
480 return;
481 }
482
483 $wp_load_path = $this->find_wp_load_path();
484 if ( ! $wp_load_path ) {
485 return;
486 }
487
488 $command = "-r 'require_once( \"$wp_load_path\" );'";
489 $output = $this->get_phpcli_output( $command );
490 if ( empty( $output ) ) {
491 $rows[] = [
492 'name' => esc_html__( 'PHP CLI configuration', 'matomo' ),
493 'value' => esc_html__( 'Configured correctly', 'matomo' ),
494 ];
495 } else {
496 $error_message = esc_html__(
497 'WordPress cannot be loaded via PHP CLI. Please ensure both WordPress and PHP CLI are correctly configured. Note: If you are using get_cfg_var() in your wp-config.php, you will need to make sure the php.ini file for PHP CLI has the correct values. You may need to contact your hosting provider for these changes to be made.',
498 'matomo'
499 );
500
501 $rows[] = [
502 'name' => esc_html__( 'PHP CLI configuration', 'matomo' ),
503 'value' => esc_html__( 'warning', 'matomo' ),
504 'comment' => $error_message,
505 ];
506 }
507 }
508
509 private function get_phpcli_output( $phpcli_params ) {
510 $output = '';
511 if ( $this->shell_exec_available && $this->get_php_cli_binary() ) {
512 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_shell_exec
513 $output = trim( '' . @shell_exec( $this->get_php_cli_binary() . ' ' . $phpcli_params ) );
514 }
515
516 return $output;
517 }
518
519 private function get_matomo_info() {
520 $rows = [];
521
522 $plugin_data = get_plugin_data( MATOMO_ANALYTICS_FILE, $markup = false, $translate = false );
523 $install_time = get_option( Installer::OPTION_NAME_INSTALL_DATE );
524
525 $rows[] = [
526 'name' => esc_html__( 'Matomo Plugin Version', 'matomo' ),
527 'value' => $plugin_data['Version'],
528 'comment' => '',
529 ];
530
531 $paths = new Paths();
532 $path_config_file = $paths->get_config_ini_path();
533 $rows = $this->check_file_exists_and_writable( $rows, $path_config_file, 'Config', true );
534
535 $path_tracker_file = $paths->get_matomo_js_upload_path();
536 $rows = $this->check_file_exists_and_writable( $rows, $path_tracker_file, 'JS Tracker', false );
537
538 $rows[] = [
539 'name' => esc_html__( 'Plugin directories', 'matomo' ),
540 'value' => ! empty( $GLOBALS['MATOMO_PLUGIN_DIRS'] ) ? 'Yes' : 'No',
541 'comment' => ! empty( $GLOBALS['MATOMO_PLUGIN_DIRS'] ) ? wp_json_encode( $GLOBALS['MATOMO_PLUGIN_DIRS'] ) : '',
542 ];
543
544 $tmp_dir = $paths->get_tmp_dir();
545
546 $rows[] = [
547 'name' => esc_html__( 'Tmp directory writable', 'matomo' ),
548 'value' => is_writable( $tmp_dir ),
549 'comment' => $tmp_dir,
550 ];
551
552 if ( ! empty( $_SERVER['MATOMO_WP_ROOT_PATH'] ) ) {
553 // we can have / in this value
554 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
555 $custom_path = rtrim( $_SERVER['MATOMO_WP_ROOT_PATH'], '/' ) . '/wp-load.php';
556 $path_exists = file_exists( $custom_path );
557 $comment = '';
558 if ( ! $path_exists ) {
559 $comment = 'It seems the path does not point to the WP root directory.';
560 }
561
562 $rows[] = [
563 'name' => 'Custom MATOMO_WP_ROOT_PATH',
564 'value' => $path_exists,
565 'is_error' => ! $path_exists,
566 'comment' => $comment,
567 ];
568 }
569
570 $report = null;
571
572 if ( ! WpMatomo::is_safe_mode() ) {
573 try {
574 Bootstrap::do_bootstrap();
575 /** @var DiagnosticService $service */
576 $service = StaticContainer::get( DiagnosticService::class );
577 $report = $service->runDiagnostics();
578
579 $rows[] = [
580 'name' => esc_html__( 'Matomo Version', 'matomo' ),
581 'value' => \Piwik\Version::VERSION,
582 'comment' => '',
583 ];
584 } catch ( Exception $e ) {
585 $rows[] = [
586 'name' => esc_html__( 'Matomo System Check', 'matomo' ),
587 'value' => 'Failed to run Matomo system check.',
588 'comment' => $e->getMessage(),
589 'is_error' => true,
590 ];
591 }
592 }
593
594 $site = new Site();
595 $idsite = $site->get_current_matomo_site_id();
596
597 $rows[] = [
598 'name' => esc_html__( 'Matomo Blog idSite', 'matomo' ),
599 'value' => $idsite,
600 'comment' => '',
601 ];
602
603 $install_date = '';
604 if ( ! empty( $install_time ) ) {
605 $install_date = 'Install date: ' . $this->convert_time_to_date( $install_time, true, false );
606 }
607
608 $rows[] = [
609 'name' => esc_html__( 'Matomo Install Version', 'matomo' ),
610 'value' => get_option( Installer::OPTION_NAME_INSTALL_VERSION ),
611 'comment' => $install_date,
612 ];
613
614 $wpmatomo_updater = new \WpMatomo\Updater( $this->settings );
615 if ( ! WpMatomo::is_safe_mode() ) {
616 $outstanding_updates = $wpmatomo_updater->get_plugins_requiring_update();
617 $upgrade_in_progress = $wpmatomo_updater->is_upgrade_in_progress();
618 $rows[] = [
619 'name' => 'Upgrades outstanding',
620 'value' => ! empty( $outstanding_updates ),
621 'comment' => ! empty( $outstanding_updates ) ? wp_json_encode( $outstanding_updates ) : '',
622 ];
623 $rows[] = [
624 'name' => 'Upgrade in progress',
625 'value' => $upgrade_in_progress,
626 'comment' => '',
627 ];
628 }
629
630 if ( ! $wpmatomo_updater->load_plugin_functions() ) {
631 // this should actually never happen...
632 $rows[] = [
633 'name' => 'Matomo Upgrade Plugin Functions',
634 'is_warning' => true,
635 'value' => false,
636 'comment' => 'Function "get_plugin_data" not available. There may be an issue with upgrades not being executed. Please reach out to us.',
637 ];
638 }
639
640 $rows[] = [
641 'section' => 'Endpoints',
642 ];
643
644 $rows[] = [
645 'name' => 'Matomo JavaScript Tracker URL',
646 'value' => '',
647 'comment' => $paths->get_js_tracker_url_in_matomo_dir(),
648 ];
649
650 $rows[] = [
651 'name' => 'Matomo JavaScript Tracker - WP Rest API',
652 'value' => '',
653 'comment' => $paths->get_js_tracker_rest_api_endpoint(),
654 ];
655
656 $rows[] = [
657 'name' => 'Matomo HTTP Tracking API',
658 'value' => '',
659 'comment' => $paths->get_tracker_api_url_in_matomo_dir(),
660 ];
661
662 $rows[] = [
663 'name' => 'Matomo HTTP Tracking API - WP Rest API',
664 'value' => '',
665 'comment' => $paths->get_tracker_api_rest_api_endpoint(),
666 ];
667
668 $matomo_plugin_dir_name = basename( dirname( MATOMO_ANALYTICS_FILE ) );
669 if ( 'matomo' !== $matomo_plugin_dir_name ) {
670 $rows[] = [
671 'name' => 'Matomo Plugin Name is correct',
672 'value' => false,
673 'is_error' => true,
674 'comment' => 'The plugin name should be "matomo" but seems to be "' . $matomo_plugin_dir_name . '". As a result, admin pages and other features might not work. You might need to rename the directory name of this plugin and reactive the plugin.',
675 ];
676 } elseif ( ! is_plugin_active( 'matomo/matomo.php' ) ) {
677 $rows[] = [
678 'name' => 'Matomo Plugin not active',
679 'value' => false,
680 'is_error' => true,
681 'comment' => 'It seems WordPress thinks that `matomo/matomo.php` is not active. As a result Matomo reporting and admin pages may not work. You may be able to fix this by deactivating and activating the Matomo Analytics plugin. One of the reasons this could happen is that you used to have Matomo installed in the wrong folder.',
682 ];
683 }
684
685 $rows[] = [
686 'section' => 'Crons',
687 ];
688
689 $scheduled_tasks = new ScheduledTasks( $this->settings );
690 $all_events = $scheduled_tasks->get_all_events();
691
692 $rows[] = [
693 'name' => esc_html__( 'Server time', 'matomo' ),
694 'value' => $this->convert_time_to_date( time(), false ),
695 'comment' => '',
696 ];
697
698 $rows[] = [
699 'name' => esc_html__( 'Blog time', 'matomo' ),
700 'value' => $this->convert_time_to_date( time(), true ),
701 'comment' => esc_html__( 'Below dates are shown in blog timezone', 'matomo' ),
702 ];
703
704 foreach ( $all_events as $event_name => $event_config ) {
705 $last_run_before = $scheduled_tasks->get_last_time_before_cron( $event_name );
706 $last_run_after = $scheduled_tasks->get_last_time_after_cron( $event_name );
707
708 $next_scheduled = wp_next_scheduled( $event_name );
709
710 $comment = ' Last started: ' . $this->convert_time_to_date( $last_run_before, true, true ) . '.';
711 $comment .= ' Last ended: ' . $this->convert_time_to_date( $last_run_after, true, true ) . '.';
712 $comment .= ' Interval: ' . $event_config['interval'];
713
714 $rows[] = [
715 'name' => $event_config['name'],
716 'value' => 'Next run: ' . $this->convert_time_to_date( $next_scheduled, true, true ),
717 'comment' => $comment,
718 ];
719 }
720
721 $suports_async = false;
722 if ( ! WpMatomo::is_safe_mode() && $report ) {
723 $rows[] = [
724 'section' => esc_html__( 'Mandatory checks', 'matomo' ),
725 ];
726
727 $rows = $this->add_diagnostic_results( $rows, $report->getMandatoryDiagnosticResults() );
728
729 $rows[] = [
730 'section' => esc_html__( 'Optional checks', 'matomo' ),
731 ];
732 $rows = $this->add_diagnostic_results( $rows, $report->getOptionalDiagnosticResults() );
733
734 try {
735 $cli_multi = new CliMulti();
736 $suports_async = $cli_multi->supportsAsync();
737
738 $rows[] = [
739 'name' => esc_html__( 'Supports Async Archiving', 'matomo' ),
740 'value' => $suports_async,
741 'comment' => '',
742 ];
743 } catch ( \Exception $e ) {
744 $rows[] = [
745 'name' => esc_html__( 'Supports Async Archiving', 'matomo' ),
746 'value' => 'Could not check if async archiving is supported.',
747 'comment' => $e->getMessage(),
748 'is_error' => true,
749 ];
750 }
751
752 $rows[] = [
753 'name' => 'Async Archiving Disabled in Setting',
754 'value' => $this->settings->is_async_archiving_disabled_by_option(),
755 'comment' => '',
756 ];
757
758 $location_provider = LocationProvider::getCurrentProvider();
759 if ( $location_provider ) {
760 try {
761 $location_provider = LocationProvider::getCurrentProvider();
762 if ( $location_provider ) {
763 $rows[] = [
764 'name' => 'Location provider ID',
765 'value' => $location_provider->getId(),
766 'comment' => '',
767 ];
768 $rows[] = [
769 'name' => 'Location provider available',
770 'value' => $location_provider->isAvailable(),
771 'comment' => '',
772 ];
773 $rows[] = [
774 'name' => 'Location provider working',
775 'value' => $location_provider->isWorking(),
776 'comment' => '',
777 ];
778 }
779 } catch ( \Exception $e ) {
780 $rows[] = [
781 'name' => 'Location provider ID',
782 'value' => 'Failed to get the configured Location Provider.',
783 'comment' => $e->getMessage(),
784 'is_error' => true,
785 ];
786 }
787 }
788
789 if ( ! WpMatomo::is_safe_mode() ) {
790 Bootstrap::do_bootstrap();
791 $general = Config::getInstance()->General;
792
793 if ( empty( $general['proxy_client_headers'] ) ) {
794 foreach ( AdvancedSettings::$valid_host_headers as $header ) {
795 if ( ! empty( $_SERVER[ $header ] ) ) {
796 $rows[] = [
797 'name' => 'Proxy header',
798 'value' => $header,
799 'is_warning' => true,
800 'comment' => sprintf( esc_html__( 'A proxy header is set which means you maybe need to configure a proxy header in the Advanced settings to make location reporting work. If the location in your reports is detected correctly, you can ignore this warning. %s', 'matomo' ), sprintf( '<a href="%s" target="_blank">%s</a>', 'https://matomo.org/faq/wordpress/how-do-i-fix-the-proxy-header-warning-in-the-matomo-for-wordpress-system-report/', esc_html__( 'Learn more', 'matomo' ) ) ),
801 ];
802 }
803 }
804 }
805
806 try {
807 $incompatible_plugins = Plugin\Manager::getInstance()->getIncompatiblePlugins( Version::VERSION );
808 if ( ! empty( $incompatible_plugins ) ) {
809 $rows[] = [
810 'section' => esc_html__( 'Incompatible Matomo plugins', 'matomo' ),
811 ];
812 foreach ( $incompatible_plugins as $plugin ) {
813 $rows[] = [
814 'name' => 'Plugin has missing dependencies',
815 'value' => $plugin->getPluginName(),
816 'is_error' => true,
817 'comment' => sprintf( esc_html__( '%s If the plugin requires a different Matomo version you may need to update it. If you no longer use it consider uninstalling it.', 'matomo' ), $plugin->getMissingDependenciesAsString( Version::VERSION ) ),
818 ];
819 }
820 }
821 } catch ( \Exception $e ) {
822 $rows[] = [
823 'section' => esc_html__( 'Incompatible Matomo plugins', 'matomo' ),
824 ];
825 $rows[] = [
826 'name' => 'Incompatible Matomo plugin check',
827 'value' => 'Failed to check for incompatible Matomo plugins.',
828 'is_error' => true,
829 'comment' => $e->getMessage(),
830 ];
831 }
832 }
833
834 $num_days_check_visits = 5;
835 $had_visits = $this->had_visits_in_last_days( $num_days_check_visits );
836
837 if ( false === $had_visits || true === $had_visits ) {
838 // do not show info if we could not detect it (had_visits === null)
839 $comment = '';
840 if ( ! $had_visits ) {
841 $comment = sprintf( esc_html__( 'It looks like there were no visits in the last %s days. This may be expected if tracking is disabled, you have not added the tracking code, or your website does not have many visitors in general and you exclude your own visits.', 'matomo' ), $num_days_check_visits );
842 }
843
844 $rows[] = [
845 'name' => 'Had visit in last ' . $num_days_check_visits . ' days',
846 'value' => $had_visits,
847 'is_warning' => ! $had_visits && $this->settings->is_tracking_enabled(),
848 'comment' => $comment,
849 ];
850 }
851
852 if ( ! WpMatomo::is_safe_mode() ) {
853 Bootstrap::do_bootstrap();
854
855 try {
856 $matomo_url = SettingsPiwik::getPiwikUrl();
857 $rows[] = [
858 'name' => 'Matomo URL',
859 'comment' => $matomo_url,
860 'value' => ! empty( $matomo_url ),
861 ];
862 } catch ( \Exception $e ) {
863 $rows[] = [
864 'name' => 'Matomo URL',
865 'comment' => $e->getMessage(),
866 'value' => 'Failed to fetch the Matomo URL.',
867 'is_error' => true,
868 ];
869 }
870 }
871
872 // check that yml files are not accessible
873 $url = plugins_url( 'app', MATOMO_ANALYTICS_FILE ) . '/vendor/matomo/device-detector/regexes/bots.yml';
874 $result = wp_remote_post(
875 $url,
876 array(
877 'method' => 'GET',
878 'sslverify' => false,
879 'timeout' => 2,
880 )
881 );
882 if ( is_array( $result ) ) {
883 $response_code = (int) $result['response']['code'];
884 if ( $response_code >= 200 && $response_code < 300 ) {
885 $rows[] = [
886 'name' => __( 'YML files should not be accessible', 'matomo' ),
887 'value' => 'warning',
888 'comment' => __( 'The .yml files in the wp-content/plugins/matomo/app/vendor directory are accessible from the internet. This can cause some web security tools to flag your website as suspicious. If you are using Apache, it is probably due to your server configuration disabling the use of .htaccess files. If you are instead using nginx, it is due to your nginx configuration allowing .yml files. You may need to contact your hosting provider to fix this.', 'matomo' ),
889 'is_warning' => true,
890 ];
891 }
892 }
893 }
894
895 $rows[] = [
896 'section' => 'Matomo Settings',
897 ];
898
899 // always show these settings
900 $global_settings_always_show = [
901 'track_mode',
902 'track_ecommerce',
903 'track_codeposition',
904 'track_api_endpoint',
905 'track_js_endpoint',
906 ];
907 foreach ( $global_settings_always_show as $key ) {
908 $rows[] = [
909 'name' => ucfirst( str_replace( '_', ' ', $key ) ),
910 'value' => $this->settings->get_global_option( $key ),
911 'comment' => '',
912 ];
913 }
914
915 // otherwise show only few customised settings
916 // mostly only numeric values and booleans to not eg accidentally show anything that would store a token etc
917 // like we don't want to show license key etc
918 foreach ( $this->settings->get_customised_global_settings() as $key => $val ) {
919 if ( is_numeric( $val ) || is_bool( $val ) || 'track_content' === $key || 'track_user_id' === $key || 'core_version' === $key || 'version_history' === $key || 'mail_history' === $key ) {
920 if ( is_array( $val ) ) {
921 $val = implode( ', ', $val );
922 }
923
924 $rows[] = [
925 'name' => ucfirst( str_replace( '_', ' ', $key ) ),
926 'value' => $val,
927 'comment' => '',
928 ];
929 }
930 }
931
932 $rows[] = [
933 'section' => 'Logs',
934 ];
935
936 $error_log_entries = $this->logger->get_last_logged_entries();
937
938 if ( ! empty( $error_log_entries ) ) {
939 foreach ( $error_log_entries as $error ) {
940 if ( ! empty( $install_time )
941 && is_numeric( $install_time )
942 && ! empty( $error['name'] )
943 && ! empty( $error['value'] )
944 && is_numeric( $error['value'] )
945 && 'cron_sync' === $error['name']
946 && $error['value'] < ( $install_time + 300 )
947 ) {
948 // the first sync might right after the installation
949 continue;
950 }
951
952 // we only consider plugin_updates as errors only if there are still outstanding updates
953 $is_plugin_update_error = ! empty( $error['name'] ) && 'plugin_update' === $error['name']
954 && ! empty( $outstanding_updates );
955
956 $skip_plugin_update = ! empty( $error['name'] ) && 'plugin_update' === $error['name']
957 && empty( $outstanding_updates );
958
959 if ( empty( $error['comment'] ) && '0' !== $error['comment'] ) {
960 $error['comment'] = '';
961 }
962
963 if ( strpos( $error['comment'], '<head>' ) ) {
964 $error['comment'] = esc_html( $error['comment'] );
965 $error['comment'] = $this->replace_hexadecimal_colors( $error['comment'] );
966 }
967
968 $error['value'] = $this->convert_time_to_date( $error['value'], true, false );
969 $error['is_warning'] = ! empty( $error['name'] ) && stripos( $error['name'], 'archiv' ) !== false && 'archive_boot' !== $error['name'];
970 $error['is_error'] = $is_plugin_update_error;
971 if ( $is_plugin_update_error ) {
972 $error['comment'] = sprintf( esc_html__( 'Please reach out to us and include the copied system report (%s)<br><br>You can also retry the update manually by clicking in the top on the "Troubleshooting" tab and then clicking on the "Run updater" button.', 'matomo' ), sprintf( '<a href="%s" target="_blank">%s</a>', 'https://matomo.org/faq/wordpress/how-do-i-troubleshoot-a-failed-database-upgrade-in-matomo-for-wordpress/', esc_html__( 'more info', 'matomo' ) ) ) . $error['comment'];
973 } elseif ( $skip_plugin_update ) {
974 $error['comment'] = esc_html__( 'As there are no outstanding plugin updates it looks like this log can be ignored.', 'matomo' ) . '<br><br>' . $error['comment'];
975 }
976 $error['comment'] = matomo_anonymize_value( $error['comment'] );
977 $rows[] = $error;
978 }
979
980 foreach ( $error_log_entries as $error ) {
981 if ( $suports_async
982 && ! empty( $error['value'] )
983 && is_string( $error['value'] )
984 && strpos( $error['value'], __( 'Your PHP installation appears to be missing the MySQL extension which is required by WordPress.', 'matomo' ) ) > 0
985 ) {
986 $rows[] = [
987 'name' => 'Cli has no MySQL',
988 'value' => true,
989 'comment' => sprintf( esc_html__( 'It looks like MySQL is not available on CLI. Please read our FAQ on how to %s', 'matomo' ), sprintf( ' <a href="%s" target="_blank">%s</a>', 'https://matomo.org/faq/wordpress/how-do-i-fix-the-error-your-php-installation-appears-to-be-missing-the-mysql-extension-which-is-required-by-wordpress-in-matomo-system-report/', esc_html__( 'fix this issue', 'matomo' ) ) ),
990 'is_error' => true,
991 ];
992 }
993 }
994 } else {
995 $rows[] = [
996 'name' => __( 'None', 'matomo' ),
997 'value' => '',
998 'comment' => '',
999 ];
1000 }
1001
1002 if ( ! WpMatomo::is_safe_mode() ) {
1003 Bootstrap::do_bootstrap();
1004 $trackfailures = [];
1005 try {
1006 $tracking_failures = new Failures();
1007 $trackfailures = $tracking_failures->getAllFailures();
1008 // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
1009 } catch ( Exception $e ) {
1010 // ignored in case not set up yet etc.
1011 }
1012 if ( ! empty( $trackfailures ) ) {
1013 $rows[] = [
1014 'section' => 'Tracking failures',
1015 ];
1016 foreach ( $trackfailures as $failure ) {
1017 $comment = sprintf(
1018 'Solution: %s<br>More info: %s<br>Date: %s<br>Request URL: %s',
1019 $failure['solution'],
1020 $failure['solution_url'],
1021 $failure['pretty_date_first_occurred'],
1022 $failure['request_url']
1023 );
1024 // do not esc_html the comment: we want the br
1025 $rows[] = [
1026 'name' => $failure['problem'],
1027 'is_warning' => true,
1028 'value' => '',
1029 'comment' => $comment,
1030 ];
1031 }
1032 }
1033 }
1034
1035 return $rows;
1036 }
1037
1038 private function had_visits_in_last_days( $num_days ) {
1039 global $wpdb;
1040
1041 if ( WpMatomo::is_safe_mode() ) {
1042 return null;
1043 }
1044
1045 $days_in_seconds = $num_days * 86400;
1046
1047 $prefix_table = $this->db_settings->prefix_table_name( 'log_visit' );
1048
1049 $suppress_errors = $wpdb->suppress_errors;
1050 $wpdb->suppress_errors( true );// prevent any of this showing in logs just in case
1051
1052 try {
1053 $time = gmdate( 'Y-m-d H:i:s', time() - $days_in_seconds );
1054 $sql = $wpdb->prepare( 'SELECT idsite from ' . $prefix_table . ' where visit_last_action_time > %s LIMIT 1', $time );
1055 $row = $wpdb->get_var( $sql );
1056 } catch ( Exception $e ) {
1057 $row = null;
1058 }
1059
1060 $wpdb->suppress_errors( $suppress_errors );
1061 // we need to differentiate between
1062 // 0 === had no visit
1063 // 1 === had visit
1064 // null === sum error... eg table was not correctly installed
1065 if ( null !== $row ) {
1066 $row = ! empty( $row );
1067 }
1068
1069 return $row;
1070 }
1071
1072 private function convert_time_to_date( $time, $in_blog_timezone, $print_diff = false ) {
1073 if ( empty( $time ) ) {
1074 return esc_html__( 'Unknown', 'matomo' );
1075 }
1076
1077 $date = gmdate( 'Y-m-d H:i:s', (int) $time );
1078
1079 if ( $in_blog_timezone ) {
1080 $date = get_date_from_gmt( $date, 'Y-m-d H:i:s' );
1081 }
1082
1083 if ( $print_diff && class_exists( '\Piwik\Metrics\Formatter' ) ) {
1084 try {
1085 $formatter = new \Piwik\Metrics\Formatter();
1086 $date .= ' (' . $formatter->getPrettyTimeFromSeconds( $time - time(), true, false ) . ')';
1087 } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
1088 // empty
1089 }
1090 }
1091
1092 return $date;
1093 }
1094
1095 private function add_diagnostic_results( $rows, $results ) {
1096 foreach ( $results as $result ) {
1097 $comment = '';
1098 /** @var DiagnosticResult $result */
1099 if ( $result->getStatus() !== DiagnosticResult::STATUS_OK ) {
1100 foreach ( $result->getItems() as $item ) {
1101 $item_comment = $item->getComment();
1102 if ( ! empty( $item_comment ) && is_string( $item_comment ) ) {
1103 if ( stripos( $item_comment, 'core:archive' ) > 0 ) {
1104 // we only want to keep the first sentence like " Archiving last ran successfully on Wednesday, January 2, 2019 00:00:00 which is 335 days 20:08:11 ago"
1105 // but not anything that asks user to set up a cronjob
1106 $item_comment = substr( $item_comment, 0, stripos( $item_comment, 'core:archive' ) );
1107 if ( strpos( $item_comment, '.' ) > 0 ) {
1108 $item_comment = substr( $item_comment, 0, strripos( $item_comment, '.' ) );
1109 } else {
1110 $item_comment = 'Archiving hasn\'t run in a while.';
1111 }
1112 }
1113 $comment .= $item_comment . '<br/>';
1114 }
1115 }
1116 }
1117
1118 $rows[] = [
1119 'name' => $result->getLabel(),
1120 'value' => $result->getStatus() . ' ' . $result->getLongErrorMessage(),
1121 'comment' => $comment,
1122 'is_warning' => $result->getStatus() === DiagnosticResult::STATUS_WARNING,
1123 'is_error' => $result->getStatus() === DiagnosticResult::STATUS_ERROR,
1124 ];
1125 }
1126
1127 return $rows;
1128 }
1129
1130 private function get_wordpress_info() {
1131 $is_multi_site = is_multisite();
1132 $num_blogs = 1;
1133 $is_network_enabled = false;
1134
1135 if ( $is_multi_site ) {
1136 if ( function_exists( 'get_blog_count' ) ) {
1137 $num_blogs = get_blog_count();
1138 }
1139 $settings = new Settings();
1140 $is_network_enabled = $settings->is_network_enabled();
1141 }
1142
1143 $rows = [];
1144 $rows[] = [
1145 'name' => 'Home URL',
1146 'value' => home_url(),
1147 ];
1148 $rows[] = [
1149 'name' => 'Site URL',
1150 'value' => site_url(),
1151 ];
1152 $rows[] = [
1153 'name' => 'WordPress Version',
1154 'value' => get_bloginfo( 'version' ),
1155 ];
1156 $rows[] = [
1157 'name' => 'Number of blogs',
1158 'value' => $num_blogs,
1159 ];
1160 $rows[] = [
1161 'name' => 'Multisite Enabled',
1162 'value' => $is_multi_site,
1163 ];
1164 $rows[] = [
1165 'name' => 'Network Enabled',
1166 'value' => $is_network_enabled,
1167 ];
1168 $consts = [
1169 'WP_DEBUG',
1170 'WP_DEBUG_DISPLAY',
1171 'WP_DEBUG_LOG',
1172 'DISABLE_WP_CRON',
1173 'FORCE_SSL_ADMIN',
1174 'WP_CACHE',
1175 'CONCATENATE_SCRIPTS',
1176 'COMPRESS_SCRIPTS',
1177 'COMPRESS_CSS',
1178 'ENFORCE_GZIP',
1179 'WP_LOCAL_DEV',
1180 'WP_CONTENT_URL',
1181 'WP_CONTENT_DIR',
1182 'UPLOADS',
1183 'BLOGUPLOADDIR',
1184 'DIEONDBERROR',
1185 'WPLANG',
1186 'ALTERNATE_WP_CRON',
1187 'WP_CRON_LOCK_TIMEOUT',
1188 'WP_DISABLE_FATAL_ERROR_HANDLER',
1189 'MATOMO_SUPPORT_ASYNC_ARCHIVING',
1190 'MATOMO_ENABLE_TAG_MANAGER',
1191 'MATOMO_SUPPRESS_DB_ERRORS',
1192 'MATOMO_ENABLE_AUTO_UPGRADE',
1193 'MATOMO_DEBUG',
1194 'MATOMO_SAFE_MODE',
1195 'MATOMO_GLOBAL_UPLOAD_DIR',
1196 'MATOMO_LOGIN_REDIRECT',
1197 ];
1198 foreach ( $consts as $const ) {
1199 $rows[] = [
1200 'name' => $const,
1201 'value' => defined( $const ) ? constant( $const ) : '-',
1202 ];
1203 }
1204
1205 $rows[] = [
1206 'name' => 'Permalink Structure',
1207 'value' => get_option( 'permalink_structure' ) ? get_option( 'permalink_structure' ) : 'Default',
1208 ];
1209
1210 $rows[] = [
1211 'name' => 'Possibly uses symlink',
1212 'value' => strpos( __DIR__, ABSPATH ) === false && strpos( __DIR__, WP_CONTENT_DIR ) === false,
1213 ];
1214
1215 $upload_dir = wp_upload_dir();
1216 $rows[] = [
1217 'name' => 'Upload base url',
1218 'value' => $upload_dir['baseurl'],
1219 ];
1220
1221 $rows[] = [
1222 'name' => 'Upload base dir',
1223 'value' => $upload_dir['basedir'],
1224 ];
1225
1226 $rows[] = [
1227 'name' => 'Upload url',
1228 'value' => $upload_dir['url'],
1229 ];
1230
1231 foreach ( [ 'upload_path', 'upload_url_path' ] as $option_read ) {
1232 $rows[] = [
1233 'name' => 'Custom ' . $option_read,
1234 'value' => get_option( $option_read ),
1235 ];
1236 }
1237
1238 if ( is_plugin_active( 'wp-piwik/wp-piwik.php' ) ) {
1239 $rows[] = [
1240 'name' => 'Connect Matomo (WP-Matomo) activated',
1241 'value' => true,
1242 'is_warning' => true,
1243 'comment' => sprintf( esc_html__( 'It is usually not recommended or needed to run Matomo for WordPress and Connect Matomo at the same time. To learn more about the differences between the two plugins view this %s', 'matomo' ), sprintf( '<a href="%s" target="_blank">%s</a>', 'https://matomo.org/faq/wordpress/why-are-there-two-different-matomo-for-wordpress-plugins-what-is-the-difference-to-wp-matomo-integration-plugin/', esc_html__( 'URL', 'matomo' ) ) ),
1244 ];
1245
1246 $mode = get_option( 'wp-piwik_global-piwik_mode' );
1247 if ( function_exists( 'get_site_option' ) && is_plugin_active_for_network( 'wp-piwik/wp-piwik.php' ) ) {
1248 $mode = get_site_option( 'wp-piwik_global-piwik_mode' );
1249 }
1250 if ( ! empty( $mode ) ) {
1251 $rows[] = [
1252 'name' => 'Connect Matomo mode',
1253 'value' => $mode,
1254 'is_warning' => 'php' === $mode || 'PHP' === $mode,
1255 'comment' => esc_html__( 'Connect Matomo is configured in "PHP mode". This is known to cause issues with Matomo for WordPress. We recommend you either deactivate Connect Matomo or you go "Settings => Connect Matomo" and change the "Matomo Mode" in the "Connect to Matomo" section to "Self-hosted HTTP API".', 'matomo' ),
1256 ];
1257 }
1258 }
1259
1260 $compatible_content_dir = matomo_has_compatible_content_dir();
1261 if ( true === $compatible_content_dir ) {
1262 $rows[] = [
1263 'name' => 'Compatible content directory',
1264 'value' => true,
1265 ];
1266 } else {
1267 $rows[] = [
1268 'name' => 'Compatible content directory',
1269 'value' => $compatible_content_dir,
1270 'is_warning' => true,
1271 'comment' => esc_html__( 'It looks like you are maybe using a custom WordPress content directory. The Matomo reporting/admin pages might not work. You may be able to workaround this.', 'matomo' ) . ' ' . sprintf( '<a href="%s" target="_blank">%s</a>', 'https://matomo.org/faq/wordpress/how-do-i-make-matomo-for-wordpress-work-when-i-have-a-custom-content-directory/', esc_html__( 'Learn more', 'matomo' ) ),
1272 ];
1273 }
1274
1275 $paths = new Paths();
1276 $paths->get_file_system();
1277
1278 $filesystem_init_succeeded = $paths->get_host_init_filesystem_succeeded();
1279 $rows[] = [
1280 'name' => 'WP_Filesystem Initialized',
1281 'value' => $filesystem_init_succeeded,
1282 'is_warning' => ! $filesystem_init_succeeded,
1283 'comment' => $filesystem_init_succeeded
1284 ? null
1285 : esc_html__( 'The WordPress Filesystem abstraction was not initialized correctly (WP_Filesystem() returned false). This indicates a WordPress or server configuration issue which may cause problems in Matomo and other plugins. To resolve it, contact your hosting provider.', 'matomo' ),
1286 ];
1287
1288 return $rows;
1289 }
1290
1291 private function add_maxminddb_row( &$rows, $maxmind_db_loaded ) {
1292 $rows[] = [
1293 'name' => esc_html__( 'PHP Maxmind DB extension', 'matomo' ),
1294 'value' => $maxmind_db_loaded ? __( 'Loaded', 'matomo' ) : __( 'Not loaded', 'matomo' ),
1295 'comment' => $maxmind_db_loaded ? sprintf( esc_html__( 'You may encounter %s', 'matomo' ), sprintf( '<a href="%s" target="_blank">%s</a>', 'https://matomo.org/faq/troubleshooting/how-do-i-fix-the-error-call-to-undefined-method-maxminddbreadergetwithprefixlen/', esc_html__( 'the following problem', 'matomo' ) ) ) : '',
1296 'is_warning' => $maxmind_db_loaded,
1297 ];
1298 }
1299
1300 private function get_server_info() {
1301 $rows = [];
1302
1303 if ( ! empty( $_SERVER['SERVER_SOFTWARE'] ) ) {
1304 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1305 $server_software = sanitize_text_field( $_SERVER['SERVER_SOFTWARE'] );
1306 $rows[] = [
1307 'name' => 'Server Info',
1308 'value' => $server_software,
1309 ];
1310 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1311 if ( strpos( $server_software, 'Apache' ) !== false ) {
1312 $url = plugins_url( 'app', MATOMO_ANALYTICS_FILE ) . '/index.php';
1313 $result = wp_remote_post(
1314 $url,
1315 array(
1316 'method' => 'GET',
1317 'sslverify' => false,
1318 'timeout' => defined( 'MATOMO_LOCAL_ENVIRONMENT' ) && MATOMO_LOCAL_ENVIRONMENT ? 5 : 2,
1319 )
1320 );
1321 if ( is_array( $result ) ) {
1322 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
1323 $file_content = file_get_contents( dirname( MATOMO_ANALYTICS_FILE ) . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . '.htaccess' );
1324 if ( strpos( $file_content, 'AddHandler' ) && ! strpos( $file_content, '# AddHandler' ) ) {
1325 switch ( (int) $result['response']['code'] ) {
1326 case 500:
1327 $value = __( 'To be confirmed', 'matomo' );
1328 $comment = sprintf( esc_html__( 'The AddHandler Apache directive maybe disabled. If you get a 500 error code when accessing Matomo, please read this %s', 'matomo' ), sprintf( '<a href="%s" target="_blank">%s<a/>', 'https://matomo.org/faq/wordpress/how-do-i-fix-the-error-addhandler-not-allowed-here/', esc_html__( 'FAQ', 'matomo' ) ) );
1329 $is_error = true;
1330 break;
1331 default:
1332 $value = __( 'Supported', 'matomo' );
1333 $comment = '';
1334 $is_error = false;
1335 }
1336 $rows[] = [
1337 'name' => 'Apache AddHandler support',
1338 'value' => $value,
1339 'comment' => $comment,
1340 'is_error' => $is_error,
1341 ];
1342 }
1343 }
1344 }
1345 }
1346
1347 if ( PHP_OS ) {
1348 $rows[] = [
1349 'name' => 'PHP OS',
1350 'value' => PHP_OS,
1351 ];
1352 }
1353 $rows[] = [
1354 'name' => 'PHP Version',
1355 'value' => phpversion(),
1356 ];
1357 $rows[] = [
1358 'name' => 'PHP SAPI',
1359 'value' => php_sapi_name(),
1360 ];
1361 if ( defined( 'PHP_BINARY' ) && PHP_BINARY ) {
1362 $rows[] = [
1363 'name' => 'PHP Binary Name',
1364 'value' => PHP_BINARY,
1365 ];
1366 }
1367
1368 $this->add_maxminddb_row( $rows, extension_loaded( 'maxminddb' ) );
1369
1370 // we report error reporting before matomo bootstraped and after to see if Matomo changed it successfully etc
1371 $rows[] = [
1372 'name' => 'PHP Error Reporting',
1373 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting
1374 'value' => $this->initial_error_reporting . ' After bootstrap: ' . @error_reporting(),
1375 ];
1376
1377 if ( ! empty( $this->get_php_cli_binary() ) ) {
1378 $rows[] = [
1379 'name' => 'PHP Found Binary',
1380 'value' => $this->get_php_cli_binary(),
1381 ];
1382 }
1383 $rows[] = [
1384 'name' => 'Timezone',
1385 'value' => date_default_timezone_get(),
1386 ];
1387 if ( function_exists( 'wp_timezone_string' ) ) {
1388 $rows[] = [
1389 'name' => 'WP timezone',
1390 'value' => wp_timezone_string(),
1391 ];
1392 }
1393 $rows[] = [
1394 'name' => 'Locale',
1395 'value' => get_locale(),
1396 ];
1397 if ( function_exists( 'get_user_locale' ) ) {
1398 $rows[] = [
1399 'name' => 'User Locale',
1400 'value' => get_user_locale(),
1401 ];
1402 }
1403
1404 $rows[] = [
1405 'name' => 'Memory Limit',
1406 'value' => @ini_get( 'memory_limit' ),
1407 'comment' => sprintf( esc_html__( 'At least %1$dMB recommended. Depending on your traffic %2$dMB or more may be needed.', 'matomo' ), 128, 256 ),
1408 ];
1409
1410 $rows[] = [
1411 'name' => 'WP Memory Limit',
1412 'value' => defined( 'WP_MEMORY_LIMIT' ) ? WP_MEMORY_LIMIT : '',
1413 'comment' => '',
1414 ];
1415
1416 $rows[] = [
1417 'name' => 'WP Max Memory Limit',
1418 'value' => defined( 'WP_MAX_MEMORY_LIMIT' ) ? WP_MAX_MEMORY_LIMIT : '',
1419 'comment' => '',
1420 ];
1421
1422 if ( function_exists( 'timezone_version_get' ) ) {
1423 $rows[] = [
1424 'name' => 'Timezone version',
1425 'value' => timezone_version_get(),
1426 ];
1427 }
1428
1429 $rows[] = [
1430 'name' => 'Time',
1431 'value' => time(),
1432 ];
1433
1434 $rows[] = [
1435 'name' => 'Max Execution Time',
1436 'value' => ini_get( 'max_execution_time' ),
1437 ];
1438 $rows[] = [
1439 'name' => 'Max Post Size',
1440 'value' => ini_get( 'post_max_size' ),
1441 ];
1442 $rows[] = [
1443 'name' => 'Max Upload Size',
1444 'value' => wp_max_upload_size(),
1445 ];
1446 $rows[] = [
1447 'name' => 'Max Input Vars',
1448 'value' => ini_get( 'max_input_vars' ),
1449 ];
1450
1451 $disabled_functions = ini_get( 'disable_functions' );
1452 $rows[] = [
1453 'name' => 'Disabled PHP functions',
1454 'value' => ! empty( $disabled_functions ),
1455 'comment' => ! empty( $disabled_functions ) ? $disabled_functions : '',
1456 ];
1457
1458 $zlib_compression = ini_get( 'zlib.output_compression' );
1459 $row = [
1460 'name' => 'zlib.output_compression is off',
1461 'value' => '1' !== $zlib_compression,
1462 ];
1463
1464 if ( '1' === $zlib_compression ) {
1465 $row['is_error'] = true;
1466 $row['comment'] = esc_html__( 'You need to set "zlib.output_compression" in your php.ini to "Off".', 'matomo' );
1467 }
1468 $rows[] = $row;
1469
1470 if ( function_exists( 'curl_version' ) ) {
1471 $curl_version = curl_version();
1472 $curl_version = $curl_version['version'] . ', ' . $curl_version['ssl_version'];
1473 $rows[] = [
1474 'name' => 'Curl Version',
1475 'value' => $curl_version,
1476 ];
1477 }
1478
1479 $suhosin_installed = ( extension_loaded( 'suhosin' ) || ( defined( 'SUHOSIN_PATCH' ) && constant( 'SUHOSIN_PATCH' ) ) );
1480 $rows[] = [
1481 'name' => 'Suhosin installed',
1482 'value' => ! empty( $suhosin_installed ),
1483 'comment' => '',
1484 ];
1485
1486 return $rows;
1487 }
1488
1489 private function get_browser_info() {
1490 $rows = [];
1491
1492 if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
1493 $rows[] = [
1494 'name' => 'Browser',
1495 'value' => '',
1496 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1497 'comment' => sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ),
1498 ];
1499 }
1500 if ( ! WpMatomo::is_safe_mode() ) {
1501 Bootstrap::do_bootstrap();
1502 try {
1503 if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
1504 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1505 $detector = StaticContainer::get( DeviceDetectorFactory::class )->makeInstance( sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ) );
1506 $client = $detector->getClient();
1507 if ( ! empty( $client['name'] ) && 'Microsoft Edge' === $client['name'] && (int) $client['version'] >= 85 ) {
1508 $rows[] = [
1509 'name' => 'Browser Compatibility',
1510 'is_warning' => true,
1511 'value' => 'Yes',
1512 'comment' => esc_html__( 'Because you are using MS Edge browser, you may see a warning like "This site has been reported as unsafe" from "Microsoft Defender SmartScreen" when you view the Matomo Reporting, Admin or Tag Manager page. This is a false alert and you can safely ignore this warning by clicking on the icon next to the URL (in the address bar) and choosing either "Report as safe" (preferred) or "Show unsafe content". We are hoping to get this false warning removed in the future.', 'matomo' ),
1513 ];
1514 }
1515 }
1516 } catch ( Exception $e ) {
1517 $rows[] = [
1518 'name' => 'Browser Compatibility',
1519 'is_error' => true,
1520 'value' => 'Failed to use Device Detector.',
1521 'comment' => $e->getMessage(),
1522 ];
1523 }
1524
1525 $rows[] = [
1526 'name' => 'Language',
1527 'value' => Common::getBrowserLanguage(),
1528 'comment' => '',
1529 ];
1530 }
1531
1532 return $rows;
1533 }
1534
1535 private function get_db_info() {
1536 global $wpdb;
1537 $rows = [];
1538
1539 $rows[] = [
1540 'name' => 'MySQL Version',
1541 'value' => ! empty( $wpdb->is_mysql ) ? $wpdb->db_version() : '',
1542 'comment' => '',
1543 ];
1544
1545 $rows[] = [
1546 'name' => 'Mysqli Connect',
1547 'value' => function_exists( 'mysqli_connect' ),
1548 'comment' => '',
1549 ];
1550 $rows[] = [
1551 'name' => 'Force MySQL over Mysqli',
1552 'value' => defined( 'WP_USE_EXT_MYSQL' ) && WP_USE_EXT_MYSQL,
1553 'comment' => '',
1554 ];
1555
1556 $rows[] = [
1557 'name' => 'DB Prefix',
1558 'value' => $wpdb->prefix,
1559 ];
1560
1561 $rows[] = [
1562 'name' => 'DB CHARSET',
1563 'value' => defined( 'DB_CHARSET' ) ? DB_CHARSET : '',
1564 ];
1565
1566 $rows[] = [
1567 'name' => 'DB COLLATE',
1568 'value' => defined( 'DB_COLLATE' ) ? DB_COLLATE : '',
1569 ];
1570
1571 $rows[] = [
1572 'name' => 'SHOW ERRORS',
1573 'value' => ! empty( $wpdb->show_errors ),
1574 ];
1575
1576 $rows[] = [
1577 'name' => 'SUPPRESS ERRORS',
1578 'value' => ! empty( $wpdb->suppress_errors ),
1579 ];
1580
1581 if ( method_exists( $wpdb, 'parse_db_host' ) ) {
1582 $host_data = $wpdb->parse_db_host( DB_HOST );
1583 if ( $host_data ) {
1584 list( $host, $port, $socket, $is_ipv6 ) = $host_data;
1585 }
1586
1587 $rows[] = [
1588 'name' => 'Uses Socket',
1589 'value' => ! empty( $socket ),
1590 ];
1591 $rows[] = [
1592 'name' => 'Uses IPv6',
1593 'value' => ! empty( $is_ipv6 ),
1594 ];
1595 }
1596
1597 $rows[] = [
1598 'name' => 'Matomo tables found',
1599 'value' => $this->get_num_matomo_tables(),
1600 ];
1601
1602 $missing_tables = $this->get_missing_tables();
1603 $has_missing_tables = ( count( $missing_tables ) > 0 );
1604 $rows[] = [
1605 'name' => 'DB tables exist',
1606 'value' => ( ! $has_missing_tables ),
1607 'comment' => $has_missing_tables ? sprintf( esc_html__( 'Some tables may be missing: %s', 'matomo' ), implode( ', ', $missing_tables ) ) : '',
1608 'is_error' => $has_missing_tables,
1609 ];
1610
1611 foreach ( [ 'user', 'site' ] as $table ) {
1612 $row = [
1613 'name' => 'Matomo ' . $table . 's found',
1614 'value' => $this->get_num_entries_in_table( $table ),
1615 ];
1616 if ( 'site' === $table ) {
1617 if ( ( ! is_multisite() ) && ( $row['value'] > 1 ) ) {
1618 $row['is_warning'] = true;
1619 $row['comment'] = esc_html__( 'There is an error in your Matomo records. Please contact wordpress@matomo.org', 'matomo' );
1620 }
1621 }
1622 $rows[] = $row;
1623 }
1624
1625 $grants = $this->get_db_grants();
1626
1627 // we only show these grants for security reasons as only they are needed and we don't need to know any other ones
1628 $needed_grants = [
1629 'SELECT',
1630 'INSERT',
1631 'UPDATE',
1632 'INDEX',
1633 'DELETE',
1634 'CREATE',
1635 'DROP',
1636 'ALTER',
1637 'CREATE TEMPORARY TABLES',
1638 'LOCK TABLES',
1639 ];
1640 if ( in_array( 'ALL PRIVILEGES', $grants, true ) ) {
1641 // ALL PRIVILEGES may be used pre MySQL 8.0
1642 $grants = $needed_grants;
1643 }
1644
1645 $grants_missing = array_diff( $needed_grants, $grants );
1646
1647 if ( empty( $grants )
1648 || ! is_array( $grants )
1649 || count( $grants_missing ) === count( $needed_grants )
1650 ) {
1651 $rows[] = [
1652 'name' => esc_html__( 'Required permissions', 'matomo' ),
1653 'value' => esc_html__( 'Failed to detect granted permissions', 'matomo' ),
1654 'comment' => esc_html__( 'Please check your MySQL user has these permissions (grants):', 'matomo' ) . '<br />' . implode( ', ', $needed_grants ),
1655 'is_warning' => false,
1656 ];
1657 } else {
1658 if ( ! empty( $grants_missing ) ) {
1659 $rows[] = [
1660 'name' => esc_html__( 'Required permissions', 'matomo' ),
1661 'value' => esc_html__( 'Error', 'matomo' ),
1662 'comment' => esc_html__( 'Missing permissions', 'matomo' ) . ': ' . implode( ', ', $grants_missing ) . '. ' . esc_html__( 'Please check if any of these MySQL permission (grants) are missing and add them if needed.', 'matomo' ) . ' ' . sprintf( '<a href="https://matomo.org/faq/troubleshooting/how-do-i-check-if-my-mysql-user-has-all-required-grants/" target="_blank">%s</a>', __( 'Learn more', 'matomo' ) ),
1663 'is_warning' => true,
1664 ];
1665 } else {
1666 $rows[] = [
1667 'name' => esc_html__( 'Required permissions', 'matomo' ),
1668 'value' => esc_html__( 'OK', 'matomo' ),
1669 'comment' => '',
1670 'is_warning' => false,
1671 ];
1672 }
1673 }
1674
1675 return $rows;
1676 }
1677
1678 /**
1679 * @return string[]
1680 */
1681 public function get_missing_tables() {
1682 global $wpdb;
1683
1684 $required_matomo_tables = $this->db_settings->get_matomo_tables();
1685 $required_matomo_tables = array_map( [ $this->db_settings, 'prefix_table_name' ], $required_matomo_tables );
1686
1687 $existing_tables = [];
1688 try {
1689 $prefix = $this->db_settings->prefix_table_name( '' );
1690 $existing_tables = $wpdb->get_col( 'SHOW TABLES LIKE "' . $prefix . '%"' );
1691 } catch ( Exception $e ) {
1692 $this->logger->log( 'no show tables: ' . $e->getMessage() );
1693 }
1694
1695 return array_diff( $required_matomo_tables, $existing_tables );
1696 }
1697
1698 private function get_num_entries_in_table( $table ) {
1699 global $wpdb;
1700
1701 $prefix = $this->db_settings->prefix_table_name( $table );
1702
1703 $results = null;
1704 try {
1705 $results = $wpdb->get_var( 'select count(*) from ' . $prefix );
1706 } catch ( Exception $e ) {
1707 $this->logger->log( 'no count(*): ' . $e->getMessage() );
1708 }
1709
1710 if ( isset( $results ) && is_numeric( $results ) ) {
1711 return $results;
1712 }
1713
1714 return 'table not exists';
1715 }
1716
1717 private function get_num_matomo_tables() {
1718 global $wpdb;
1719
1720 $prefix = $this->db_settings->prefix_table_name( '' );
1721
1722 $results = null;
1723 try {
1724 $results = $wpdb->get_results( 'show tables like "' . $prefix . '%"' );
1725 } catch ( Exception $e ) {
1726 $this->logger->log( 'no show tables: ' . $e->getMessage() );
1727 }
1728
1729 if ( is_array( $results ) ) {
1730 return count( $results );
1731 }
1732
1733 return 'show tables not working';
1734 }
1735
1736 private function get_db_grants() {
1737 global $wpdb;
1738
1739 $suppress_errors = $wpdb->suppress_errors;
1740 $wpdb->suppress_errors( true );// prevent any of this showing in logs just in case
1741
1742 try {
1743 $values = $wpdb->get_results( 'SHOW GRANTS', ARRAY_N );
1744 } catch ( Exception $e ) {
1745 // We ignore any possible error in case of permission or not supported etc.
1746 $values = [];
1747 }
1748
1749 $wpdb->suppress_errors( $suppress_errors );
1750
1751 $grants = [];
1752 foreach ( $values as $index => $value ) {
1753 if ( empty( $value[0] ) || ! is_string( $value[0] ) ) {
1754 continue;
1755 }
1756
1757 if ( stripos( $value[0], 'ALL PRIVILEGES' ) !== false ) {
1758 return [ 'ALL PRIVILEGES' ]; // the split on empty string wouldn't work otherwise
1759 }
1760
1761 foreach ( [ ' ON ', ' TO ', ' IDENTIFIED ', ' BY ' ] as $keyword ) {
1762 if ( stripos( $values[ $index ][0], $keyword ) !== false ) {
1763 // make sure to never show by any accident a db user or password by cutting anything after on/to
1764 $values[ $index ][0] = substr( $values[ $index ][0], 0, stripos( $values[ $index ][0], $keyword ) );
1765 }
1766 if ( stripos( $values[ $index ][0], 'GRANT' ) !== false ) {
1767 // otherwise we end up having "grant select"... instead of just "select"
1768 $values[ $index ][0] = substr( $values[ $index ][0], stripos( $values[ $index ][0], 'GRANT' ) + 5 );
1769 }
1770 }
1771 // make sure to never show by any accident a db user or password
1772 $values[ $index ][0] = str_replace(
1773 [ DB_USER, DB_PASSWORD ],
1774 [
1775 'DB_USER',
1776 'DB_PASS',
1777 ],
1778 $values[ $index ][0]
1779 );
1780
1781 $grants = array_merge( $grants, explode( ',', $values[ $index ][0] ) );
1782 }
1783 $grants = array_map( 'trim', $grants );
1784 $grants = array_map( 'strtoupper', $grants );
1785 $grants = array_unique( $grants );
1786
1787 return $grants;
1788 }
1789
1790 /**
1791 * @return string[]
1792 */
1793 private function get_actives_plugins() {
1794 $active_plugins = get_option( 'active_plugins', [] );
1795 if ( ! empty( $active_plugins ) && is_array( $active_plugins ) ) {
1796 $plugins = get_plugins();
1797 $active_plugins = array_map(
1798 function ( $active_plugin ) use ( $plugins ) {
1799 $plugin_version = isset( $plugins[ $active_plugin ]['Version'] ) ? $plugins[ $active_plugin ]['Version'] : null;
1800 $result_suffix = $plugin_version ? ( ':' . $plugin_version ) : '';
1801 $parts = explode( '/', trim( $active_plugin ) );
1802 return trim( $parts[0] ) . $result_suffix;
1803 },
1804 $active_plugins
1805 );
1806 }
1807
1808 return $active_plugins;
1809 }
1810
1811 private function get_plugins_info() {
1812 $rows = [];
1813 $mu_plugins = get_mu_plugins();
1814
1815 if ( ! empty( $mu_plugins ) ) {
1816 $rows[] = [
1817 'section' => 'MU Plugins',
1818 ];
1819
1820 foreach ( $mu_plugins as $mu_pin ) {
1821 $comment = '';
1822 if ( ! empty( $plugin['Network'] ) ) {
1823 $comment = esc_html__( 'Network enabled', 'matomo' );
1824 }
1825 $rows[] = [
1826 'name' => $mu_pin['Name'],
1827 'value' => $mu_pin['Version'],
1828 'comment' => $comment,
1829 ];
1830 }
1831
1832 $rows[] = [
1833 'section' => 'Plugins',
1834 ];
1835 }
1836
1837 $plugins = get_plugins();
1838
1839 foreach ( $plugins as $plugin_file => $plugin ) {
1840 $comment = '';
1841 if ( ! empty( $plugin['Network'] ) ) {
1842 $comment = esc_html__( 'Network enabled', 'matomo' );
1843 }
1844
1845 if ( strpos( $plugin_file, 'ninjafirewall/' ) === 0 ) {
1846 $comment .= '<br/><br/>' . esc_html__( 'We noticed you are using Matomo with Ninja Firewall. This can result in Matomo cache file changes showing up in Ninja Firewall which likely undesired.', 'matomo' ) . '
1847 <a href="https://matomo.org/faq/wordpress/how-do-i-prevent-matomo-cache-file-changes-to-show-up-in-ninja-firewall/" rel="noreferrer noopener" target="_blank">' . esc_html__( 'Read our FAQ to learn how to prevent these entries.', 'matomo' ) . '</a>';
1848 }
1849
1850 $rows[] = [
1851 'name' => $plugin['Name'],
1852 'value' => $plugin['Version'],
1853 'comment' => $comment,
1854 ];
1855 }
1856
1857 $active_plugins_with_version = $this->get_actives_plugins();
1858
1859 if ( ! empty( $active_plugins_with_version ) && is_array( $active_plugins_with_version ) ) {
1860 $rows[] = [
1861 'name' => 'Active Plugins',
1862 'value' => count( $active_plugins_with_version ),
1863 'comment' => implode( ' ', $active_plugins_with_version ),
1864 ];
1865
1866 $active_plugins = array_map(
1867 function ( $plugin_with_version ) {
1868 $parts = explode( ':', $plugin_with_version );
1869 return $parts[0];
1870 },
1871 $active_plugins_with_version
1872 );
1873
1874 $used_not_compatible = array_intersect( $active_plugins, $this->not_compatible_plugins );
1875 if ( ! empty( $used_not_compatible ) ) {
1876 $additional_comment = '';
1877 if ( count( $used_not_compatible ) ) {
1878 $rows[] = [
1879 'name' => __( 'Not compatible plugins', 'matomo' ),
1880 'value' => count( $used_not_compatible ),
1881 'comment' => implode( ', ', $used_not_compatible ) . '<br><br>' . sprintf( esc_html__( 'Matomo may work fine when using these plugins but there may be some issues. For more information %1$sSee %2$s', 'matomo' ), '<br/>', sprintf( '<a href="%s" target="_blank">%s</a>', 'https://matomo.org/faq/wordpress/which-plugins-is-matomo-for-wordpress-known-to-be-not-compatible-with/', esc_html__( 'this FAQ', 'matomo' ) ) ) . $additional_comment,
1882 'is_warning' => true,
1883 'is_error' => false,
1884 ];
1885 }
1886 }
1887 }
1888
1889 $rows[] = [
1890 'name' => 'Theme',
1891 'value' => function_exists( 'get_template' ) ? get_template() : '',
1892 'comment' => get_option( 'stylesheet' ),
1893 ];
1894
1895 if ( is_plugin_active( 'better-wp-security/better-wp-security.php' ) ) {
1896 if ( class_exists( 'ITSEC_Modules' ) ) {
1897 if ( method_exists( '\ITSEC_Modules', 'get_setting' ) ) {
1898 $input = ITSEC_Modules::get_settings( 'system-tweaks' );
1899 // old plugin versions
1900 $long_url_strings_options = [ 'long_url_strings', 'st_longurl' ];
1901 $long_url_strings_enabled = false;
1902 foreach ( $long_url_strings_options as $option ) {
1903 if ( isset( $input[ $option ] ) && $input[ $option ] ) {
1904 $long_url_strings_enabled = true;
1905 }
1906 }
1907 if ( $long_url_strings_enabled ) {
1908 $rows[] = [
1909 'name' => "iThemes Security 'Long URLs' Enabled",
1910 'value' => true,
1911 'comment' => esc_html__( 'Tracking might not work because it looks like you have Long URLs disabled in iThemes Security. To fix this please contact ithemes security support.', 'matomo' ),
1912 'is_error' => true,
1913 ];
1914 }
1915 if ( $input['plugins_php'] ) {
1916 $rows[] = [
1917 'name' => "iThemes Security 'Disable PHP in plugins' Enabled",
1918 'value' => true,
1919 'comment' => esc_html__( 'You have disabled the PHP usage in the plugins folder from your ithemes security plugin. Matomo won\'t work in this configuration. You must uncheck the checkbox "Security > Settings > Advanced > System tweaks > Disable PHP in plugins."', 'matomo' ),
1920 'is_error' => true,
1921 ];
1922 }
1923 }
1924 }
1925 }
1926
1927 if ( is_plugin_active( 'secupress/secupress.php' ) ) {
1928 if ( function_exists( 'secupress_is_submodule_active' ) ) {
1929 $blocked_methods = (int) secupress_is_submodule_active( 'firewall', 'request-methods-header' );
1930 if ( $blocked_methods ) {
1931 if ( ! defined( 'MATOMO_SUPPORT_ASYNC_ARCHIVING' ) || MATOMO_SUPPORT_ASYNC_ARCHIVING ) {
1932 $rows[] = [
1933 'name' => "Secupress 'Block Bad Request Methods' Enabled",
1934 'value' => true,
1935 'comment' => esc_html__( "If reports aren't being generated then you may need to disable the feature \"Firewall -> Block Bad Request Methods\" in SecuPress (if it is enabled) or add the following line to your \"wp-config.php\"", 'matomo' ) . ": <br><code>define( 'MATOMO_SUPPORT_ASYNC_ARCHIVING', false );</code>.",
1936 'is_error' => true,
1937 ];
1938 }
1939 }
1940 }
1941 }
1942
1943 return $rows;
1944 }
1945
1946 /**
1947 * Convert the hexadecimal colors in the content into their rgb values
1948 *
1949 * @param string $content
1950 *
1951 * @return string
1952 */
1953 private function replace_hexadecimal_colors( $content ) {
1954 $matches = array();
1955 if ( preg_match_all( '/ (#(([a-f0-9]{8})|([a-f0-9]{4}[ ;])))/i', $content, $matches ) ) {
1956 foreach ( $matches[1] as $hexadecimal_color ) {
1957 switch ( strlen( $hexadecimal_color ) ) {
1958 case 9:
1959 list( $r, $g, $b, $a ) = sscanf( $hexadecimal_color, '#%02x%02x%02x%02x' );
1960 break;
1961 case 6:
1962 $hexadecimal_color = substr( $hexadecimal_color, 0, 5 );
1963 list( $r, $g, $b, $a ) = sscanf( $hexadecimal_color, '#%01x%01x%01x%01x' );
1964 break;
1965 }
1966 $content = str_replace( $hexadecimal_color, 'rgb(' . $r . ',' . $g . ',' . $b . ',' . $a . ')', $content );
1967 }
1968 }
1969 if ( preg_match_all( '/ (#(([a-f0-9]{6})|([a-f0-9]{3})))/i', $content, $matches ) ) {
1970 foreach ( $matches[1] as $hexadecimal_color ) {
1971 switch ( strlen( $hexadecimal_color ) ) {
1972 case 7:
1973 list( $r, $g, $b ) = sscanf( $hexadecimal_color, '#%02x%02x%02x' );
1974 break;
1975 case 4:
1976 list( $r, $g, $b ) = sscanf( $hexadecimal_color, '#%01x%01x%01x' );
1977 break;
1978 }
1979 $content = str_replace( $hexadecimal_color, 'rgb(' . $r . ',' . $g . ',' . $b . ')', $content );
1980 }
1981 }
1982
1983 return $content;
1984 }
1985
1986 private function get_abs_path_to_plugin() {
1987 if ( is_dir( ABSPATH . '/wp-content/plugins/matomo' ) ) {
1988 return ABSPATH . '/wp-content/plugins/matomo';
1989 } elseif ( is_dir( ABSPATH . '/wp-content/mu-plugins/matomo' ) ) {
1990 return ABSPATH . '/wp-content/mu-plugins/matomo';
1991 } else {
1992 return __DIR__ . '/../../..';
1993 }
1994 }
1995
1996 private function find_wp_load_path() {
1997 $abs_path = rtrim( ABSPATH, '/' );
1998 $search_dir = $this->get_abs_path_to_plugin();
1999
2000 while ( ! is_file( $search_dir . '/wp-load.php' ) && strpos( $search_dir, $abs_path ) === 0 ) {
2001 $search_dir = dirname( $search_dir );
2002 }
2003
2004 if ( strpos( $search_dir, $abs_path ) !== 0 ) {
2005 return null;
2006 }
2007
2008 return $search_dir . '/wp-load.php';
2009 }
2010
2011 private function get_php_cli_binary() {
2012 if ( ! $this->binary && ! WpMatomo::is_safe_mode() ) {
2013 Bootstrap::do_bootstrap();
2014 $cli_php = new CliPhp();
2015 $this->binary = $cli_php->findPhpBinary();
2016 }
2017
2018 return $this->binary;
2019 }
2020 }
2021