diff --git a/apps/cli/commands/site/set.ts b/apps/cli/commands/site/set.ts index f466f683e1..9cdd21adbb 100644 --- a/apps/cli/commands/site/set.ts +++ b/apps/cli/commands/site/set.ts @@ -44,10 +44,12 @@ export interface SetCommandOptions { php?: string; wp?: string; xdebug?: boolean; + debugLog?: boolean; + debugDisplay?: boolean; } export async function runCommand( sitePath: string, options: SetCommandOptions ): Promise< void > { - const { name, domain, https, php, wp, xdebug } = options; + const { name, domain, https, php, wp, xdebug, debugLog, debugDisplay } = options; if ( name === undefined && @@ -55,10 +57,14 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) https === undefined && php === undefined && wp === undefined && - xdebug === undefined + xdebug === undefined && + debugLog === undefined && + debugDisplay === undefined ) { throw new LoggerError( - __( 'At least one option (--name, --domain, --https, --php, --wp, --xdebug) is required.' ) + __( + 'At least one option (--name, --domain, --https, --php, --wp, --xdebug, --debug-log, --debug-display) is required.' + ) ); } @@ -112,9 +118,19 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) const phpChanged = php !== undefined && php !== site.phpVersion; const wpChanged = wp !== undefined; const xdebugChanged = xdebug !== undefined && xdebug !== site.enableXdebug; + const debugLogChanged = debugLog !== undefined && debugLog !== site.enableDebugLog; + const debugDisplayChanged = + debugDisplay !== undefined && debugDisplay !== site.enableDebugDisplay; const hasChanges = - nameChanged || domainChanged || httpsChanged || phpChanged || wpChanged || xdebugChanged; + nameChanged || + domainChanged || + httpsChanged || + phpChanged || + wpChanged || + xdebugChanged || + debugLogChanged || + debugDisplayChanged; if ( ! hasChanges ) { throw new LoggerError( __( 'No changes to apply. The site already has the specified settings.' ) @@ -127,6 +143,8 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) phpChanged, wpChanged, xdebugChanged, + debugLogChanged, + debugDisplayChanged, } ); const oldDomain = site.customDomain; @@ -153,6 +171,12 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) if ( xdebugChanged ) { foundSite.enableXdebug = xdebug; } + if ( debugLogChanged ) { + foundSite.enableDebugLog = debugLog; + } + if ( debugDisplayChanged ) { + foundSite.enableDebugDisplay = debugDisplay; + } await saveAppdata( appdata ); site = foundSite; @@ -287,6 +311,14 @@ export const registerCommand = ( yargs: StudioArgv ) => { .option( 'xdebug', { type: 'boolean', description: __( 'Enable Xdebug' ), + } ) + .option( 'debug-log', { + type: 'boolean', + description: __( 'Enable WP_DEBUG_LOG' ), + } ) + .option( 'debug-display', { + type: 'boolean', + description: __( 'Enable WP_DEBUG_DISPLAY' ), } ); }, handler: async ( argv ) => { @@ -298,6 +330,8 @@ export const registerCommand = ( yargs: StudioArgv ) => { php: argv.php, wp: argv.wp, xdebug: argv.xdebug, + debugLog: argv.debugLog, + debugDisplay: argv.debugDisplay, } ); } catch ( error ) { if ( error instanceof LoggerError ) { diff --git a/apps/cli/commands/site/tests/set.test.ts b/apps/cli/commands/site/tests/set.test.ts index 7cabcf1701..6516e05820 100644 --- a/apps/cli/commands/site/tests/set.test.ts +++ b/apps/cli/commands/site/tests/set.test.ts @@ -98,7 +98,7 @@ describe( 'CLI: studio site set', () => { describe( 'Validation', () => { it( 'should throw when no options provided', async () => { await expect( runCommand( testSitePath, {} ) ).rejects.toThrow( - 'At least one option (--name, --domain, --https, --php, --wp, --xdebug) is required.' + 'At least one option (--name, --domain, --https, --php, --wp, --xdebug, --debug-log, --debug-display) is required.' ); } ); diff --git a/apps/cli/lib/types/wordpress-server-ipc.ts b/apps/cli/lib/types/wordpress-server-ipc.ts index 9550c899bc..7f4506db54 100644 --- a/apps/cli/lib/types/wordpress-server-ipc.ts +++ b/apps/cli/lib/types/wordpress-server-ipc.ts @@ -13,6 +13,8 @@ const serverConfig = z.object( { siteLanguage: z.string().optional(), isWpAutoUpdating: z.boolean().optional(), enableXdebug: z.boolean().optional(), + enableDebugLog: z.boolean().optional(), + enableDebugDisplay: z.boolean().optional(), blueprint: z .object( { contents: z.any(), // Blueprint type is complex, allow any for now diff --git a/apps/cli/lib/wordpress-server-manager.ts b/apps/cli/lib/wordpress-server-manager.ts index 3609082a22..d289cbc05b 100644 --- a/apps/cli/lib/wordpress-server-manager.ts +++ b/apps/cli/lib/wordpress-server-manager.ts @@ -105,6 +105,14 @@ export async function startWordPressServer( serverConfig.enableXdebug = true; } + if ( site.enableDebugLog ) { + serverConfig.enableDebugLog = true; + } + + if ( site.enableDebugDisplay ) { + serverConfig.enableDebugDisplay = true; + } + const env = { ELECTRON_RUN_AS_NODE: '1', STUDIO_WORDPRESS_SERVER_CONFIG: JSON.stringify( serverConfig ), @@ -371,6 +379,14 @@ export async function runBlueprint( serverConfig.enableXdebug = true; } + if ( site.enableDebugLog ) { + serverConfig.enableDebugLog = true; + } + + if ( site.enableDebugDisplay ) { + serverConfig.enableDebugDisplay = true; + } + const env = { ELECTRON_RUN_AS_NODE: '1', STUDIO_WORDPRESS_SERVER_CONFIG: JSON.stringify( serverConfig ), diff --git a/apps/cli/patches/@wp-playground+wordpress+3.1.1.patch b/apps/cli/patches/@wp-playground+wordpress+3.1.1.patch index 172f90bc5b..279c1e6f80 100644 --- a/apps/cli/patches/@wp-playground+wordpress+3.1.1.patch +++ b/apps/cli/patches/@wp-playground+wordpress+3.1.1.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@wp-playground/wordpress/index.cjs b/node_modules/@wp-playground/wordpress/index.cjs -index 9c468e3..3e5ab08 100644 +index 9c468e3..324e27d 100644 --- a/node_modules/@wp-playground/wordpress/index.cjs +++ b/node_modules/@wp-playground/wordpress/index.cjs @@ -352,7 +352,7 @@ function skip_whitespace($tokens) { @@ -11,8 +11,21 @@ index 9c468e3..3e5ab08 100644 ob_start(); $wp_load = getenv('DOCUMENT_ROOT') . '/wp-load.php'; if (!file_exists($wp_load)) { +@@ -619,7 +619,11 @@ function skip_whitespace($tokens) { + + $log_file = WP_CONTENT_DIR . '/debug.log'; + define('ERROR_LOG_FILE', $log_file); +- ini_set('error_log', $log_file); ++ if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { ++ ini_set('error_log', $log_file); ++ } else { ++ ini_set('log_errors', '0'); ++ } + ?>`),await e.writeFile("/internal/shared/mu-plugins/sitemap-redirect.php",`` + ), await e.writeFile( + "/internal/shared/mu-plugins/sitemap-redirect.php", diff --git a/apps/cli/wordpress-server-child.ts b/apps/cli/wordpress-server-child.ts index ad303df87f..afcb500064 100644 --- a/apps/cli/wordpress-server-child.ts +++ b/apps/cli/wordpress-server-child.ts @@ -166,8 +166,14 @@ async function getBaseRunCLIArgs( }, ]; - const defaultConstants = { + const enableDebugLog = config.enableDebugLog ?? false; + const enableDebugDisplay = config.enableDebugDisplay ?? false; + + const defaultConstants: Record< string, boolean > = { WP_SQLITE_AST_DRIVER: true, + WP_DEBUG: enableDebugLog || enableDebugDisplay, + WP_DEBUG_LOG: enableDebugLog, + WP_DEBUG_DISPLAY: enableDebugDisplay, }; let blueprintBundle: BlueprintBundle | undefined; diff --git a/apps/studio/src/components/content-tab-settings.tsx b/apps/studio/src/components/content-tab-settings.tsx index d9f799281f..8d8dd86d2a 100644 --- a/apps/studio/src/components/content-tab-settings.tsx +++ b/apps/studio/src/components/content-tab-settings.tsx @@ -143,9 +143,20 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps ) { selectedSite.phpVersion } + + +

{ __( 'Debugging' ) }

+ + { selectedSite.enableXdebug ? __( 'Enabled' ) : __( 'Disabled' ) } + + { selectedSite.enableDebugLog ? __( 'Enabled' ) : __( 'Disabled' ) } + + + { selectedSite.enableDebugDisplay ? __( 'Enabled' ) : __( 'Disabled' ) } + diff --git a/apps/studio/src/components/tests/content-tab-settings.test.tsx b/apps/studio/src/components/tests/content-tab-settings.test.tsx index 2b671b9fac..e88abb15c1 100644 --- a/apps/studio/src/components/tests/content-tab-settings.test.tsx +++ b/apps/studio/src/components/tests/content-tab-settings.test.tsx @@ -155,8 +155,8 @@ describe( 'ContentTabSettings', () => { ).toHaveTextContent( 'localhost:8881' ); expect( screen.getByText( 'HTTPS' ) ).toBeVisible(); expect( screen.getByText( 'Xdebug' ) ).toBeVisible(); - // Both HTTPS and Xdebug show "Disabled" - expect( screen.getAllByText( 'Disabled' ) ).toHaveLength( 2 ); + // HTTPS, Xdebug, Debug log, and Debug display show "Disabled" + expect( screen.getAllByText( 'Disabled' ) ).toHaveLength( 4 ); expect( screen.getByRole( 'button', { name: 'Copy local path to clipboard' } ) ).toBeVisible(); expect( screen.getByText( '7.7.7' ) ).toBeVisible(); expect( diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index c8e6d443df..dc3b30be05 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -377,6 +377,14 @@ export async function updateSite( options.xdebug = updatedSite.enableXdebug ?? false; } + if ( updatedSite.enableDebugLog !== currentSite.enableDebugLog ) { + options.debugLog = updatedSite.enableDebugLog ?? false; + } + + if ( updatedSite.enableDebugDisplay !== currentSite.enableDebugDisplay ) { + options.debugDisplay = updatedSite.enableDebugDisplay ?? false; + } + const hasCliChanges = Object.keys( options ).length > 2; if ( hasCliChanges ) { diff --git a/apps/studio/src/ipc-types.d.ts b/apps/studio/src/ipc-types.d.ts index e83eebb762..00353d0da0 100644 --- a/apps/studio/src/ipc-types.d.ts +++ b/apps/studio/src/ipc-types.d.ts @@ -31,6 +31,8 @@ interface StoppedSiteDetails { autoStart?: boolean; latestCliPid?: number; enableXdebug?: boolean; + enableDebugLog?: boolean; + enableDebugDisplay?: boolean; sortOrder?: number; } diff --git a/apps/studio/src/modules/cli/lib/cli-site-editor.ts b/apps/studio/src/modules/cli/lib/cli-site-editor.ts index 67da71d8fd..e4e6578e7e 100644 --- a/apps/studio/src/modules/cli/lib/cli-site-editor.ts +++ b/apps/studio/src/modules/cli/lib/cli-site-editor.ts @@ -17,6 +17,8 @@ export interface EditSiteOptions { php?: string; wp?: string; xdebug?: boolean; + debugLog?: boolean; + debugDisplay?: boolean; } export async function editSiteViaCli( options: EditSiteOptions ): Promise< void > { @@ -79,5 +81,13 @@ function buildCliArgs( options: EditSiteOptions ): string[] { args.push( options.xdebug ? '--xdebug' : '--no-xdebug' ); } + if ( options.debugLog !== undefined ) { + args.push( options.debugLog ? '--debug-log' : '--no-debug-log' ); + } + + if ( options.debugDisplay !== undefined ) { + args.push( options.debugDisplay ? '--debug-display' : '--no-debug-display' ); + } + return args; } diff --git a/apps/studio/src/modules/site-settings/edit-site-details.tsx b/apps/studio/src/modules/site-settings/edit-site-details.tsx index bc0a189b97..15e002ecc5 100644 --- a/apps/studio/src/modules/site-settings/edit-site-details.tsx +++ b/apps/studio/src/modules/site-settings/edit-site-details.tsx @@ -5,7 +5,7 @@ import { } from '@studio/common/lib/domains'; import { siteNeedsRestart } from '@studio/common/lib/site-needs-restart'; import { SupportedPHPVersions } from '@studio/common/types/php-versions'; -import { SelectControl } from '@wordpress/components'; +import { SelectControl, TabPanel } from '@wordpress/components'; import { createInterpolateElement } from '@wordpress/element'; import { sprintf } from '@wordpress/i18n'; import { useI18n } from '@wordpress/react-i18n'; @@ -38,6 +38,10 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = const [ isEditingSite, setIsEditingSite ] = useState( false ); const [ needsRestart, setNeedsRestart ] = useState( false ); const [ enableXdebug, setEnableXdebug ] = useState( selectedSite?.enableXdebug ?? false ); + const [ enableDebugLog, setEnableDebugLog ] = useState( selectedSite?.enableDebugLog ?? false ); + const [ enableDebugDisplay, setEnableDebugDisplay ] = useState( + selectedSite?.enableDebugDisplay ?? false + ); const [ xdebugEnabledSite, setXdebugEnabledSite ] = useState< SiteDetails | null >( null ); const { data: isCertificateTrusted } = useCheckCertificateTrustQuery(); @@ -101,7 +105,9 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = Boolean( selectedSite.customDomain ) === useCustomDomain && usedCustomDomain === customDomain && !! selectedSite.enableHttps === ( !! usedCustomDomain && enableHttps ) && - !! selectedSite.enableXdebug === enableXdebug; + !! selectedSite.enableXdebug === enableXdebug && + !! selectedSite.enableDebugLog === enableDebugLog && + !! selectedSite.enableDebugDisplay === enableDebugDisplay; const hasValidationErrors = ! selectedSite || ! siteName.trim() || ( useCustomDomain && !! customDomainError ); @@ -118,6 +124,8 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = setErrorUpdatingWpVersion( null ); setEnableHttps( selectedSite.enableHttps ?? false ); setEnableXdebug( selectedSite.enableXdebug ?? false ); + setEnableDebugLog( selectedSite.enableDebugLog ?? false ); + setEnableDebugDisplay( selectedSite.enableDebugDisplay ?? false ); }, [ selectedSite, getEffectiveWpVersion ] ); const onSiteEdit = async ( event: FormEvent ) => { @@ -131,6 +139,9 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = const hasWpVersionChanged = selectedWpVersion !== getEffectiveWpVersion(); const hasPhpVersionChanged = selectedPhpVersion !== selectedSite.phpVersion; const hasXdebugChanged = enableXdebug !== ( selectedSite.enableXdebug ?? false ); + const hasDebugLogChanged = enableDebugLog !== ( selectedSite.enableDebugLog ?? false ); + const hasDebugDisplayChanged = + enableDebugDisplay !== ( selectedSite.enableDebugDisplay ?? false ); const hasDomainChanged = Boolean( selectedSite.customDomain ) !== useCustomDomain || ( useCustomDomain && customDomain !== selectedSite.customDomain ); @@ -145,6 +156,8 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = phpChanged: hasPhpVersionChanged, wpChanged: hasWpVersionChanged, xdebugChanged: hasXdebugChanged, + debugLogChanged: hasDebugLogChanged, + debugDisplayChanged: hasDebugDisplayChanged, } ); setNeedsRestart( needsRestart ); @@ -164,6 +177,8 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = customDomain: usedCustomDomain, enableHttps: !! usedCustomDomain && enableHttps, enableXdebug, + enableDebugLog, + enableDebugDisplay, }, hasWpVersionChanged ? selectedWpVersion : undefined ); @@ -205,170 +220,267 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = focusOnMount="firstContentElement" onRequestClose={ closeModal } className={ cx( + '[&_[role="document"]]:px-0', isEditingSite && '[&_[aria-label="Close"]_svg]:opacity-50 [&_[aria-label="Close"]]:cursor-not-allowed' ) } >
-
- - -
- + + { ( { name } ) => ( +
+ { name === 'general' && ( + <> + - -
- { errorUpdatingWpVersion && ( - { errorUpdatingWpVersion } - ) } +
+ -
-
- setUseCustomDomain( e.target.checked ) } - disabled={ isEditingSite } - /> - -
+ +
+ { errorUpdatingWpVersion && ( + + { errorUpdatingWpVersion } + + ) } - { useCustomDomain && ( -
- - - { customDomainError && ( - { customDomainError } - ) } -
- { __( 'Your system password will be required to set up the domain.' ) } -
-
- ) } +
+
+ setUseCustomDomain( e.target.checked ) } + disabled={ isEditingSite } + /> + +
- { useCustomDomain && ( -
- setEnableHttps( e.target.checked ) } - disabled={ isEditingSite } - /> - -
- ) } + { useCustomDomain && ( +
+ + + { customDomainError && ( + + { customDomainError } + + ) } +
+ { __( + 'Your system password will be required to set up the domain.' + ) } +
+
+ ) } - { ! isCertificateTrusted && useCustomDomain && ( -
- { __( - 'You need to manually add the Studio certificate authority to your keychain and trust it.' - ) }{ ' ' } - -
- ) } -
+ { useCustomDomain && ( +
+ setEnableHttps( e.target.checked ) } + disabled={ isEditingSite } + /> + +
+ ) } -
- + { __( + 'You need to manually add the Studio certificate authority to your keychain and trust it.' + ) }{ ' ' } + +
+ ) } +
+ ) } - placement="top-start" - > -
-
- setEnableXdebug( e.target.checked ) } - disabled={ - isEditingSite || - !! ( xdebugEnabledSite && xdebugEnabledSite.id !== selectedSite?.id ) - } - /> -
- -
-
+ +
+
+ setEnableXdebug( e.target.checked ) } + disabled={ + isEditingSite || + !! ( + xdebugEnabledSite && xdebugEnabledSite.id !== selectedSite?.id + ) + } + /> + +
+
+ { createInterpolateElement( + __( + 'Enable PHP debugging with Xdebug. Only one site can have Xdebug enabled at a time. Note that Xdebug may slow down site performance. ' + ), + { + learn_more_link: , + } + ) } +
+
+
+
+ +
+
+ setEnableDebugLog( e.target.checked ) } + disabled={ isEditingSite } + /> + +
+
+ { __( + "Log PHP errors and warnings to a debug.log file in your site's wp-content directory by setting the WP_DEBUG_LOG constant." + ) } +
+
+ +
+
+ setEnableDebugDisplay( e.target.checked ) } + disabled={ isEditingSite } + /> + +
+
+ { __( + 'Display PHP errors and warnings directly in the browser by setting the WP_DEBUG_DISPLAY constant.' + ) } +
+
+ + ) } + + ) } + -
+
diff --git a/apps/studio/src/storage/user-data.ts b/apps/studio/src/storage/user-data.ts index 5f893fb912..d34cf32e76 100644 --- a/apps/studio/src/storage/user-data.ts +++ b/apps/studio/src/storage/user-data.ts @@ -197,6 +197,8 @@ function toDiskFormat( { sites, ...rest }: UserData ): PersistedUserData { autoStart, latestCliPid, enableXdebug, + enableDebugLog, + enableDebugDisplay, sortOrder, } ) => { // No object spreading allowed. TypeScript's structural typing is too permissive and @@ -215,6 +217,8 @@ function toDiskFormat( { sites, ...rest }: UserData ): PersistedUserData { autoStart, latestCliPid, enableXdebug, + enableDebugLog, + enableDebugDisplay, sortOrder, themeDetails: { name: themeDetails?.name || '', diff --git a/tools/common/lib/site-events.ts b/tools/common/lib/site-events.ts index eab72fa9bc..8a6b4b7abe 100644 --- a/tools/common/lib/site-events.ts +++ b/tools/common/lib/site-events.ts @@ -22,6 +22,8 @@ export const siteDetailsSchema = z.object( { isWpAutoUpdating: z.boolean().optional(), autoStart: z.boolean().optional(), enableXdebug: z.boolean().optional(), + enableDebugLog: z.boolean().optional(), + enableDebugDisplay: z.boolean().optional(), } ); export type SiteDetails = z.infer< typeof siteDetailsSchema >; diff --git a/tools/common/lib/site-needs-restart.ts b/tools/common/lib/site-needs-restart.ts index 90a6592cb5..c22ab9cbe7 100644 --- a/tools/common/lib/site-needs-restart.ts +++ b/tools/common/lib/site-needs-restart.ts @@ -4,10 +4,28 @@ export interface SiteSettingChanges { phpChanged?: boolean; wpChanged?: boolean; xdebugChanged?: boolean; + debugLogChanged?: boolean; + debugDisplayChanged?: boolean; } export function siteNeedsRestart( changes: SiteSettingChanges ): boolean { - const { domainChanged, httpsChanged, phpChanged, wpChanged, xdebugChanged } = changes; + const { + domainChanged, + httpsChanged, + phpChanged, + wpChanged, + xdebugChanged, + debugLogChanged, + debugDisplayChanged, + } = changes; - return !! ( domainChanged || httpsChanged || phpChanged || wpChanged || xdebugChanged ); + return !! ( + domainChanged || + httpsChanged || + phpChanged || + wpChanged || + xdebugChanged || + debugLogChanged || + debugDisplayChanged + ); }