diff --git a/client/my-sites/site-settings/allow-list.jsx b/client/my-sites/site-settings/allow-list.jsx
new file mode 100644
index 0000000000000..b665ec4035729
--- /dev/null
+++ b/client/my-sites/site-settings/allow-list.jsx
@@ -0,0 +1,165 @@
+import { Button, Card } from '@automattic/components';
+import { ToggleControl } from '@wordpress/components';
+import { localize } from 'i18n-calypso';
+import { includes, some } from 'lodash';
+import PropTypes from 'prop-types';
+import { Component } from 'react';
+import FormFieldset from 'calypso/components/forms/form-fieldset';
+import FormLegend from 'calypso/components/forms/form-legend';
+import FormSettingExplanation from 'calypso/components/forms/form-setting-explanation';
+import FormTextarea from 'calypso/components/forms/form-textarea';
+
+class AllowList extends Component {
+ static propTypes = {
+ fields: PropTypes.object.isRequired,
+ isRequestingSettings: PropTypes.bool,
+ isSavingSettings: PropTypes.bool,
+ onChangeField: PropTypes.func.isRequired,
+ setFieldValue: PropTypes.func.isRequired,
+ };
+
+ static defaultProps = {
+ fields: {},
+ isRequestingSettings: true,
+ isSavingSettings: false,
+ };
+
+ togglingAllowListSupported = () => {
+ return this.props.settings.jetpack_waf_ip_allow_list_enabled !== undefined;
+ };
+
+ showAllowList = () => {
+ return (
+ ! this.togglingAllowListSupported() || this.props.fields.jetpack_waf_ip_allow_list_enabled
+ );
+ };
+
+ handleAddToAllowedList = () => {
+ const { setFieldValue } = this.props;
+ let allowedIps = this.getProtectAllowedIps().trimEnd();
+
+ if ( allowedIps.length ) {
+ allowedIps += '\n';
+ }
+
+ setFieldValue( 'jetpack_waf_ip_allow_list', allowedIps + this.getIpAddress() );
+ };
+
+ getIpAddress() {
+ if ( window.app && window.app.clientIp ) {
+ return window.app.clientIp;
+ }
+
+ return null;
+ }
+
+ getProtectAllowedIps() {
+ const { jetpack_waf_ip_allow_list } = this.props.fields;
+ return jetpack_waf_ip_allow_list || '';
+ }
+
+ isIpAddressAllowed() {
+ const ipAddress = this.getIpAddress();
+ if ( ! ipAddress ) {
+ return false;
+ }
+
+ const allowedIps = this.getProtectAllowedIps().split( '\n' );
+
+ return (
+ includes( allowedIps, ipAddress ) ||
+ some( allowedIps, ( entry ) => {
+ if ( entry.indexOf( '-' ) < 0 ) {
+ return false;
+ }
+
+ const range = entry.split( '-' ).map( ( ip ) => ip.trim() );
+ return includes( range, ipAddress );
+ } )
+ );
+ }
+
+ disableForm() {
+ return this.props.isRequestingSettings || this.props.isSavingSettings;
+ }
+
+ render() {
+ const { translate } = this.props;
+ const ipAddress = this.getIpAddress();
+ const isIpAllowed = this.isIpAddressAllowed();
+
+ return (
+
+
+
+ { this.togglingAllowListSupported() ? (
+
+ ) : (
+ { translate( 'Always allow specific IP addresses' ) }
+ ) }{ ' ' }
+
+ { translate(
+ "IP addresses added to this list will never be blocked by Jetpack's security features."
+ ) }
+
+ { this.showAllowList() && (
+
+
+
+ { translate(
+ 'IPv4 and IPv6 are acceptable. ' +
+ 'To specify a range, enter the low value and high value separated by a dash. ' +
+ 'Example: 12.12.12.1-12.12.12.100'
+ ) }
+
+
+ { translate( 'Your current IP address: {{strong}}%(IP)s{{/strong}}{{br/}}', {
+ args: {
+ IP: ipAddress || translate( 'Unknown IP address' ),
+ },
+ components: {
+ strong: ,
+ br: ,
+ },
+ } ) }
+
+ { ipAddress && (
+
+ { isIpAllowed
+ ? translate( 'Already in list of allowed IPs' )
+ : translate( 'Add to list of allowed IPs' ) }
+
+ ) }
+
+
+ ) }
+
+
+
+ );
+ }
+}
+
+export default localize( AllowList );
diff --git a/client/my-sites/site-settings/firewall.jsx b/client/my-sites/site-settings/firewall.jsx
new file mode 100644
index 0000000000000..b80aa9a94229d
--- /dev/null
+++ b/client/my-sites/site-settings/firewall.jsx
@@ -0,0 +1,269 @@
+import { PRODUCT_JETPACK_SCAN, WPCOM_FEATURES_SCAN } from '@automattic/calypso-products';
+import { Card } from '@automattic/components';
+import { ToggleControl } from '@wordpress/components';
+import { localize } from 'i18n-calypso';
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import { Component } from 'react';
+import { connect } from 'react-redux';
+import { UpsellNudge } from 'calypso/blocks/upsell-nudge';
+import QueryJetpackConnection from 'calypso/components/data/query-jetpack-connection';
+import FormFieldset from 'calypso/components/forms/form-fieldset';
+import FormSettingExplanation from 'calypso/components/forms/form-setting-explanation';
+import FormTextarea from 'calypso/components/forms/form-textarea';
+import { activateModule } from 'calypso/state/jetpack/modules/actions';
+import getJetpackModule from 'calypso/state/selectors/get-jetpack-module';
+import isFetchingJetpackModules from 'calypso/state/selectors/is-fetching-jetpack-modules';
+import isJetpackModuleActive from 'calypso/state/selectors/is-jetpack-module-active';
+import isJetpackModuleUnavailableInDevelopmentMode from 'calypso/state/selectors/is-jetpack-module-unavailable-in-development-mode';
+import isJetpackSiteInDevelopmentMode from 'calypso/state/selectors/is-jetpack-site-in-development-mode';
+import siteHasFeature from 'calypso/state/selectors/site-has-feature';
+import { getSelectedSiteId, getSelectedSiteSlug } from 'calypso/state/ui/selectors';
+import AllowList from './allow-list';
+import Protect from './protect';
+import SettingsSectionHeader from './settings-section-header';
+
+class Firewall extends Component {
+ static propTypes = {
+ fields: PropTypes.object.isRequired,
+ handleSubmitForm: PropTypes.func.isRequired,
+ isAtomic: PropTypes.bool.isRequired,
+ isRequestingSettings: PropTypes.bool.isRequired,
+ isSavingSettings: PropTypes.bool.isRequired,
+ isSimple: PropTypes.bool.isRequired,
+ isVip: PropTypes.bool.isRequired,
+ onChangeField: PropTypes.func.isRequired,
+ setFieldValue: PropTypes.func.isRequired,
+ settings: PropTypes.object.isRequired,
+ };
+
+ static defaultProps = {
+ isSavingSettings: false,
+ isRequestingSettings: true,
+ fields: {},
+ settings: {},
+ };
+
+ disableForm() {
+ return (
+ this.props.moduleDetailsLoading ||
+ this.props.isRequestingSettings ||
+ this.props.isSavingSettings
+ );
+ }
+
+ wafModuleSupported = () => {
+ return (
+ this.props.firewallModuleAvailable &&
+ ! this.props.isAtomic &&
+ ! this.props.isVip &&
+ ! this.props.isSimple
+ );
+ };
+
+ disableWafForm = () => {
+ return this.disableForm() || ! this.wafModuleSupported();
+ };
+
+ ensureWafModuleActive = () => {
+ if ( this.wafModuleSupported() && ! this.props.firewallModuleActive ) {
+ this.props.activateModule( this.props.selectedSiteId, 'waf', true );
+ }
+ };
+
+ hasAutomaticRulesInstalled = () => {
+ return !! this.props.settings.jetpack_waf_automatic_rules_last_updated_timestamp;
+ };
+
+ canUpdateAutomaticRules = () => {
+ return this.props.hasRequiredFeature || this.hasAutomaticRulesInstalled();
+ };
+
+ automaticRulesLastUpdated = () => {
+ const timestamp = parseInt(
+ this.props.settings.jetpack_waf_automatic_rules_last_updated_timestamp
+ );
+ if ( timestamp ) {
+ return moment( timestamp * 1000 ).format( 'MMMM D, YYYY h:mm A' );
+ }
+ return '';
+ };
+
+ handleAutomaticRulesToggle = ( event ) => {
+ this.ensureWafModuleActive();
+ this.props.handleAutosavingToggle( 'jetpack_waf_automatic_rules' )( event );
+ };
+
+ handleBlockListToggle = ( event ) => {
+ this.ensureWafModuleActive();
+ this.props.handleAutosavingToggle( 'jetpack_waf_ip_block_list_enabled' )( event );
+ };
+
+ handleSubmitForm = ( event ) => {
+ this.ensureWafModuleActive();
+ return this.props.handleSubmitForm( event );
+ };
+
+ render() {
+ const {
+ dirtyFields,
+ disableProtect,
+ fields,
+ handleAutosavingToggle,
+ hasRequiredFeature,
+ isRequestingSettings,
+ isSavingSettings,
+ onChangeField,
+ selectedSiteId,
+ selectedSiteSlug,
+ setFieldValue,
+ settings,
+ translate,
+ } = this.props;
+
+ return (
+
+
+
+
+
+ { /* Automatic Rules */ }
+ { this.wafModuleSupported() && this.canUpdateAutomaticRules() && (
+
+
+
+ { this.automaticRulesLastUpdated() && (
+
+ { translate( 'Automatic security rules installed. Last updated on %(date)s.', {
+ args: {
+ date: this.automaticRulesLastUpdated(),
+ },
+ } ) }
+
+ ) }
+
+ { translate(
+ 'Block untrusted traffic sources by scanning every request made to your site. Jetpackās advanced security rules are automatically kept up-to-date to protect your site from the latest threats.'
+ ) }
+
+
+
+ ) }
+
+ { /* Upgrade Prompt */ }
+ { ! hasRequiredFeature && this.wafModuleSupported() && (
+
+ ) }
+
+ { /* Brute Force Login Protection */ }
+
+
+ { /* IP Block List */ }
+ { this.wafModuleSupported() && settings.jetpack_waf_ip_block_list_enabled !== undefined && (
+
+
+
+
+ { translate(
+ 'IP addresses added to this list will be blocked from accessing your site.'
+ ) }
+
+ { fields.jetpack_waf_ip_block_list_enabled && (
+
+
+
+ ) }
+
+
+ ) }
+
+ { /* IP Allow List */ }
+
+
+ );
+ }
+}
+
+export default connect(
+ ( state ) => {
+ const selectedSiteSlug = getSelectedSiteSlug( state );
+ const selectedSiteId = getSelectedSiteId( state );
+ const moduleDetailsLoading = isFetchingJetpackModules( state, selectedSiteId );
+ const firewallModuleDetails = getJetpackModule( state, selectedSiteId, 'waf' );
+ const siteInDevMode = isJetpackSiteInDevelopmentMode( state, selectedSiteId );
+ const moduleUnavailableInDevMode = isJetpackModuleUnavailableInDevelopmentMode(
+ state,
+ selectedSiteId,
+ 'waf'
+ );
+ const hasRequiredFeature = siteHasFeature( state, selectedSiteId, WPCOM_FEATURES_SCAN );
+
+ return {
+ selectedSiteId,
+ selectedSiteSlug,
+ firewallModuleActive: !! isJetpackModuleActive( state, selectedSiteId, 'waf' ),
+ firewallModuleAvailable:
+ firewallModuleDetails !== null && ( ! siteInDevMode || ! moduleUnavailableInDevMode ),
+ moduleDetailsLoading,
+ hasRequiredFeature,
+ };
+ },
+ {
+ activateModule,
+ }
+)( localize( Firewall ) );
diff --git a/client/my-sites/site-settings/form-security.jsx b/client/my-sites/site-settings/form-security.jsx
index b2cfe83710925..ee056cdba7570 100644
--- a/client/my-sites/site-settings/form-security.jsx
+++ b/client/my-sites/site-settings/form-security.jsx
@@ -9,8 +9,10 @@ import isJetpackModuleActive from 'calypso/state/selectors/is-jetpack-module-act
import isJetpackModuleUnavailableInDevelopmentMode from 'calypso/state/selectors/is-jetpack-module-unavailable-in-development-mode';
import isJetpackSiteInDevelopmentMode from 'calypso/state/selectors/is-jetpack-site-in-development-mode';
import isSiteAutomatedTransfer from 'calypso/state/selectors/is-site-automated-transfer';
+import isVipSite from 'calypso/state/selectors/is-vip-site';
+import { isSimpleSite } from 'calypso/state/sites/selectors';
import { getSelectedSiteId } from 'calypso/state/ui/selectors';
-import Protect from './protect';
+import Firewall from './firewall';
import SpamFilteringSettings from './spam-filtering-settings';
import Sso from './sso';
import wrapSettingsForm from './wrap-settings-form';
@@ -24,6 +26,8 @@ class SiteSettingsFormSecurity extends Component {
handleAutosavingToggle,
handleSubmitForm,
isAtomic,
+ isSimple,
+ isVip,
isRequestingSettings,
isSavingSettings,
onChangeField,
@@ -33,6 +37,7 @@ class SiteSettingsFormSecurity extends Component {
settings,
siteId,
translate,
+ activateModule,
} = this.props;
const disableProtect = ! protectModuleActive || protectModuleUnavailable;
const disableSpamFiltering = ! fields.akismet || akismetUnavailable;
@@ -44,24 +49,25 @@ class SiteSettingsFormSecurity extends Component {
className="site-settings__security-settings"
>
+
-
-
-
-
{ ! isAtomic && (
{
const siteId = getSelectedSiteId( state );
const isAtomic = isSiteAutomatedTransfer( state, siteId );
+ const isSimple = isSimpleSite( state, siteId );
+ const isVip = isVipSite( state, siteId );
const protectModuleActive = !! isJetpackModuleActive( state, siteId, 'protect' );
const siteInDevMode = isJetpackSiteInDevelopmentMode( state, siteId );
const protectIsUnavailableInDevMode = isJetpackModuleUnavailableInDevelopmentMode(
@@ -114,6 +122,8 @@ const connectComponent = connect( ( state ) => {
return {
siteId,
isAtomic,
+ isSimple,
+ isVip,
protectModuleActive,
protectModuleUnavailable: siteInDevMode && protectIsUnavailableInDevMode,
akismetUnavailable: siteInDevMode && akismetIsUnavailableInDevMode,
@@ -125,6 +135,12 @@ const getFormSettings = ( settings ) =>
'akismet',
'protect',
'jetpack_protect_global_whitelist',
+ 'jetpack_waf_automatic_rules',
+ 'jetpack_waf_ip_allow_list',
+ 'jetpack_waf_ip_allow_list_enabled',
+ 'jetpack_waf_ip_block_list',
+ 'jetpack_waf_ip_block_list_enabled',
+ 'jetpack_waf_automatic_rules_last_updated_timestamp',
'sso',
'jetpack_sso_match_by_email',
'jetpack_sso_require_two_step',
diff --git a/client/my-sites/site-settings/protect.jsx b/client/my-sites/site-settings/protect.jsx
index 22a8158504f7d..f100d786ca1e7 100644
--- a/client/my-sites/site-settings/protect.jsx
+++ b/client/my-sites/site-settings/protect.jsx
@@ -1,14 +1,10 @@
-import { Button, FoldableCard, FormLabel } from '@automattic/components';
+import { Card } from '@automattic/components';
import { localize } from 'i18n-calypso';
-import { includes, some } from 'lodash';
import PropTypes from 'prop-types';
import { Component } from 'react';
import { connect } from 'react-redux';
-import QueryJetpackConnection from 'calypso/components/data/query-jetpack-connection';
import FormFieldset from 'calypso/components/forms/form-fieldset';
import FormSettingExplanation from 'calypso/components/forms/form-setting-explanation';
-import FormTextarea from 'calypso/components/forms/form-textarea';
-import SupportInfo from 'calypso/components/support-info';
import JetpackModuleToggle from 'calypso/my-sites/site-settings/jetpack-module-toggle';
import isJetpackModuleActive from 'calypso/state/selectors/is-jetpack-module-active';
import isJetpackModuleUnavailableInDevelopmentMode from 'calypso/state/selectors/is-jetpack-module-unavailable-in-development-mode';
@@ -22,6 +18,7 @@ class Protect extends Component {
isSavingSettings: PropTypes.bool,
isRequestingSettings: PropTypes.bool,
fields: PropTypes.object,
+ disableProtect: PropTypes.bool,
};
static defaultProps = {
@@ -30,136 +27,31 @@ class Protect extends Component {
fields: {},
};
- handleAddToAllowedList = () => {
- const { setFieldValue } = this.props;
- let allowedIps = this.getProtectAllowedIps().trimEnd();
-
- if ( allowedIps.length ) {
- allowedIps += '\n';
- }
-
- setFieldValue( 'jetpack_protect_global_whitelist', allowedIps + this.getIpAddress() );
- };
-
- getIpAddress() {
- if ( window.app && window.app.clientIp ) {
- return window.app.clientIp;
- }
-
- return null;
- }
-
- getProtectAllowedIps() {
- const { jetpack_protect_global_whitelist } = this.props.fields;
- return jetpack_protect_global_whitelist || '';
- }
-
- isIpAddressAllowed() {
- const ipAddress = this.getIpAddress();
- if ( ! ipAddress ) {
- return false;
- }
-
- const allowedIps = this.getProtectAllowedIps().split( '\n' );
-
- return (
- includes( allowedIps, ipAddress ) ||
- some( allowedIps, ( entry ) => {
- if ( entry.indexOf( '-' ) < 0 ) {
- return false;
- }
-
- const range = entry.split( '-' ).map( ( ip ) => ip.trim() );
- return includes( range, ipAddress );
- } )
- );
- }
-
render() {
const {
isRequestingSettings,
isSavingSettings,
- onChangeField,
- protectModuleActive,
protectModuleUnavailable,
selectedSiteId,
translate,
} = this.props;
- const ipAddress = this.getIpAddress();
- const isIpAllowed = this.isIpAddressAllowed();
- const disabled =
- isRequestingSettings || isSavingSettings || protectModuleUnavailable || ! protectModuleActive;
- const protectToggle = (
-
- );
-
return (
-
-
-
+
-
-
-
- { translate( 'Your current IP address: {{strong}}%(IP)s{{/strong}}{{br/}}', {
- args: {
- IP: ipAddress || translate( 'Unknown IP address' ),
- },
- components: {
- strong: ,
- br: ,
- },
- } ) }
-
- { ipAddress && (
-
- { isIpAllowed
- ? translate( 'Already in list of allowed IPs' )
- : translate( 'Add to list of allowed IPs' ) }
-
- ) }
-
-
-
- { translate( 'Allowed IP addresses' ) }
-
-
-
- { translate(
- 'You may explicitly allow an IP address or series of addresses preventing them from ' +
- 'ever being blocked by Jetpack. IPv4 and IPv6 are acceptable. ' +
- 'To specify a range, enter the low value and high value separated by a dash. ' +
- 'Example: 12.12.12.1-12.12.12.100'
- ) }
-
-
+
+
+ { translate(
+ 'Prevent and block unwanted login attempts from bots and hackers attempting to log in to your website with common username and password combinations.'
+ ) }
+
-
+
);
}
}
diff --git a/client/my-sites/site-settings/style.scss b/client/my-sites/site-settings/style.scss
index c86425b8b5814..00527cb02e655 100644
--- a/client/my-sites/site-settings/style.scss
+++ b/client/my-sites/site-settings/style.scss
@@ -753,3 +753,12 @@ button.site-settings__general-settings-set-guessed-timezone.button.is-borderless
.verbum-comments-toggle {
flex-direction: column;
}
+
+.site-settings__firewall-settings .card:not(:last-child) {
+ margin-bottom: 0;
+}
+
+.site-settings__security-settings-firewall-nudge {
+ margin-bottom: 0;
+ z-index: 1;
+}