Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/components/add-site.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@ export default function AddSite( { className }: AddSiteProps ) {
<Button
type="submit"
variant="primary"
disabled={ !! error || !! customDomainError || ! siteName?.trim() }
disabled={
!! error || ( !! customDomainError && useCustomDomain ) || ! siteName?.trim()
}
>
{ __( 'Add site' ) }
</Button>
Expand Down
7 changes: 4 additions & 3 deletions src/components/site-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ACCEPTED_IMPORT_FILE_TYPES } from 'src/constants';
import { useDocsLink } from 'src/hooks/use-docs-link';
import { useOffline } from 'src/hooks/use-offline';
import { cx } from 'src/lib/cx';
import { generateCustomDomainFromSiteName } from 'src/lib/generate-custom-domain';
import { generateCustomDomainFromSiteName } from 'src/lib/domains';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { useAppDispatch, useRootSelector } from 'src/stores';
import {
Expand Down Expand Up @@ -294,6 +294,7 @@ export const SiteForm = ( {
chevronIcon = chevronRight;
}
const generatedDomainName = generateCustomDomainFromSiteName( siteName );
const shouldShowCustomDomainError = customDomainError && useCustomDomain;

return (
<form className={ className } onSubmit={ onSubmit }>
Expand Down Expand Up @@ -353,10 +354,10 @@ export const SiteForm = ( {
{ __( 'Advanced settings' ) }
</div>
</Button>
{ ( error || customDomainError ) && (
{ ( error || shouldShowCustomDomainError ) && (
<span className="text-red-500 text-[13px] leading-[16px] ml-2 flex items-center">
<Icon icon={ warning } size={ 16 } className="mr-1 fill-red-500" />
{ error && customDomainError
{ error && shouldShowCustomDomainError
? __( '2 errors found' )
: __( '1 error found' ) }
</span>
Expand Down
17 changes: 3 additions & 14 deletions src/hooks/use-add-site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useI18n } from '@wordpress/react-i18n';
import { useCallback, useMemo, useState } from 'react';
import { useImportExport } from 'src/hooks/use-import-export';
import { useSiteDetails } from 'src/hooks/use-site-details';
import { generateCustomDomainFromSiteName } from 'src/lib/generate-custom-domain';
import { generateCustomDomainFromSiteName, validateDomainName } from 'src/lib/domains';
import { getIpcApi } from 'src/lib/get-ipc-api';
import {
DEFAULT_PHP_VERSION,
Expand Down Expand Up @@ -39,20 +39,9 @@ export function useAddSite() {
const handleCustomDomainChange = useCallback(
( value: string | null ) => {
setCustomDomain( value );
// Validate custom domain if enabled
const domainPattern =
/^(?!-)[\p{L}\p{N}][\p{L}\p{N}-]{0,61}[\p{L}\p{N}](?<!-)(?:\.(?!-)[\p{L}\p{N}-]{1,61}[\p{L}\p{N}](?<!-))+$/u;
if ( useCustomDomain && value && ! domainPattern.test( value ) ) {
setCustomDomainError( __( 'Please enter a valid domain name' ) );
} else if ( useCustomDomain && value && value.length > 253 ) {
setCustomDomainError( __( 'The domain name is too long' ) );
} else if ( useCustomDomain && value === '' ) {
setCustomDomainError( __( 'The domain name is required' ) );
} else {
setCustomDomainError( '' );
}
setCustomDomainError( validateDomainName( useCustomDomain, value ) );
},
[ __, useCustomDomain, setCustomDomain, setCustomDomainError ]
[ useCustomDomain, setCustomDomain, setCustomDomainError ]
);

const handlePathSelectorClick = useCallback( async () => {
Expand Down
14 changes: 12 additions & 2 deletions src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { download } from 'src/lib/download';
import { isEmptyDir, pathExists, isWordPressDirectory, sanitizeFolderName } from 'src/lib/fs-utils';
import { getImageData } from 'src/lib/get-image-data';
import { getSyncBackupTempPath } from 'src/lib/get-sync-backup-temp-path';
import { addDomainToHosts } from 'src/lib/hosts-file';
import { addDomainToHosts, updateDomainInHosts } from 'src/lib/hosts-file';
import { exportBackup } from 'src/lib/import-export/export/export-manager';
import { ExportOptions } from 'src/lib/import-export/export/types';
import { ImportExportEventData } from 'src/lib/import-export/handle-events';
Expand All @@ -45,7 +45,7 @@ import { portFinder } from 'src/lib/port-finder';
import { shellOpenExternalWrapper } from 'src/lib/shell-open-external-wrapper';
import { sortSites } from 'src/lib/sort-sites';
import { installSqliteIntegration, keepSqliteIntegrationUpdated } from 'src/lib/sqlite-versions';
import { updateSiteUrlToLocal } from 'src/lib/updateSiteUrlToLocal';
import { updateSiteUrlToLocal } from 'src/lib/update-site-url-to-local';
import * as windowsHelpers from 'src/lib/windows-helpers';
import { getLogsFilePath, writeLogToFile, type LogLevel } from 'src/logging';
import { getMainWindow } from 'src/main-window';
Expand Down Expand Up @@ -220,6 +220,10 @@ export async function updateSite(
updatedSite: SiteDetails
): Promise< SiteDetails[] > {
const userData = await loadUserData();

const existingSite = userData.sites.find( ( site ) => site.id === updatedSite.id );
const oldDomain = existingSite?.customDomain;
const newDomain = updatedSite.customDomain;
const updatedSites = userData.sites.map( ( site ) =>
site.id === updatedSite.id ? updatedSite : site
);
Expand All @@ -228,6 +232,12 @@ export async function updateSite(
const server = SiteServer.get( updatedSite.id );
if ( server ) {
server.updateSiteDetails( updatedSite );

// Handle domain changes if the site is running (updates hosts and database)
if ( oldDomain !== newDomain ) {
updateDomainInHosts( oldDomain, newDomain, server.details.port );
await updateSiteUrlToLocal( updatedSite.id );
}
}
await saveUserData( userData );
return mergeSiteDetailsWithRunningDetails( userData.sites );
Expand Down
33 changes: 33 additions & 0 deletions src/lib/domains.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { __ } from '@wordpress/i18n';
import { sanitizeFolderName } from './generate-site-name';

/**
* Generates a suitable domain name from site name
*/
export const generateCustomDomainFromSiteName = ( siteName: string ): string => {
const domainBase = sanitizeFolderName( siteName );

return `${ domainBase }.wp.local`;
};

export const validateDomainName = (
useCustomDomain: boolean,
domainName: string | null
): string => {
// Validate custom domain if enabled
const domainPattern =
/^(?!-)[\p{L}\p{N}][\p{L}\p{N}-]{0,61}[\p{L}\p{N}](?<!-)(?:\.(?!-)[\p{L}\p{N}-]{1,61}[\p{L}\p{N}](?<!-))+$/u;
if ( useCustomDomain && domainName && ! domainPattern.test( domainName ) ) {
return __( 'Please enter a valid domain name' );
}

if ( useCustomDomain && domainName && domainName.length > 253 ) {
return __( 'The domain name is too long' );
}

if ( useCustomDomain && domainName === '' ) {
return __( 'The domain name is required' );
}

return '';
};
10 changes: 0 additions & 10 deletions src/lib/generate-custom-domain.ts

This file was deleted.

42 changes: 42 additions & 0 deletions src/lib/hosts-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,48 @@ export const removeDomainFromHosts = async ( domain: string ): Promise< void > =
}
};

export const updateDomainInHosts = async (
oldDomain: string | undefined,
newDomain: string | undefined,
port: number
): Promise< void > => {
if ( oldDomain === newDomain ) {
return;
}

if ( ! oldDomain && newDomain ) {
await addDomainToHosts( newDomain, port );
return;
}

if ( oldDomain && ! newDomain ) {
await removeDomainFromHosts( oldDomain );
return;
}

try {
const hostsContent = await readHostsFile();
const encodedOldDomain = domainToASCII( oldDomain as string );
const encodedNewDomain = domainToASCII( newDomain as string );
const oldPattern = createHostsEntryPattern( encodedOldDomain );
const newContent = updateStudioBlock( hostsContent, ( entries ) => {
const filtered = entries.filter( ( entry ) => ! entry.match( oldPattern ) );
return [ ...filtered, `127.0.0.1 ${ encodedNewDomain } # Port ${ port }` ];
} );

if ( newContent !== hostsContent ) {
await writeHostsFile( newContent );
}
} catch ( error ) {
Sentry.captureException( error );
console.error(
`Error replacing domain ${ oldDomain } with ${ newDomain } in hosts file:`,
error
);
throw error;
}
};

/**
* Helper function for manipulating the "block" of entries in the hosts file
* pertaining to WordPres Studio.
Expand Down
2 changes: 1 addition & 1 deletion src/lib/import-export/import/importers/importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { generateBackupFilename } from 'src/lib/import-export/export/generate-ba
import { ImportEvents } from 'src/lib/import-export/import/events';
import { BackupContents, MetaFileData } from 'src/lib/import-export/import/types';
import { serializePlugins } from 'src/lib/serialize-plugins';
import { updateSiteUrlToLocal } from 'src/lib/updateSiteUrlToLocal';
import { updateSiteUrlToLocal } from 'src/lib/update-site-url-to-local';
import { SiteServer } from 'src/site-server';
import { DEFAULT_PHP_VERSION } from 'vendor/wp-now/src/constants';

Expand Down
91 changes: 90 additions & 1 deletion src/lib/tests/hosts-file.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { readFile, writeFile } from 'fs';
import { addDomainToHosts, removeDomainFromHosts } from '../hosts-file';
import { addDomainToHosts, removeDomainFromHosts, updateDomainInHosts } from '../hosts-file';

const readFileCallbackMock = jest.fn();

Expand Down Expand Up @@ -191,4 +191,93 @@ describe( 'hosts-file', () => {
await expect( removeDomainFromHosts( 'test.wp.cloud' ) ).rejects.toThrow( 'Read error' );
} );
} );

describe( 'updateDomainInHosts', () => {
it( 'should replace an existing domain with a new domain', async () => {
readFileCallbackMock.mockResolvedValueOnce( sampleHostsContent );

await updateDomainInHosts( 'foo.wp.cloud', 'new-domain.wp.cloud', 8002 );

expect( readFile ).toHaveBeenCalled();
expect( writeFile ).toHaveBeenCalled();

const newContent = ( writeFile as unknown as jest.Mock ).mock.calls[ 0 ][ 1 ];

expect( newContent ).toEqual(
`127.0.0.1 localhost
::1 localhost

# Some comment

# BEGIN WordPress Studio
127.0.0.1 bar.wp.cloud # Port 8001
127.0.0.1 new-domain.wp.cloud # Port 8002
# END WordPress Studio

# Other entries
192.168.1.1 router`
);
} );

it( 'should add a new domain if old domain is undefined', async () => {
readFileCallbackMock.mockResolvedValueOnce( sampleHostsContent );

await updateDomainInHosts( undefined, 'new-domain.wp.cloud', 8002 );

expect( readFile ).toHaveBeenCalled();
expect( writeFile ).toHaveBeenCalled();

const newContent = ( writeFile as unknown as jest.Mock ).mock.calls[ 0 ][ 1 ];

expect( newContent ).toEqual(
`127.0.0.1 localhost
::1 localhost

# Some comment

# BEGIN WordPress Studio
127.0.0.1 foo.wp.cloud # Port 8000
127.0.0.1 bar.wp.cloud # Port 8001
127.0.0.1 new-domain.wp.cloud # Port 8002
# END WordPress Studio

# Other entries
192.168.1.1 router`
);
} );

it( 'should remove the old domain if new domain is undefined', async () => {
readFileCallbackMock.mockResolvedValueOnce( sampleHostsContent );

await updateDomainInHosts( 'foo.wp.cloud', undefined, 8000 );

expect( readFile ).toHaveBeenCalled();
expect( writeFile ).toHaveBeenCalled();

const newContent = ( writeFile as unknown as jest.Mock ).mock.calls[ 0 ][ 1 ];

expect( newContent ).toEqual(
`127.0.0.1 localhost
::1 localhost

# Some comment

# BEGIN WordPress Studio
127.0.0.1 bar.wp.cloud # Port 8001
# END WordPress Studio

# Other entries
192.168.1.1 router`
);
} );

it( 'should not modify the hosts file if old and new domains are the same', async () => {
readFileCallbackMock.mockResolvedValueOnce( sampleHostsContent );

await updateDomainInHosts( 'foo.wp.cloud', 'foo.wp.cloud', 8000 );

expect( readFile ).not.toHaveBeenCalled();
expect( writeFile ).not.toHaveBeenCalled();
} );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ export const updateSiteUrlToLocal = async ( siteId: string ) => {
} );

if ( ! currentSiteUrl ) {
console.error( 'Failed to fetch site URL after import' );
console.error( 'Failed to fetch site URL' );
Comment thread
youknowriad marked this conversation as resolved.
return;
}

const studioUrl = getSiteUrl( server.details );
const oldUrl = currentSiteUrl.trim();
if ( studioUrl === oldUrl ) {
return;
}
const urlWithoutProtocol = oldUrl.replace( /^https?:\/\//, '' );

const oldUrlVariants = [
Expand Down
Loading