';
}
private function redirect_notice($message,$type='success',$tab='connect') {
set_transient(
'wpgss_admin_notice_' . get_current_user_id(),
array(
'message' => sanitize_text_field( (string) $message ),
'type' => ( $type === 'error' ) ? 'error' : 'success',
),
MINUTE_IN_SECONDS
);
wp_safe_redirect( add_query_arg( array( 'page' => 'wpgss-sync', 'tab' => sanitize_key( $tab ) ), admin_url( 'admin.php' ) ) );
exit;
}
private function action_button($action,$label,$class='button') { echo ''; }
/** Fixed custom-table name derived only from the trusted $wpdb prefix. */
private function queue_table_name() {
global $wpdb;
return $wpdb->prefix . 'wpgss_queue';
}
private function bust_queue_count_cache() {
foreach ( array( 'pending', 'processing', 'completed', 'failed', 'retry' ) as $status ) {
wp_cache_delete( 'wpgss_queue_count_' . $status, 'wpgss' );
}
}
public function page() {
if (!current_user_can('manage_options')) return;
$s=$this->settings();
// Display-only tab switch; capability checked above. No form mutation occurs here.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'dashboard';
if (!in_array($tab,array('dashboard','connect','integrations','activity','diagnostics'),true)) $tab='dashboard';
$connected = !empty($s['oauth_refresh_token']) || (!empty($s['oauth_access_token']) && !empty($s['oauth_token_expires_at']) && (int) $s['oauth_token_expires_at'] > time() + 60); $sheet_ready=!empty($s['spreadsheet_id']);
global $wpdb; $table=$this->queue_table_name(); $counts=array();
foreach ( array( 'pending', 'processing', 'completed', 'failed', 'retry' ) as $status ) {
$cache_key = 'wpgss_queue_count_' . $status;
$count = wp_cache_get( $cache_key, 'wpgss' );
if ( false === $count ) {
// Custom plugin queue table — not available via WP APIs. %i is the identifier placeholder (WP 6.2+).
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$count = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE status = %s', $table, $status ) );
wp_cache_set( $cache_key, $count, 'wpgss', 30 );
}
$counts[ $status ] = (int) $count;
}
$logs=get_option('wpgss_logs',array());
echo '
WORDPRESS INTEGRATION
Sheets Sync for Google
Connect once, choose what to send, and let your website keep your spreadsheet updated.
';
$this->notice(); echo '';
if ($tab==='dashboard') {
echo '
Setup checklist
Follow these steps in order. You only need to connect your account once.
';
$this->check_step('1','Connect Google account',$connected?'Connected'.(!empty($s['google_email'])?' as '.$s['google_email']:''):'Authorize access to Google Sheets',$connected,admin_url('admin.php?page=wpgss-sync&tab=connect'));
$this->check_step('2','Choose your spreadsheet',$sheet_ready?'Spreadsheet ID saved':'Paste your spreadsheet URL or ID',$sheet_ready,admin_url('admin.php?page=wpgss-sync&tab=connect'));
$this->check_step('3','Choose worksheet tabs',!empty($s['worksheet_names'])?'Worksheets loaded':'Load worksheets and select destinations',!empty($s['worksheet_names']),admin_url('admin.php?page=wpgss-sync&tab=connect'));
$this->check_step('4','Enable integrations','Select the data sources you want to sync',!empty($s['elementor_enabled'])||!empty($s['cf7_enabled'])||!empty($s['woocommerce_orders_enabled'])||!empty($s['woocommerce_customers_enabled']),admin_url('admin.php?page=wpgss-sync&tab=integrations'));
echo '
'; }
/**
* Step-by-step OAuth / Google Cloud setup instructions shown on the Connect tab.
*
* @param string $redirect_uri Exact WordPress redirect URI for Google Cloud.
*/
private function oauth_setup_guide( $redirect_uri ) {
$guide_url = plugins_url( 'docs/SETUP-GUIDE.md', WPGSS_FILE );
echo '';
echo '
How to create OAuth Client ID & Secret (step by step)
';
echo '
Follow these steps once in Google Cloud, then paste the Client ID and Secret into the form below. You do not need a Service Account JSON key.
';
echo '';
echo '
Create a Google Cloud project ';
echo 'Open Google Cloud Console, sign in, and create or select a project (for example “WordPress Sheets Sync”).
';
echo '
Enable Google Sheets API ';
echo 'Open Google Sheets API and click Enable.
';
echo '
Configure the OAuth consent screen ';
echo 'Open Google Auth Platform / OAuth consent screen. Set app name, support email, and developer contact. Choose External (unless everyone is in your Google Workspace). If the app is in Testing mode, add your Google account under Test users.
';
echo '
Create an OAuth Client (Web application) ';
echo 'Open Credentials / Clients → Create Client → choose Web application (not Desktop, Android, iOS, or Service account).
';
echo '
Add this exact Authorized redirect URI ';
echo 'Under Authorized redirect URIs, click Add URI and paste this value exactly (https, domain, path, and query must match):
';
echo '';
echo '
';
echo '';
echo '
Do not put this under Authorized JavaScript origins. A mismatch causes Google’s redirect_uri_mismatch error.
';
echo '';
echo '
Copy Client ID and Client Secret ';
echo 'After creating the client, copy the Client ID and Client Secret from Google Cloud.
';
echo '
Save them in WordPress (form below) ';
echo 'Paste Client ID and Client Secret into the fields below → click Save OAuth settings → then click Connect Google account and approve access with the Google account that can edit your spreadsheet.
';
echo '
Finish spreadsheet setup ';
echo 'On this same page: paste your spreadsheet URL or ID → Save spreadsheet → Test connection → Load worksheet tabs → choose destinations and save.
';
echo '';
echo 'Troubleshooting common issues
';
echo '
redirect_uri_mismatch — Copy the URI from this page again. Check https vs http, www vs non-www, and staging vs production domain.
';
echo '
Access blocked / app not verified — Continue only if you own the Cloud project. In Testing mode, add your account as a test user.
';
echo '
Connected but no refresh token — Remove the app from Google Account → Connections, then click Reconnect Google account here.
';
echo '
HTTP 403 on Test connection — Enable Sheets API and give the connected Google account Editor access on the spreadsheet.
This plugin uses Google OAuth 2.0 and the Google Sheets API only. Data is sent to Google when an administrator connects an account, tests the connection, loads worksheets, or when an enabled integration syncs a form submission or WooCommerce record.
';
if (!empty($s['oauth_client_id'])&&!empty($s['oauth_client_secret'])) $this->action_button('wpgss_oauth_start',$connected?'Reconnect Google account':'Connect Google account','button-primary');
if ($connected) $this->action_button('wpgss_disconnect','Disconnect Google account','button-secondary');
echo '
2. Select the exact spreadsheet
Paste the URL from your browser or just the spreadsheet ID. The plugin will use this spreadsheet only.
Found '.count($s['worksheet_names']).' worksheet tab(s). Choose a destination for each integration below.
';
echo '
3. Choose destination worksheet tabs
Use standard WooCommerce worksheet tabs: Orders, Order Items, and Customers. These dropdowns are populated from the spreadsheet above. If you change the spreadsheet, click “Load worksheet tabs” again.
' . ( empty( $s['worksheet_names'] ) ? 'Save your spreadsheet, then click “Load worksheet tabs” to populate these dropdowns.' : 'A missing worksheet tab is created automatically when the first record is synced.' ) . '
';
}
private function integrations_page($s) {
echo '';
}
private function activity_page($counts,$logs) {
echo '
Manual tools
Use these if you need to process queued data or recover failed jobs.
'; $this->action_button('wpgss_sync_now','Process queue now','button-primary'); $this->action_button('wpgss_retry_failed','Retry failed jobs','button-secondary'); $this->action_button('wpgss_import_orders','Queue up to 100 historical orders','button-secondary'); echo '
'; foreach(array('pending'=>'Waiting','processing'=>'In progress','completed'=>'Synced','failed'=>'Failed','retry'=>'Retrying') as $key=>$label) echo '
'.esc_html($counts[$key]).''.esc_html($label).'
'; echo '
Recent activity
Time
Type
Message
';
if (!$logs) echo '
No activity yet. Test the connection or send a test form submission.
'; else foreach(array_slice($logs,0,40) as $log) echo '
'.esc_html($log['time']??'').'
'.esc_html($log['type']??'').'
'.esc_html($log['message']??'').'
';
echo '
';
}
public function wpgss_oauth_start() {
if (!current_user_can('manage_options') || !check_admin_referer('wpgss_oauth_start')) wp_die('Unauthorized'); $s=$this->settings();
if (empty($s['oauth_client_id'])||empty($s['oauth_client_secret'])) $this->redirect_notice('Save the OAuth Client ID and Client Secret first.','error');
$state=wp_generate_password(48,false,false);
$state_key = hash('sha256', $state);
$state_data = array('user_id'=>get_current_user_id(),'created'=>time());
// Store state in both transient and a non-autoloaded option. Some hosts/object caches
// drop transients between the outbound redirect and Google's callback.
set_transient('wpgss_oauth_state_'.$state_key, $state_data, 15*MINUTE_IN_SECONDS);
update_option('wpgss_oauth_state_'.$state_key, $state_data, false);
$url = add_query_arg(
array(
'client_id' => $s['oauth_client_id'],
'redirect_uri' => admin_url('admin-post.php?action=wpgss_oauth_callback'),
'response_type' => 'code',
'scope' => 'https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/userinfo.email',
'access_type' => 'offline',
'prompt' => 'consent',
'include_granted_scopes' => 'true',
'state' => $state,
),
'https://accounts.google.com/o/oauth2/v2/auth'
);
// Allow only Google's OAuth host for this redirect, then use the safe
// WordPress redirect API. This satisfies redirect validation without
// permitting arbitrary external redirect destinations.
$google_oauth_host = 'accounts.google.com';
$allow_google_oauth_redirect = function ( $hosts ) use ( $google_oauth_host ) {
$hosts[] = $google_oauth_host;
return array_unique( $hosts );
};
add_filter( 'allowed_redirect_hosts', $allow_google_oauth_redirect );
wp_safe_redirect( esc_url_raw( $url ), 302, 'Sheets Sync Manager for Google' );
remove_filter( 'allowed_redirect_hosts', $allow_google_oauth_redirect );
exit;
}
public function wpgss_oauth_callback() {
/*
* Google redirects here with ?state=&code= (or ?error=). CSRF is enforced via the
* one-time hashed state stored when Connect started — not a WordPress nonce.
*/
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$state = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : '';
$google_error_raw = isset( $_GET['error'] ) ? sanitize_text_field( wp_unslash( $_GET['error'] ) ) : '';
$code = isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : '';
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$state_key = $state !== '' ? 'wpgss_oauth_state_' . hash('sha256', $state) : '';
$state_data = $state_key !== '' ? get_transient($state_key) : false;
if (!is_array($state_data) && $state !== '') { $state_data = get_option('wpgss_oauth_state_'.hash('sha256', $state), false); }
if ($state_key !== '') delete_transient($state_key);
if ($state !== '') delete_option('wpgss_oauth_state_'.hash('sha256', $state));
if (!is_array($state_data) || empty($state_data['user_id']) || empty($state) || (time() - absint($state_data['created'] ?? 0)) > 15 * MINUTE_IN_SECONDS) {
$message = 'OAuth callback reached WordPress, but its security state was missing or expired. No token was saved. Start Connect again; if this repeats, check caching/security plugins and ensure the site uses one consistent HTTPS/www domain.';
$this->record_diagnostic('oauth_error', $message);
$this->redirect_notice($message, 'error');
}
$uid = absint($state_data['user_id']);
$oauth_user = get_user_by('id', $uid);
if (!$oauth_user || !user_can($oauth_user, 'manage_options')) $this->redirect_notice('The administrator who started this connection is no longer authorized. Sign in as an administrator and connect again.','error');
if ($google_error_raw !== '') { $s = $this->settings(); $s['oauth_last_error'] = 'Google authorization failed: ' . $google_error_raw; update_option('wpgss_settings', $s, false); $this->record_diagnostic('oauth_error', $s['oauth_last_error']); $this->redirect_notice($s['oauth_last_error'], 'error'); }
$s = $this->settings();
if (!$code) { $err = 'Google did not return an authorization code. Check the OAuth consent screen, redirect URI, and Google account authorization.'; $s['oauth_last_error'] = $err; update_option('wpgss_settings', $s, false); $this->record_diagnostic('oauth_error', $err); $this->redirect_notice($err, 'error'); }
$response = wp_remote_post('https://oauth2.googleapis.com/token', array(
'timeout'=>25,
'headers'=>array('Content-Type'=>'application/x-www-form-urlencoded'),
'body'=>array('code'=>$code,'client_id'=>$s['oauth_client_id'] ?? '',
'client_secret'=>$s['oauth_client_secret'] ?? '',
'redirect_uri'=>admin_url('admin-post.php?action=wpgss_oauth_callback'),
'grant_type'=>'authorization_code')
));
if (is_wp_error($response)) {
$err = 'Could not contact Google: '.$response->get_error_message();
$s['oauth_last_error'] = $err; update_option('wpgss_settings',$s,false);
$this->record_diagnostic('oauth_error', $err);
$this->redirect_notice($err,'error');
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (wp_remote_retrieve_response_code($response)<200 || wp_remote_retrieve_response_code($response)>=300 || empty($body['access_token'])) {
$err = 'Google token exchange failed (HTTP '.absint(wp_remote_retrieve_response_code($response)).'): '.sanitize_text_field($body['error_description'] ?? $body['error'] ?? 'Check OAuth client credentials, consent screen, and exact redirect URI.');
$s['oauth_last_error'] = $err; update_option('wpgss_settings',$s,false);
$this->record_diagnostic('oauth_error', $err);
$this->redirect_notice($err,'error');
}
if (!empty($body['refresh_token'])) $s['oauth_refresh_token'] = sanitize_text_field($body['refresh_token']);
$s['oauth_access_token'] = sanitize_text_field($body['access_token']);
$s['oauth_token_expires_at'] = time() + max(60, absint($body['expires_in'] ?? 3600));
$s['oauth_last_error'] = '';
$email_response = wp_remote_get('https://www.googleapis.com/oauth2/v2/userinfo', array('timeout'=>15,'headers'=>array('Authorization'=>'Bearer '.$s['oauth_access_token'])));
if (!is_wp_error($email_response)) {
$u = json_decode(wp_remote_retrieve_body($email_response), true);
if (!empty($u['email'])) $s['google_email'] = sanitize_email($u['email']);
}
$saved_ok = $this->save_oauth_settings($s);
if (!$saved_ok) {
global $wpdb;
$db_detail = !empty($wpdb->last_error) ? ' Database reported an error while saving plugin options.' : '';
$err = 'Google returned an access token, but WordPress could not verify the dedicated OAuth token options after saving.' . $db_detail . ' Check the database error log and any persistent object-cache or security plugin. No token value was written to the diagnostic log.';
$this->record_diagnostic('oauth_error', $err);
$this->redirect_notice($err, 'error', 'diagnostics');
}
if (empty($s['oauth_refresh_token'])) {
$err = 'Google returned a temporary access token but no refresh token. The plugin cannot maintain a long-lived connection. In Google Cloud, set the OAuth consent screen to External and add this Google account as a test user if the app is in Testing. Then remove the app from Google Account → Security → Your connections to third-party apps, and click Reconnect Google account again. If the app is Internal, use an account from that Workspace organization.';
$s['oauth_last_error'] = $err; update_option('wpgss_settings', $s, false); $this->record_diagnostic('oauth_error', $err);
$this->redirect_notice($err, 'error');
}
$this->redirect_notice('Google account connected successfully'.(!empty($s['google_email']) ? ' as '.$s['google_email'] : '').'. Now test access to your selected spreadsheet.','success');
}
public function wpgss_disconnect() { if (!current_user_can('manage_options')||!check_admin_referer('wpgss_disconnect')) wp_die('Unauthorized'); $s=$this->settings(); unset($s['oauth_refresh_token'],$s['oauth_access_token'],$s['oauth_token_expires_at'],$s['google_email'],$s['oauth_last_error']); foreach (array('oauth_refresh_token','oauth_access_token','oauth_token_expires_at','google_email') as $key) { delete_option('wpgss_'.$key); } update_option('wpgss_settings',$s,false); $this->redirect_notice('Google account disconnected.','success'); }
private function record_diagnostic($type, $message) { $logs=get_option('wpgss_logs',array()); if(!is_array($logs)) $logs=array(); array_unshift($logs,array('time'=>current_time('mysql'),'type'=>sanitize_key($type),'message'=>sanitize_text_field($message))); update_option('wpgss_logs',array_slice($logs,0,200),false); }
public function wpgss_test_connection() { if (!current_user_can('manage_options')||!check_admin_referer('wpgss_test_connection')) wp_die('Unauthorized'); $result=WPGSS_Sync_Manager::instance()->test_connection(); if (is_wp_error($result)) { $this->record_diagnostic('connection_error', $result->get_error_message()); $this->redirect_notice($result->get_error_message(),'error','diagnostics'); } $s=$this->settings(); $s['spreadsheet_title']=$result['properties']['title']??'Connected spreadsheet'; update_option('wpgss_settings',$s,false); $this->redirect_notice('Connection successful. Google can access “'.$s['spreadsheet_title'].'”.','success'); }
public function wpgss_load_sheets() {
if (!current_user_can('manage_options') || !check_admin_referer('wpgss_load_sheets')) { wp_die('Unauthorized'); }
$s = $this->settings();
if (empty($s['spreadsheet_id'])) { $this->redirect_notice('Paste and save a spreadsheet URL or ID first.', 'error'); }
$client = new WPGSS_Google_Sheets($s);
$result = $client->list_worksheets($s['spreadsheet_id']);
if (is_wp_error($result)) { $this->redirect_notice($result->get_error_message(), 'error'); }
$names = $result['worksheets'];
// Rename legacy course-specific tabs to standard WooCommerce names when the target does not exist.
$renames = array('Courses' => 'Orders', 'Course Items' => 'Order Items');
foreach ($renames as $legacy => $standard) {
if (in_array($legacy, $names, true) && !in_array($standard, $names, true)) {
$rename_result = $client->rename_worksheet($s['spreadsheet_id'], $legacy, $standard);
if (is_wp_error($rename_result)) { $this->redirect_notice('Could not rename worksheet “' . $legacy . '” to “' . $standard . '”: ' . $rename_result->get_error_message(), 'error'); }
$names = array_map(function($name) use ($legacy, $standard) { return $name === $legacy ? $standard : $name; }, $names);
}
}
$s['worksheet_names'] = $names;
$s['spreadsheet_title'] = $result['title'];
foreach (array('forms_sheet' => 'Contact Forms', 'orders_sheet' => 'Orders', 'items_sheet' => 'Order Items', 'customers_sheet' => 'Customers') as $key => $default) {
$current = $s[$key] ?? '';
if ($key === 'orders_sheet' && $current === 'Courses') { $current = 'Orders'; }
if ($key === 'items_sheet' && $current === 'Course Items') { $current = 'Order Items'; }
if (empty($current) || !in_array($current, $names, true)) { $s[$key] = in_array($default, $names, true) ? $default : ($names[0] ?? $default); }
else { $s[$key] = $current; }
}
update_option('wpgss_settings', $s, false);
$this->redirect_notice('Loaded ' . count($names) . ' worksheet tabs from “' . $result['title'] . '”. Legacy Courses tabs were renamed where applicable. Save destinations below.');
}
public function wpgss_sync_now() { if (!current_user_can('manage_options')||!check_admin_referer('wpgss_sync_now')) wp_die('Unauthorized'); WPGSS_Sync_Manager::instance()->process_queue(); $this->redirect_notice('Queue processing was triggered. Review Activity & tools for results.','success','activity'); }
public function wpgss_retry_failed() {
if ( ! current_user_can( 'manage_options' ) || ! check_admin_referer( 'wpgss_retry_failed' ) ) {
wp_die( 'Unauthorized' );
}
global $wpdb;
$table = $this->queue_table_name();
// Custom plugin queue table — not available via WP APIs. %i is the identifier placeholder (WP 6.2+).
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query(
$wpdb->prepare(
"UPDATE %i SET status = 'retry', attempts = 0, updated_at = %s WHERE status = 'failed'",
$table,
current_time( 'mysql' )
)
);
$this->bust_queue_count_cache();
WPGSS_Sync_Manager::instance()->process_queue();
$this->redirect_notice( 'Failed jobs have been queued for retry.', 'success', 'activity' );
}
public function wpgss_import_orders() {
if ( ! current_user_can( 'manage_options' ) || ! check_admin_referer( 'wpgss_import_orders' ) ) {
wp_die( 'Unauthorized' );
}
if ( ! function_exists( 'wc_get_orders' ) ) {
$this->redirect_notice( 'WooCommerce is not active.', 'error', 'activity' );
}
$ids = wc_get_orders(
array(
'limit' => 100,
'return' => 'ids',
'orderby' => 'date',
'order' => 'ASC',
)
);
$sync = WPGSS_Sync_Manager::instance();
foreach ( $ids as $id ) {
// Call the plugin sync path directly (avoids inventing/firing unprefixed hooks).
$sync->woocommerce_order_created( $id );
}
$this->bust_queue_count_cache();
$this->redirect_notice( 'Queued up to 100 historical orders. Check Activity & tools for progress.', 'success', 'activity' );
}
private function diagnostics_page($s, $counts, $logs) {
global $wpdb;
$table = $this->queue_table_name();
// Custom plugin queue table presence check.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table;
$checks = array(
array('WordPress version', get_bloginfo('version'), version_compare(get_bloginfo('version'), '6.2', '>=')),
array('PHP version', PHP_VERSION, version_compare(PHP_VERSION, '7.4', '>=')),
array('WordPress HTTP API', function_exists('wp_remote_post') ? 'Available' : 'Missing', function_exists('wp_remote_post')),
array('OpenSSL extension', extension_loaded('openssl') ? 'Available' : 'Missing', extension_loaded('openssl')),
array('Queue database table', $table_exists ? 'Present' : 'Missing — deactivate/activate plugin to recreate it', $table_exists),
array('Google OAuth Client ID', !empty($s['oauth_client_id']) ? 'Saved' : 'Not saved', !empty($s['oauth_client_id'])),
array('Google OAuth Client Secret', !empty($s['oauth_client_secret']) ? 'Saved (hidden)' : 'Not saved', !empty($s['oauth_client_secret'])),
array('Google refresh token', !empty($s['oauth_refresh_token']) ? 'Saved' : 'Not saved — reconnect Google', !empty($s['oauth_refresh_token'])),
array('Spreadsheet ID', !empty($s['spreadsheet_id']) ? 'Saved' : 'Not saved', !empty($s['spreadsheet_id'])),
array('Debug logging', (defined('WP_DEBUG') && WP_DEBUG) ? 'Enabled' : 'Disabled', true),
);
echo '
System health checks
These checks do not expose your client secret or OAuth tokens. A green row means the local prerequisite is present; it does not by itself prove that Google API access works.
Check
Result
Status
';
foreach ($checks as $check) {
echo '
'.esc_html($check[0]).'
'.esc_html($check[1]).'
'.($check[2]?'OK':'ACTION REQUIRED').'
';
}
echo '
Live Google test
This sends a real request to the Google Sheets API using the saved authorization.
';
$this->action_button('wpgss_test_connection','Test Google + spreadsheet access','button-primary');
echo '
';
if (++$shown >= 30) break;
}
if (!$shown) echo '
No diagnostic errors recorded yet. Run the live Google test above.
';
echo '
';
if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) {
echo '
WordPress debug logging is enabled. Check the configured debug log for PHP fatal errors. Plugin diagnostics cannot intercept every fatal error safely.
';
} else {
echo '
WordPress debug logging is not enabled. If the site crashes, enable WP_DEBUG and WP_DEBUG_LOG temporarily in wp-config.php, reproduce the issue, then inspect wp-content/debug.log. Do not leave WP_DEBUG_DISPLAY enabled on a live site.