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