From 8ec7c37715e410c5564c512162be03383b577e39 Mon Sep 17 00:00:00 2001
From: Ildar Kamalov <i.kamalov@adguard.com>
Date: Wed, 22 Jan 2020 17:25:50 +0300
Subject: [PATCH] + client: handle check host

---
 client/src/__locales/en.json                 |  14 +-
 client/src/actions/filtering.js              |  20 +++
 client/src/api/Api.js                        |   7 +
 client/src/components/Filters/Check/Info.js  | 127 +++++++++++++++++++
 client/src/components/Filters/Check/index.js |  95 ++++++++++++++
 client/src/components/Filters/UserRules.js   |   2 +-
 client/src/components/Filters/index.js       |  21 ++-
 client/src/components/ui/Card.css            |  12 ++
 client/src/containers/Filters.js             |   2 +
 client/src/helpers/constants.js              |   4 +
 client/src/helpers/helpers.js                |   8 ++
 client/src/reducers/filtering.js             |  10 ++
 12 files changed, 318 insertions(+), 4 deletions(-)
 create mode 100644 client/src/components/Filters/Check/Info.js
 create mode 100644 client/src/components/Filters/Check/index.js

diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index b594c20a..9e11009a 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -446,5 +446,17 @@
     "autofix_warning_result": "As a result all DNS requests from your system will be processed by AdGuardHome by default.",
     "tags_title": "Tags",
     "tags_desc": "You can select the tags that correspond to the client. Tags can be included in the filtering rules and allow you to apply them more accurately. <0>Learn more</0>",
-    "form_select_tags": "Select client tags"
+    "form_select_tags": "Select client tags",
+    "check_title": "Check the filtering",
+    "check_desc": "Check if the host name is filtered",
+    "check": "Check",
+    "form_enter_host": "Enter a host name",
+    "filtered_custom_rules": "Filtered by Custom filtering rules",
+    "host_whitelisted": "The host is whitelisted",
+    "check_ip": "IP addresses: {{ip}}",
+    "check_cname": "CNAME: {{cname}}",
+    "check_reason": "Reason: {{reason}}",
+    "check_rule": "Rule: {{rule}}",
+    "check_service": "Service name: {{service}}",
+    "check_not_found": "Doesn't exist in any filter"
 }
diff --git a/client/src/actions/filtering.js b/client/src/actions/filtering.js
index 117c22ee..ee8d5c03 100644
--- a/client/src/actions/filtering.js
+++ b/client/src/actions/filtering.js
@@ -161,3 +161,23 @@ export const setFiltersConfig = config => async (dispatch, getState) => {
         dispatch(setFiltersConfigFailure());
     }
 };
