lines * ( array $sections, Settings $settings ). A section that is not a list of * lines is left out. */ const FILTER = 'careback_support_report'; /** * The report. * * @param Settings $settings Plugin settings. * @param Requests_Repository $requests The requests table. * @param Events_Repository $events The security log. * @return string */ public static function text( Settings $settings, Requests_Repository $requests, Events_Repository $events ) { unset( $requests, $events ); $lines = array( 'Careback support report — ' . gmdate( 'j F Y, H:i' ) . ' UTC', '' ); $lines = array_merge( $lines, self::head( $settings ) ); /** * Filters the sections of the support report: title => lines. An add-on * adds its own; keep personal data out of it. See api/admin.md. * * @param array $sections Title => string[]. * @param Settings $settings Plugin settings. */ $sections = apply_filters( self::FILTER, self::sections( $settings ), $settings ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- careback_support_report, the final prefix. foreach ( (array) $sections as $title => $rows ) { if ( ! is_array( $rows ) ) { continue; } $lines[] = ''; $lines[] = (string) $title; foreach ( $rows as $row ) { if ( is_scalar( $row ) ) { $lines[] = ' ' . (string) $row; } } } return implode( "\n", $lines ); } /** * The head: Careback, what builds on it, WordPress, WooCommerce, PHP and * the database, the theme, the active plugins. * * @param Settings $settings Plugin settings. * @return string[] */ private static function head( Settings $settings ) { global $wpdb, $wp_version; if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugins = get_plugins(); $self = dirname( plugin_basename( CAREBACK_FILE ) ); $builds = array(); $active = array(); foreach ( $plugins as $file => $data ) { $required = array_map( 'trim', explode( ',', strtolower( isset( $data['RequiresPlugins'] ) ? (string) $data['RequiresPlugins'] : '' ) ) ); $name = trim( $data['Name'] . ' ' . $data['Version'] ); if ( in_array( $self, $required, true ) ) { $builds[] = $name . ( is_plugin_active( $file ) ? '' : ' (inactive)' ); } if ( is_plugin_active( $file ) ) { $active[] = $name; } } $storage = 'posts'; $sync = ''; if ( class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) { $storage = 'HPOS'; } if ( class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) && method_exists( '\Automattic\WooCommerce\Utilities\OrderUtil', 'is_custom_order_tables_in_sync' ) ) { $sync = \Automattic\WooCommerce\Utilities\OrderUtil::is_custom_order_tables_in_sync() ? ' (compatibility mode on)' : ' (compatibility mode off)'; } $woo_form = Woo_Withdrawal_Form::state( $settings ); $server = is_object( $wpdb ) && method_exists( $wpdb, 'db_server_info' ) ? (string) $wpdb->db_server_info() : ''; $database = false !== stripos( $server, 'mariadb' ) && 1 === preg_match( '/(\d+\.\d+\.\d+)/', $server, $m ) ? 'MariaDB ' . $m[1] : 'MySQL ' . ( is_object( $wpdb ) ? (string) $wpdb->db_version() : '' ); $theme = wp_get_theme(); return array( 'Careback ' . CAREBACK_VERSION . ' (database ' . (string) get_option( 'careback_db_version', 'not set' ) . ')', 'Plugins that build on Careback: ' . ( $builds ? implode( ', ', $builds ) : 'none' ), 'WordPress ' . $wp_version . ', ' . ( is_multisite() ? 'multisite' : 'single site' ) . ', language ' . get_locale() . ', time zone ' . wp_timezone_string(), 'WooCommerce ' . ( function_exists( 'WC' ) ? WC()->version : 'not active' ) . ', order storage: ' . $storage . $sync, 'WooCommerce\'s own withdrawal form: ' . ( 'none' === $woo_form ? 'not in this WooCommerce' : ( 'off' === $woo_form ? 'off' : 'on' ) ) . ( 'kept' === $woo_form ? ' (kept beside Careback\'s)' : '' ), 'PHP ' . PHP_VERSION . ', memory limit ' . ( defined( 'WP_MEMORY_LIMIT' ) ? WP_MEMORY_LIMIT : (string) ini_get( 'memory_limit' ) ) . '; database ' . $database, 'Theme: ' . $theme->get( 'Name' ) . ' ' . $theme->get( 'Version' ) . ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ? ' (block theme)' : ' (classic theme)' ), 'Active plugins (' . count( $active ) . '): ' . implode( ', ', $active ), ); } /** * Careback's own sections: the form, the requests, the schedule, the emails, * the official texts and their versions. * * @param Settings $settings Plugin settings. * @return array */ private static function sections( Settings $settings ) { global $wpdb; $pages = array(); foreach ( Setup_Link::form_pages() as $post ) { $path = (string) wp_parse_url( (string) get_permalink( $post ), PHP_URL_PATH ); $pages[] = ( '' !== $path ? $path : '/' ) . ' (' . ( 'publish' === $post->post_status ? 'published' : $post->post_status ) . ')'; } $on = Routes::withdrawal_offered( $settings ); $policy = (int) $settings->get( 'returns_policy_page', 0 ); $recip = array_filter( array_map( 'trim', explode( ',', (string) $settings->get( 'admin_recipients', '' ) ) ), 'is_email' ); $flag = static function ( $value ) { return empty( $value ) ? 'off' : 'on'; }; $own = static function ( $key ) use ( $settings ) { return '' === trim( (string) $settings->get( $key, '' ) ) ? 'default' : 'custom'; }; $form = array( 'Switched on: ' . ( empty( $settings->get( 'portal_enabled' ) ) ? 'no' : 'yes' ) . '; on pages: ' . ( $pages ? implode( ', ', $pages ) : 'none' ), 'Statutory withdrawal: ' . ( $on ? 'on, ' . (int) $settings->get( 'statutory_window_days', 14 ) . ' days' : 'off' ) . '; voluntary return: ' . (int) $settings->get( 'return_window_days', 30 ) . ' days, reason ' . Reasons::mode( 'return', $settings ), 'Periods start: status "' . Periods::delivered_status( $settings ) . '" + ' . Periods::buffer_days( $settings ) . ' days buffer', 'Returns policy page: ' . ( $policy > 0 ? 'set (' . (string) get_post_status( $policy ) . ')' : 'not set' ) . '; consent: ' . $flag( $settings->get( 'policy_consent', 0 ) ), 'Legal wording: link text ' . $own( 'legal_link_text' ) . ', button ' . $own( 'legal_confirm_text' ), 'Fields: reason ' . $flag( $settings->get( 'field_reason' ) ) . ', comment ' . $flag( $settings->get( 'field_comment' ) ) . ', quantity ' . $flag( $settings->get( 'field_quantity' ) ) . '; item details: ' . $flag( $settings->get( 'variation_display' ) ), 'Fallback contact line: ' . ( '' === trim( (string) $settings->get( 'fallback_contact', '' ) ) ? 'not set' : 'set' ), 'Team email recipients: ' . count( $recip ), 'Bot protection: timing ' . ( empty( $settings->get( 'enable_time_token' ) ) ? 'off' : (int) $settings->get( 'min_submit_seconds' ) . '–' . (int) $settings->get( 'max_submit_seconds' ) . ' s' ), 'Throttling: ' . (int) $settings->get( 'lookup_max_attempts' ) . ' lookups per ' . (int) $settings->get( 'lookup_window_minutes' ) . ' min; auto-block after ' . (int) $settings->get( 'ban_threshold' ) . ', for ' . (int) $settings->get( 'ban_minutes' ) . ' min', 'Tracking page: ' . $flag( $settings->get( 'track_enabled' ) ), ); $by_type = array(); $total = 0; if ( Database::ready() ) { $table = Database::table( 'requests' ); $counts = array(); foreach ( (array) $wpdb->get_results( $wpdb->prepare( 'SELECT type, COUNT(*) AS n FROM %i GROUP BY type', $table ), ARRAY_A ) as $row ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- the plugin's own table, read live for the report. $counts[ (string) $row['type'] ] = (int) $row['n']; $total += (int) $row['n']; } // In the order of the routes, then any other type stored. foreach ( array_merge( array_intersect( Routes::keys(), array_keys( $counts ) ), array_diff( array_keys( $counts ), Routes::keys() ) ) as $type ) { $by_type[] = $type . ' ' . $counts[ $type ]; } $open = ( new Requests_Repository() )->count( Statuses::open() ); $events = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', Database::table( 'events' ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- the plugin's own table, read live for the report. $requests = array( $total . ' in total' . ( $by_type ? ': ' . implode( ', ', $by_type ) : '' ) . '; open ' . (int) $open, 'Security log: ' . $events . ' entries, kept ' . (int) $settings->get( 'retention_events_days', 90 ) . ' days', ); } else { $requests = array( 'Tables not set up on this site yet' ); } $next = wp_next_scheduled( Retention::HOOK ); $overdue = 0; foreach ( (array) _get_cron_array() as $timestamp => $hooks ) { if ( (int) $timestamp < time() - 10 * MINUTE_IN_SECONDS ) { foreach ( (array) $hooks as $events ) { $overdue += count( (array) $events ); } } } $schedule = array( Retention::HOOK . ': ' . ( $next ? 'next ' . gmdate( 'Y-m-d H:i', (int) $next ) . ' UTC' : 'not scheduled' ), 'WP-Cron: ' . ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ? 'off (DISABLE_WP_CRON)' : 'on' ), 'Overdue tasks of all plugins: ' . $overdue, ); $emails = array(); foreach ( array_keys( Emails::OWN ) as $class ) { $email = Emails::get( $class ); // The name WooCommerce → Emails shows, its id beside it. if ( $email ) { $emails[] = $email->get_title() . ' (' . $email->id . '): ' . ( $email->is_enabled() ? 'on' : 'off' ); } } return array( 'Form' => $form, 'Requests' => $requests, 'Scheduled tasks' => $schedule, 'Emails' => $emails ? $emails : array( 'not loaded' ), 'Official texts' => Official_Texts::report_lines( $settings ), ); } }