asset_base_path = $asset_base_path; } /** * @return array> */ public function sections(): array { $sections = [ 'environment' => $this->environmentSection(), 'daytidy' => $this->daytidySection(), 'database' => $this->databaseSection(), 'cron' => $this->cronSection(), 'notifications' => $this->notificationsSection(), 'payments' => $this->paymentsSection(), 'assets' => $this->assetsSection(), ]; return apply_filters('DayTidy/F__DayTidy/HelpSupport/Diagnostics__sections/sections', $sections); } /** * @param array>|null $sections * @return array> */ public function warnings(?array $sections = null): array { $sections = $sections ?: $this->sections(); $warnings = []; foreach ($sections as $section) { foreach ((array) ($section['rows'] ?? []) as $row) { if (($row['status'] ?? 'ok') !== 'warning') continue; $warnings[] = [ 'label' => (string) ($row['label'] ?? ''), 'message' => (string) ($row['message'] ?? $row['value'] ?? ''), ]; } } return apply_filters('DayTidy/F__DayTidy/HelpSupport/Diagnostics__warnings/warnings', $warnings, $sections); } /** * @param array>|null $sections * @param array>|null $warnings */ public function debugInfo(?array $sections = null, ?array $warnings = null): string { $sections = $sections ?: $this->sections(); $warnings = $warnings ?: $this->warnings($sections); $data = [ 'generated_at_utc' => gmdate('Y-m-d H:i:s'), 'warning_count' => count($warnings), 'warnings' => $warnings, 'sections' => $sections, ]; return (string) wp_json_encode($this->redact($data), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } /** * @param array>|null $sections * @param array>|null $warnings */ public function siteSummary(?array $sections = null, ?array $warnings = null): string { $sections = $sections ?: $this->sections(); $warnings = $warnings ?: $this->warnings($sections); $environment = $sections['environment']['rows'] ?? []; $daytidy = $sections['daytidy']['rows'] ?? []; return implode("\n", [ 'DayTidy Support Summary', 'Warnings: ' . count($warnings), 'WordPress: ' . $this->rowValue($environment, 'wordpress_version'), 'PHP: ' . $this->rowValue($environment, 'php_version'), 'DayTidy: ' . $this->rowValue($daytidy, 'daytidy_version'), 'Site timezone: ' . $this->rowValue($environment, 'timezone'), ]); } /** * @param mixed $value * @return mixed */ public function redact($value, string $key = '') { if ($key && preg_match('/(secret|password|passwd|token|nonce|salt|authorization|private|credential|api[_-]?key|client[_-]?secret)/i', $key)) { return '[redacted]'; } if (is_array($value)) { $redacted = []; foreach ($value as $item_key => $item_value) { $redacted[$item_key] = $this->redact($item_value, (string) $item_key); } return $redacted; } if (is_string($value)) { return preg_replace('/(sk|pk|whsec|rk)_[A-Za-z0-9_\\-]+/', '$1_[redacted]', $value); } return $value; } /** * @return array */ private function environmentSection(): array { global $wpdb; $wp_version = function_exists('get_bloginfo') ? (string) get_bloginfo('version') : ''; $php_warning = version_compare(PHP_VERSION, Requirements::$PHP, '<'); $wp_warning = $wp_version && version_compare($wp_version, Requirements::$WP, '<'); return [ 'label' => __('Environment', 'daytidy'), 'rows' => [ // translators: Placeholder values. $this->row('wordpress_version', __('WordPress version', 'daytidy'), $wp_version, $wp_warning ? 'warning' : 'ok', $wp_warning ? sprintf(__('DayTidy requires WordPress %s or higher.', 'daytidy'), Requirements::$WP) : ''), // translators: Placeholder values. $this->row('php_version', __('PHP version', 'daytidy'), PHP_VERSION, $php_warning ? 'warning' : 'ok', $php_warning ? sprintf(__('DayTidy requires PHP %s or higher.', 'daytidy'), Requirements::$PHP) : ''), $this->row('mysql_version', __('MySQL version', 'daytidy'), is_object($wpdb) && method_exists($wpdb, 'db_version') ? (string) $wpdb->db_version() : __('Unavailable', 'daytidy')), $this->row('server_software', __('Server software', 'daytidy'), isset($_SERVER['SERVER_SOFTWARE']) ? sanitize_text_field(wp_unslash((string) $_SERVER['SERVER_SOFTWARE'])) : __('Unavailable', 'daytidy')), $this->row('multisite', __('Multisite', 'daytidy'), function_exists('is_multisite') && is_multisite() ? __('Yes', 'daytidy') : __('No', 'daytidy')), $this->row('timezone', __('Timezone', 'daytidy'), function_exists('wp_timezone_string') ? wp_timezone_string() : (string) ini_get('date.timezone')), $this->row('permalink_structure', __('Permalink structure', 'daytidy'), function_exists('get_option') ? ((string) get_option('permalink_structure', '') ?: __('Plain', 'daytidy')) : ''), $this->row('wp_cron', __('WP-Cron', 'daytidy'), defined('DISABLE_WP_CRON') && DISABLE_WP_CRON ? __('Disabled', 'daytidy') : __('Enabled', 'daytidy'), defined('DISABLE_WP_CRON') && DISABLE_WP_CRON ? 'warning' : 'ok', defined('DISABLE_WP_CRON') && DISABLE_WP_CRON ? __('DayTidy scheduled tasks need an external cron when WP-Cron is disabled.', 'daytidy') : ''), $this->row('wp_debug', __('WP_DEBUG', 'daytidy'), defined('WP_DEBUG') && WP_DEBUG ? __('Enabled', 'daytidy') : __('Disabled', 'daytidy')), ], ]; } /** * @return array */ private function daytidySection(): array { $profile = Profile::getInstance(); $plugins = $this->plugins(); $active_plugins = (array) get_option('active_plugins', []); $daytidy_plugins = []; foreach ($active_plugins as $plugin_file) { if (strpos((string) $plugin_file, 'daytidy') === false) continue; $plugin = $plugins[$plugin_file] ?? []; $daytidy_plugins[] = sprintf( '%s %s', $this->displayText((string) ($plugin['Name'] ?? $plugin_file)), (string) ($plugin['Version'] ?? '') ); } $theme = function_exists('wp_get_theme') ? wp_get_theme() : null; $module_values = []; $config = Config::modules(); foreach (Modules::get() as $module) { if (!$module instanceof BaseModule) continue; $module_values[] = sprintf( '%s: %s', $module->key, $module->enabled($config) ? __('enabled', 'daytidy') : __('disabled', 'daytidy') ); } return [ 'label' => __('DayTidy', 'daytidy'), 'rows' => [ $this->row('daytidy_version', __('DayTidy version', 'daytidy'), $profile->version()), $this->row('active_theme', __('Active theme', 'daytidy'), $theme ? $this->displayText((string) $theme->get('Name')) . ' ' . $this->displayText((string) $theme->get('Version')) : __('Unavailable', 'daytidy')), $this->row('daytidy_plugins', __('DayTidy plugins', 'daytidy'), $daytidy_plugins ? implode(', ', $daytidy_plugins) : __('None detected', 'daytidy')), $this->row('modules', __('Modules', 'daytidy'), $module_values ? implode(', ', $module_values) : __('No modules detected', 'daytidy')), ], ]; } /** * @return array */ private function databaseSection(): array { try { $db = new Db(); $missing = []; foreach (CreateTables::tables() as $table) { $name = $db->table($table); $found = $db->getVar($db->prepare('SHOW TABLES LIKE %s', $name)); if ($found !== $name) $missing[] = $table; } return [ 'label' => __('Database', 'daytidy'), 'rows' => [ $this->row( 'tables', __('DayTidy tables', 'daytidy'), sprintf('%d / %d', count(CreateTables::tables()) - count($missing), count(CreateTables::tables())), count($missing) ? 'warning' : 'ok', count($missing) ? sprintf( /* translators: %s: Comma-separated list of missing DayTidy database tables. */ __('Missing tables: %s', 'daytidy'), implode(', ', $missing) ) : '' ), ], ]; } catch (Throwable $exception) { return [ 'label' => __('Database', 'daytidy'), 'rows' => [ $this->row('database_status', __('Database status', 'daytidy'), __('Unavailable', 'daytidy'), 'warning', $exception->getMessage()), ], ]; } } /** * @return array */ private function cronSection(): array { $rows = []; foreach (Hooks::crons() as $hook => $recurrence) { $next = wp_next_scheduled($hook); $rows[] = $this->row( 'cron_' . sanitize_key((string) $hook), (string) $hook, $next ? sprintf('%s (%s)', gmdate('Y-m-d H:i:s', (int) $next), $recurrence) : __('Missing', 'daytidy'), $next ? 'ok' : 'warning', $next ? '' : __('This DayTidy scheduled task is not currently scheduled.', 'daytidy') ); } return [ 'label' => __('Cron', 'daytidy'), 'rows' => $rows, ]; } /** * @return array */ private function notificationsSection(): array { $enabled = Modules::enabled('notifications/core'); $counts = function_exists('wp_count_posts') ? wp_count_posts(Notification::PT) : null; $published = $counts && isset($counts->publish) ? (int) $counts->publish : 0; $disabled = $this->notificationCountByEnabled(false); $enabled_count = $this->notificationCountByEnabled(true); return [ 'label' => __('Notifications', 'daytidy'), 'rows' => [ $this->row('notifications_module', __('Notifications module', 'daytidy'), $enabled ? __('Enabled', 'daytidy') : __('Disabled', 'daytidy')), $this->row('notification_templates', __('Published templates', 'daytidy'), (string) $published), $this->row('enabled_notifications', __('Enabled email notifications', 'daytidy'), (string) $enabled_count, $enabled && $published > 0 && $enabled_count === 0 ? 'warning' : 'ok', $enabled && $published > 0 && $enabled_count === 0 ? __('Notification templates exist, but none are enabled.', 'daytidy') : ''), $this->row('disabled_notifications', __('Disabled email notifications', 'daytidy'), (string) $disabled), $this->row('email_delivery', __('Email delivery', 'daytidy'), __('Uses wp_mail and host/mail plugin configuration.', 'daytidy')), ], ]; } /** * @return array */ private function paymentsSection(): array { $options = (new Payment())->get(); $gateways = Gateways::all(); $rows = [ $this->row('payment_method', __('Payment method', 'daytidy'), (string) ($options['method'] ?? 'woo')), ]; foreach ($gateways as $gateway) { $gateway_options = $gateway->options(); $missing = $this->missingGatewayFields($gateway->key(), $gateway_options); $enabled = !empty($gateway_options['status']); $message = $enabled && $missing // translators: Placeholder values. ? sprintf(__('Missing required configuration: %s', 'daytidy'), implode(', ', $missing)) : ''; $value = $enabled ? __('Enabled', 'daytidy') : __('Disabled', 'daytidy'); if (isset($gateway_options['mode']) && is_scalar($gateway_options['mode'])) { $value .= ' (' . sanitize_text_field((string) $gateway_options['mode']) . ')'; } $rows[] = $this->row( 'gateway_' . sanitize_key($gateway->key()), $gateway->title(), $value, $enabled && $missing ? 'warning' : 'ok', $message ); } return [ 'label' => __('Payments', 'daytidy'), 'rows' => $rows, ]; } /** * @return array */ private function assetsSection(): array { $profile = Profile::getInstance(); $assets = [ 'dist/js/backend.min.js', 'dist/js/frontend.min.js', 'dist/css/backend.css', 'dist/css/frontend.css', ]; $rows = [ $this->row('asset_version', __('Asset version', 'daytidy'), $profile->version()), ]; foreach ($assets as $asset) { $exists = file_exists($this->assetBasePath() . '/' . $asset); $rows[] = $this->row( 'asset_' . sanitize_key(str_replace(['/', '.'], '_', $asset)), $asset, $exists ? __('Present', 'daytidy') : __('Missing', 'daytidy'), $exists ? 'ok' : 'warning', $exists ? '' : __('Expected built asset file was not found.', 'daytidy') ); } return [ 'label' => __('Assets', 'daytidy'), 'rows' => $rows, ]; } /** * @return array */ private function row(string $key, string $label, string $value, string $status = 'ok', string $message = ''): array { return [ 'key' => $key, 'label' => $label, 'value' => $value, 'status' => $status, 'message' => $message, ]; } /** * @param array> $rows */ private function rowValue(array $rows, string $key): string { foreach ($rows as $row) { if (($row['key'] ?? '') === $key) return (string) ($row['value'] ?? ''); } return ''; } private function displayText(string $value): string { if (function_exists('wp_specialchars_decode')) return wp_specialchars_decode($value, ENT_QUOTES); return html_entity_decode($value, ENT_QUOTES, 'UTF-8'); } /** * @return array> */ private function plugins(): array { if (!function_exists('get_plugins') && defined('ABSPATH') && file_exists(ABSPATH . 'wp-admin/includes/plugin.php')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } return function_exists('get_plugins') ? get_plugins() : []; } /** * @param array $options * @return string[] */ private function missingGatewayFields(string $gateway, array $options): array { $fields = [ Gateways::STRIPE => ['public-key', 'secret-key'], Gateways::PAYPAL_STANDARD => ['client-id', 'secret'], ]; $missing = []; foreach ($fields[$gateway] ?? [] as $field) { if (isset($options[$field]) && trim((string) $options[$field]) !== '') continue; $missing[] = $field; } return $missing; } private function notificationCountByEnabled(bool $enabled): int { if (!function_exists('get_posts')) return 0; return count(get_posts([ 'post_type' => Notification::PT, 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => -1, 'meta_query' => [ [ 'key' => 'daytidy_notification_enabled', 'value' => $enabled ? '1' : '0', ], ], ])); } private function assetBasePath(): string { if ($this->asset_base_path !== null) return rtrim($this->asset_base_path, '/'); return rtrim(Profile::getInstance()->path() . '/assets', '/'); } }