+
+export const checkHostRequest = createAction('CHECK_HOST_REQUEST');
+export const checkHostFailure = createAction('CHECK_HOST_FAILURE');
+export const checkHostSuccess = createAction('CHECK_HOST_SUCCESS');
+
+export const checkHost = host => async (dispatch) => {
+    dispatch(checkHostRequest());
+    try {
+        const data = await apiClient.checkHost(host);
+        const [hostname] = Object.values(host);
+
+        dispatch(checkHostSuccess({
+            hostname,
+            ...data,
+        }));
+    } catch (error) {
+        dispatch(addErrorToast({ error }));
+        dispatch(checkHostFailure());
+    }
+};
diff --git a/client/src/api/Api.js b/client/src/api/Api.js
index 3d215144..97d2cc6f 100644
--- a/client/src/api/Api.js
+++ b/client/src/api/Api.js
@@ -82,6 +82,7 @@ class Api {
     FILTERING_REFRESH = { path: 'filtering/refresh', method: 'POST' };
     FILTERING_SET_URL = { path: 'filtering/set_url', method: 'POST' };
     FILTERING_CONFIG = { path: 'filtering/config', method: 'POST' };
+    FILTERING_CHECK_HOST = { path: 'filtering/check_host', method: 'GET' };
 
     getFilteringStatus() {
         const { path, method } = this.FILTERING_STATUS;
@@ -141,6 +142,12 @@ class Api {
         return this.makeRequest(path, method, parameters);
     }
 
+    checkHost(params) {
+        const { path, method } = this.FILTERING_CHECK_HOST;
+        const url = getPathWithQueryString(path, params);
+        return this.makeRequest(url, method);
+    }
+
     // Parental
     PARENTAL_STATUS = { path: 'parental/status', method: 'GET' };
     PARENTAL_ENABLE = { path: 'parental/enable', method: 'POST' };
diff --git a/client/src/components/Filters/Check/Info.js b/client/src/components/Filters/Check/Info.js
new file mode 100644
index 00000000..d3cd0136
--- /dev/null
+++ b/client/src/components/Filters/Check/Info.js
@@ -0,0 +1,127 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withNamespaces } from 'react-i18next';
+
+import { checkFiltered, checkRewrite, checkBlackList, checkNotFilteredNotFound, checkWhiteList } from '../../../helpers/helpers';
+
+const getFilterName = (id, filters, t) => {
+    if (id === 0) {
+        return t('filtered_custom_rules');
+    }
+
+    const filter = filters.find(filter => filter.id === id);
+
+    if (filter && filter.name) {
+        return t('query_log_filtered', { filter: filter.name });
+    }
+
+    return '';
+};
+
+const getTitle = (reason, filterName, t) => {
+    if (checkNotFilteredNotFound(reason)) {
+        return t('check_not_found');
+    }
+
+    if (checkRewrite(reason)) {
+        return t('rewrite_applied');
+    }
+
+    if (checkBlackList(reason)) {
+        return filterName;
+    }
+
+    if (checkWhiteList(reason)) {
+        return (
+            <Fragment>
+                <div>
+                    {t('host_whitelisted')}
+                </div>
+                <div>
+                    {filterName}
+                </div>
+            </Fragment>
+        );
+    }
+
+    return (
+        <Fragment>
+            <div>
+                {t('check_reason', { reason })}
+            </div>
+            <div>
+                {filterName}
+            </div>
+        </Fragment>
+    );
+};
+
+const getColor = (reason) => {
+    if (checkFiltered(reason)) {
+        return 'red';
+    } else if (checkRewrite(reason)) {
+        return 'blue';
+    } else if (checkWhiteList(reason)) {
+        return 'green';
+    }
+
+    return '';
+};
+
+const Info = ({
+    filters,
+    hostname,
+    reason,
+    filter_id,
+    rule,
+    service_name,
+    cname,
+    ip_addrs,
+    t,
+}) => {
+    const filterName = getFilterName(filter_id, filters, t);
+    const title = getTitle(reason, filterName, t);
+    const color = getColor(reason);
+
+    return (
+        <div className={`card mb-0 p-3 ${color}`}>
+            <div>
+                <strong>{hostname}</strong>
+            </div>
+
+            <div>{title}</div>
+
+            {rule && (
+                <div>{t('check_rule', { rule })}</div>
+            )}
+
+            {service_name && (
+                <div>{t('check_service', { service: service_name })}</div>
+            )}
+
+            {cname && (
+                <div>{t('check_cname', { cname })}</div>
+            )}
+
+            {ip_addrs && (
+                <div>
+                    {t('check_ip', { ip: ip_addrs.join(', ') })}
+                </div>
+            )}
+        </div>
+    );
+};
+
+Info.propTypes = {
+    filters: PropTypes.array.isRequired,
+    hostname: PropTypes.string.isRequired,
+    reason: PropTypes.string.isRequired,
+    filter_id: PropTypes.number,
+    rule: PropTypes.string,
+    service_name: PropTypes.string,
+    cname: PropTypes.string,
+    ip_addrs: PropTypes.array,
+    t: PropTypes.func.isRequired,
+};
+
+export default withNamespaces()(Info);
diff --git a/client/src/components/Filters/Check/index.js b/client/src/components/Filters/Check/index.js
new file mode 100644
index 00000000..13011157
--- /dev/null
+++ b/client/src/components/Filters/Check/index.js
@@ -0,0 +1,95 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { Trans, withNamespaces } from 'react-i18next';
+import { Field, reduxForm } from 'redux-form';
+import flow from 'lodash/flow';
+import Card from '../../ui/Card';
+
+import { renderInputField } from '../../../helpers/form';
+import Info from './Info';
+
+const Check = (props) => {
+    const {
+        t,
+        handleSubmit,
+        pristine,
+        invalid,
+        processing,
+        check,
+        filters,
+    } = props;
+
+    const {
+        hostname,
+        reason,
+        filter_id,
+        rule,
+        service_name,
+        cname,
+        ip_addrs,
+    } = check;
+
+    return (
+        <Card
+            title={t('check_title')}
+            subtitle={t('check_desc')}
+        >
+            <form onSubmit={handleSubmit}>
+                <div className="row">
+                    <div className="col-12 col-md-6">
+                        <div className="input-group">
+                            <Field
+                                id="name"
+                                name="name"
+                                component={renderInputField}
+                                type="text"
+                                className="form-control"
+                                placeholder={t('form_enter_host')}
+                            />
+                            <span className="input-group-append">
+                                <button
+                                    className="btn btn-success btn-standard btn-large"
+                                    type="submit"
+                                    onClick={this.handleSubmit}
+                                    disabled={pristine || invalid || processing}
+                                >
+                                    <Trans>check</Trans>
+                                </button>
+                            </span>
+                        </div>
+                        {check.hostname && (
+                            <Fragment>
+                                <hr/>
+                                <Info
+                                    filters={filters}
+                                    hostname={hostname}
+                                    reason={reason}
+                                    filter_id={filter_id}
+                                    rule={rule}
+                                    service_name={service_name}
+                                    cname={cname}
+                                    ip_addrs={ip_addrs}
+                                />
+                            </Fragment>
+                        )}
+                    </div>
+                </div>
+            </form>
+        </Card>
+    );
+};
+
+Check.propTypes = {
+    t: PropTypes.func.isRequired,
+    handleSubmit: PropTypes.func.isRequired,
+    pristine: PropTypes.bool.isRequired,
+    invalid: PropTypes.bool.isRequired,
+    processing: PropTypes.bool.isRequired,
+    check: PropTypes.object.isRequired,
+    filters: PropTypes.array.isRequired,
+};
+
+export default flow([
+    withNamespaces(),
+    reduxForm({ form: 'domainCheckForm' }),
+])(Check);
diff --git a/client/src/components/Filters/UserRules.js b/client/src/components/Filters/UserRules.js
index a2f10507..312d175c 100644
--- a/client/src/components/Filters/UserRules.js
+++ b/client/src/components/Filters/UserRules.js
@@ -26,7 +26,7 @@ class UserRules extends Component {
                     />
                     <div className="card-actions">
                         <button
-                            className="btn btn-success btn-standard"
+                            className="btn btn-success btn-standard btn-large"
                             type="submit"
                             onClick={this.handleSubmit}
                         >
diff --git a/client/src/components/Filters/index.js b/client/src/components/Filters/index.js
index 0fb4efdf..3f8232f6 100644
--- a/client/src/components/Filters/index.js
+++ b/client/src/components/Filters/index.js
@@ -8,8 +8,9 @@ import Card from '../ui/Card';
 import CellWrap from '../ui/CellWrap';
 import UserRules from './UserRules';
 import Modal from './Modal';
-import { formatDetailedDateTime } from '../../helpers/helpers';
+import Check from './Check';
 
+import { formatDetailedDateTime } from '../../helpers/helpers';
 import { MODAL_TYPE } from '../../helpers/constants';
 
 class Filters extends Component {
@@ -76,6 +77,10 @@ class Filters extends Component {
         return { name: '', url: '' };
     };
 
+    handleCheck = (values) => {
+        this.props.checkHost(values);
+    }
+
     columns = [
         {
             Header: <Trans>enabled_table_header</Trans>,
@@ -180,6 +185,8 @@ class Filters extends Component {
             processingFilters,
             modalType,
             modalFilterUrl,
+            processingCheck,
+            check,
         } = filtering;
 
         const currentFilterData = this.getFilter(modalFilterUrl, filters);
@@ -216,7 +223,7 @@ class Filters extends Component {
                                 />
                                 <div className="card-actions">
                                     <button
-                                        className="btn btn-success btn-standard mr-2"
+                                        className="btn btn-success btn-standard mr-2 btn-large"
                                         type="submit"
                                         onClick={() =>
                                             toggleFilteringModal({ type: MODAL_TYPE.ADD })
@@ -242,6 +249,14 @@ class Filters extends Component {
                                 handleRulesSubmit={this.handleRulesSubmit}
                             />
                         </div>
+                        <div className="col-md-12">
+                            <Check
+                                filters={filters}
+                                check={check}
+                                onSubmit={this.handleCheck}
+                                processing={processingCheck}
+                            />
+                        </div>
                     </div>
                 </div>
                 <Modal
@@ -274,6 +289,7 @@ Filters.propTypes = {
         processingConfigFilter: PropTypes.bool.isRequired,
         processingRemoveFilter: PropTypes.bool.isRequired,
         modalType: PropTypes.string.isRequired,
+        processingCheck: PropTypes.bool.isRequired,
     }),
     removeFilter: PropTypes.func.isRequired,
     toggleFilterStatus: PropTypes.func.isRequired,
@@ -282,6 +298,7 @@ Filters.propTypes = {
     handleRulesChange: PropTypes.func.isRequired,
     refreshFilters: PropTypes.func.isRequired,
     editFilter: PropTypes.func.isRequired,
+    checkHost: PropTypes.func.isRequired,
     t: PropTypes.func.isRequired,
 };
 
diff --git a/client/src/components/ui/Card.css b/client/src/components/ui/Card.css
index 176b0160..577fa60a 100644
--- a/client/src/components/ui/Card.css
+++ b/client/src/components/ui/Card.css
@@ -112,3 +112,15 @@
         font-size: 14px;
     }
 }
+
+.card .red {
+    background-color: #fff4f2;
+}
+
+.card .green {
+    background-color: #f1faf3;
+}
+
+.card .blue {
+    background-color: #ecf7ff;
+}
diff --git a/client/src/containers/Filters.js b/client/src/containers/Filters.js
index d061cb70..0b66b632 100644
--- a/client/src/containers/Filters.js
+++ b/client/src/containers/Filters.js
@@ -9,6 +9,7 @@ import {
     refreshFilters,
     handleRulesChange,
     editFilter,
+    checkHost,
 } from '../actions/filtering';
 import Filters from '../components/Filters';
 
@@ -28,6 +29,7 @@ const mapDispatchToProps = {
     refreshFilters,
     handleRulesChange,
     editFilter,
+    checkHost,
 };
 
 export default connect(
diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js
index 65f8cc5d..1f1ce1a4 100644
--- a/client/src/helpers/constants.js
+++ b/client/src/helpers/constants.js
@@ -349,10 +349,14 @@ export const ENCRYPTION_SOURCE = {
 export const FILTERED_STATUS = {
     FILTERED_BLACK_LIST: 'FilteredBlackList',
     NOT_FILTERED_WHITE_LIST: 'NotFilteredWhiteList',
+    NOT_FILTERED_NOT_FOUND: 'NotFilteredNotFound',
     FILTERED_BLOCKED_SERVICE: 'FilteredBlockedService',
     REWRITE: 'Rewrite',
 };
 
+export const FILTERED = 'Filtered';
+export const NOT_FILTERED = 'NotFiltered';
+
 export const STATS_INTERVALS_DAYS = [1, 7, 30, 90];
 
 export const QUERY_LOG_INTERVALS_DAYS = [1, 7, 30, 90];
diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js
index fefd27fa..795ec054 100644
--- a/client/src/helpers/helpers.js
+++ b/client/src/helpers/helpers.js
@@ -22,6 +22,8 @@ import {
     DEFAULT_DATE_FORMAT_OPTIONS,
     DETAILED_DATE_FORMAT_OPTIONS,
     DEFAULT_LANGUAGE,
+    FILTERED_STATUS,
+    FILTERED,
 } from './constants';
 
 /**
@@ -418,3 +420,9 @@ export const createOnBlurHandler = (event, input, normalizeOnBlur) => (
     normalizeOnBlur
         ? input.onBlur(normalizeOnBlur(event.target.value))
         : input.onBlur());
+
+export const checkFiltered = reason => reason.indexOf(FILTERED) === 0;
+export const checkRewrite = reason => reason === FILTERED_STATUS.REWRITE;
+export const checkBlackList = reason => reason === FILTERED_STATUS.FILTERED_BLACK_LIST;
+export const checkWhiteList = reason => reason === FILTERED_STATUS.NOT_FILTERED_WHITE_LIST;
+export const checkNotFilteredNotFound = reason => reason === FILTERED_STATUS.NOT_FILTERED_NOT_FOUND;
diff --git a/client/src/reducers/filtering.js b/client/src/reducers/filtering.js
index f3f3d894..da298426 100644
--- a/client/src/reducers/filtering.js
+++ b/client/src/reducers/filtering.js
@@ -79,6 +79,14 @@ const filtering = handleActions(
             ...payload,
             processingSetConfig: false,
         }),
+
+        [actions.checkHostRequest]: state => ({ ...state, processingCheck: true }),
+        [actions.checkHostFailure]: state => ({ ...state, processingCheck: false }),
+        [actions.checkHostSuccess]: (state, { payload }) => ({
+            ...state,
+            check: payload,
+            processingCheck: false,
+        }),
     },
     {
         isModalOpen: false,
@@ -89,6 +97,7 @@ const filtering = handleActions(
         processingConfigFilter: false,
         processingRemoveFilter: false,
         processingSetConfig: false,
+        processingCheck: false,
         isFilterAdded: false,
         filters: [],
         userRules: '',
@@ -96,6 +105,7 @@ const filtering = handleActions(
         enabled: true,
         modalType: '',
         modalFilterUrl: '',
+        check: {},
     },
 );