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