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