Skip to content

Commit

Permalink
Components: Add ThreatModal (#40197)
Browse files Browse the repository at this point in the history
* Add ThreatDetailsModal component and stories

* changelog

* Fix type error

* Fix child component overflow-x styling

* Update scan package changelog

* Add util for fixerState and separate subcomponents

* Generalize component name

* Updates after renaming

* Ensure close button exists for vulns modal

* Fixes and improvements

* ThreatModal: Add user connection gate (#40204)

* Add user connection gate

* Fix stories

* ThreatModal: Add credentials gate (#40205)

* Add credentials gate

* Fix stories

* Fix credentials type

* Fix build issues, and add early returns for gates

Committed via a GitHub action: https://github.com/Automattic/jetpack/actions/runs/11919780318

Upstream-Ref: Automattic/jetpack@c783873
  • Loading branch information
dkmyta authored and matticbot committed Nov 19, 2024
1 parent 743ff1b commit cd864af
Show file tree
Hide file tree
Showing 12 changed files with 534 additions and 73 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

### This is a list detailing changes for the Jetpack RNA Components package releases.

## [0.61.1-alpha] - unreleased
## [0.62.0-alpha] - unreleased

This is an alpha version! The changes listed here are not final.

### Added
- Adds ThreatModal component and stories

## [0.61.0] - 2024-11-18
### Added
- Gridicon Component: Add support for help-outline icon. [#39867]
Expand Down Expand Up @@ -1222,7 +1225,7 @@ This is an alpha version! The changes listed here are not final.
### Changed
- Update node version requirement to 14.16.1

[0.61.1-alpha]: https://github.com/Automattic/jetpack-components/compare/0.61.0...0.61.1-alpha
[0.62.0-alpha]: https://github.com/Automattic/jetpack-components/compare/0.61.0...0.62.0-alpha
[0.61.0]: https://github.com/Automattic/jetpack-components/compare/0.60.0...0.61.0
[0.60.0]: https://github.com/Automattic/jetpack-components/compare/0.59.0...0.60.0
[0.59.0]: https://github.com/Automattic/jetpack-components/compare/0.58.1...0.59.0
Expand Down
2 changes: 1 addition & 1 deletion components/diff-viewer/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
display: flex;
font-family: "Courier 10 Pitch", Courier, monospace;
flex-direction: row;
overflow-x: scroll;
overflow-x: auto;
white-space: pre;
}

Expand Down
2 changes: 1 addition & 1 deletion components/marked-lines/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
font-family: monospace;
display: flex;
flex-direction: row;
overflow-x: scroll;
overflow-x: auto;
}

.marked-lines__marked-line {
Expand Down
74 changes: 7 additions & 67 deletions components/threat-fixer-button/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Button } from '@automattic/jetpack-components';
import {
type Threat,
fixerIsInError,
fixerIsInProgress,
fixerStatusIsStale,
getFixerState,
getFixerAction,
getFixerMessage,
} from '@automattic/jetpack-scan';
import { Tooltip } from '@wordpress/components';
import { useCallback, useMemo } from '@wordpress/element';
Expand All @@ -30,10 +30,7 @@ export default function ThreatFixerButton( {
className?: string;
} ): JSX.Element {
const fixerState = useMemo( () => {
const inProgress = threat.fixer && fixerIsInProgress( threat.fixer );
const error = threat.fixer && fixerIsInError( threat.fixer );
const stale = threat.fixer && fixerStatusIsStale( threat.fixer );
return { inProgress, error, stale };
return getFixerState( threat.fixer );
}, [ threat.fixer ] );

const tooltipText = useMemo( () => {
Expand All @@ -53,54 +50,7 @@ export default function ThreatFixerButton( {
return __( 'An auto-fixer is in progress.', 'jetpack' );
}

switch ( threat.fixable.fixer ) {
case 'delete':
if ( threat.filename ) {
if ( threat.filename.endsWith( '/' ) ) {
return __( 'Deletes the directory that the infected file is in.', 'jetpack' );
}

if ( threat.signature === 'Core.File.Modification' ) {
return __( 'Deletes the unexpected file in a core WordPress directory.', 'jetpack' );
}

return __( 'Deletes the infected file.', 'jetpack' );
}

if ( threat.extension?.type === 'plugin' ) {
return __( 'Deletes the plugin directory to fix the threat.', 'jetpack' );
}

if ( threat.extension?.type === 'theme' ) {
return __( 'Deletes the theme directory to fix the threat.', 'jetpack' );
}
break;
case 'update':
return __( 'Upgrades the plugin or theme to a newer version.', 'jetpack' );
case 'replace':
case 'rollback':
if ( threat.filename ) {
return threat.signature === 'Core.File.Modification'
? __(
'Replaces the modified core WordPress file with the original clean version from the WordPress source code.',
'jetpack'
)
: __(
'Replaces the infected file with a previously backed up version that is clean.',
'jetpack'
);
}

if ( threat.signature === 'php_hardening_WP_Config_NoSalts_001' ) {
return __(
'Replaces the default salt keys in wp-config.php with unique ones.',
'jetpack'
);
}
break;
default:
return __( 'An auto-fixer is available.', 'jetpack' );
}
return getFixerMessage( threat );
}, [ threat, fixerState ] );

const buttonText = useMemo( () => {
Expand All @@ -112,18 +62,8 @@ export default function ThreatFixerButton( {
return __( 'Error', 'jetpack' );
}

switch ( threat.fixable.fixer ) {
case 'delete':
return __( 'Delete', 'jetpack' );
case 'update':
return __( 'Update', 'jetpack' );
case 'replace':
case 'rollback':
return __( 'Replace', 'jetpack' );
default:
return __( 'Fix', 'jetpack' );
}
}, [ threat.fixable, fixerState.error ] );
return getFixerAction( threat );
}, [ threat, fixerState.error ] );

const handleClick = useCallback(
( event: React.MouseEvent ) => {
Expand Down
65 changes: 65 additions & 0 deletions components/threat-modal/credentials-gate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Text, Button } from '@automattic/jetpack-components';
import { Notice } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import React, { ReactElement } from 'react';
import styles from './styles.module.scss';

const CredentialsGate = ( {
siteCredentialsNeeded,
credentialsIsFetching,
credentialsRedirectUrl,
children,
}: {
siteCredentialsNeeded: boolean;
credentialsIsFetching: boolean;
credentialsRedirectUrl: string;
children: ReactElement;
} ): JSX.Element => {
if ( ! siteCredentialsNeeded ) {
return children;
}

return (
<>
<Notice
status="warning"
isDismissible={ false }
children={
<Text>
{ __(
'Before Jetpack can auto-fix threats on your site, it needs your server credentials.',
'jetpack'
) }
</Text>
}
/>

<Text>
{ __(
'Your server credentials allow Jetpack to access the server that’s powering your website. This information is securely saved and only used to perform fix threats detected on your site.',
'jetpack'
) }
</Text>

<Text>
{ __(
'Once you’ve entered server credentials, Jetpack will be fixing the selected threats.',
'jetpack'
) }
</Text>

<div className={ styles[ 'modal-actions' ] }>
<Button
isExternalLink={ true }
weight="regular"
href={ credentialsRedirectUrl }
isLoading={ credentialsIsFetching }
>
{ __( 'Enter server credentials', 'jetpack' ) }
</Button>
</div>
</>
);
};

export default CredentialsGate;
153 changes: 153 additions & 0 deletions components/threat-modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { Button, ThreatSeverityBadge } from '@automattic/jetpack-components';
import { type Threat, getFixerState } from '@automattic/jetpack-scan';
import { Modal, Notice } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useMemo } from 'react';
import Text from '../text';
import CredentialsGate from './credentials-gate';
import styles from './styles.module.scss';
import ThreatActions from './threat-actions';
import ThreatFixDetails from './threat-fix-details';
import ThreatTechnicalDetails from './threat-technical-details';
import UserConnectionGate from './user-connection-gate';

/**
* ThreatModal component
*
* @param {object} props - The props.
* @param {object} props.threat - The threat.
* @param {boolean} props.isUserConnected - Whether the user is connected.
* @param {boolean} props.hasConnectedOwner - Whether the user has a connected owner.
* @param {boolean} props.userIsConnecting - Whether the user is connecting.
* @param {Function} props.handleConnectUser - The handleConnectUser function.
* @param {object} props.credentials - The credentials.
* @param {boolean} props.credentialsIsFetching - Whether the credentials are fetching.
* @param {string} props.credentialsRedirectUrl - The credentials redirect URL.
* @param {Function} props.handleUpgradeClick - The handleUpgradeClick function.
* @param {Function} props.handleFixThreatClick - The handleFixThreatClick function.
* @param {Function} props.handleIgnoreThreatClick - The handleIgnoreThreatClick function.
* @param {Function} props.handleUnignoreThreatClick - The handleUnignoreThreatClick function.
*
* @return {JSX.Element} The threat modal.
*/
export default function ThreatModal( {
threat,
isUserConnected,
hasConnectedOwner,
userIsConnecting,
handleConnectUser,
credentials,
credentialsIsFetching,
credentialsRedirectUrl,
handleUpgradeClick,
handleFixThreatClick,
handleIgnoreThreatClick,
handleUnignoreThreatClick,
...modalProps
}: {
threat: Threat;
isUserConnected: boolean;
hasConnectedOwner: boolean;
userIsConnecting: boolean;
handleConnectUser: () => void;
credentials: false | Record< string, unknown >[];
credentialsIsFetching: boolean;
credentialsRedirectUrl: string;
handleUpgradeClick?: () => void;
handleFixThreatClick?: ( threats: Threat[] ) => void;
handleIgnoreThreatClick?: ( threats: Threat[] ) => void;
handleUnignoreThreatClick?: ( threats: Threat[] ) => void;
} & React.ComponentProps< typeof Modal > ): JSX.Element {
const userConnectionNeeded = ! isUserConnected || ! hasConnectedOwner;
const siteCredentialsNeeded = ! credentials || credentials.length === 0;

const fixerState = useMemo( () => {
return getFixerState( threat.fixer );
}, [ threat.fixer ] );

const getModalTitle = useMemo( () => {
if ( userConnectionNeeded ) {
return <Text variant="title-small">{ __( 'User connection needed', 'jetpack' ) }</Text>;
}

if ( siteCredentialsNeeded ) {
return <Text variant="title-small">{ __( 'Site credentials needed', 'jetpack' ) }</Text>;
}

return (
<>
<Text variant="title-small">{ threat.title }</Text>
{ !! threat.severity && <ThreatSeverityBadge severity={ threat.severity } /> }
</>
);
}, [ userConnectionNeeded, siteCredentialsNeeded, threat.title, threat.severity ] );

return (
<Modal
size="large"
title={ <div className={ styles.title }>{ getModalTitle }</div> }
{ ...modalProps }
>
<div className={ styles[ 'threat-details' ] }>
<UserConnectionGate
userConnectionNeeded={ userConnectionNeeded }
userIsConnecting={ userIsConnecting }
handleConnectUser={ handleConnectUser }
>
<CredentialsGate
siteCredentialsNeeded={ siteCredentialsNeeded }
credentialsIsFetching={ credentialsIsFetching }
credentialsRedirectUrl={ credentialsRedirectUrl }
>
<>
{ fixerState.error && (
<Notice isDismissible={ false } status="error">
<Text>{ __( 'An error occurred auto-fixing this threat.', 'jetpack' ) }</Text>
</Notice>
) }
{ fixerState.stale && (
<Notice isDismissible={ false } status="error">
<Text>{ __( 'The auto-fixer is taking longer than expected.', 'jetpack' ) }</Text>
</Notice>
) }
{ fixerState.inProgress && ! fixerState.stale && (
<Notice isDismissible={ false } status="success">
<Text>{ __( 'The auto-fixer is in progress.', 'jetpack' ) }</Text>
</Notice>
) }
<div className={ styles.section }>
{ !! threat.description && <Text>{ threat.description }</Text> }

{ !! threat.source && (
<div>
<Button
variant="link"
isExternalLink={ true }
weight="regular"
href={ threat.source }
>
{ __( 'See more technical details of this threat', 'jetpack' ) }
</Button>
</div>
) }
</div>

<ThreatFixDetails threat={ threat } handleUpgradeClick={ handleUpgradeClick } />

<ThreatTechnicalDetails threat={ threat } />

<ThreatActions
threat={ threat }
closeModal={ modalProps.onRequestClose }
handleFixThreatClick={ handleFixThreatClick }
handleIgnoreThreatClick={ handleIgnoreThreatClick }
handleUnignoreThreatClick={ handleUnignoreThreatClick }
fixerState={ fixerState }
/>
</>
</CredentialsGate>
</UserConnectionGate>
</div>
</Modal>
);
}
Loading

0 comments on commit cd864af

Please sign in to comment.