Pull request: 1163 safesearch http api vol.3

Merge in DNS/adguard-home from 1163-safesearch-1-3 to master

Squashed commit of the following:

commit f26c5fb4f7a27dc61b10c28d6672d5307796784c
Merge: e7a1b885 143616ca
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 23 18:45:25 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1163-safesearch-1-3

    # Conflicts:
    #	CHANGELOG.md

commit e7a1b885d6
Merge: 01b73d76 eb5d8a49
Author: Vladislav Abdulmyanov <v.abdulmyanov@adguard.com>
Date:   Wed Mar 22 13:55:23 2023 +0200

    Merge branch '1163-safesearch-1-3' of ssh://bit.adguard.com:7999/dns/adguard-home into 1163-safesearch-1-3

commit 01b73d763c
Author: Vladislav Abdulmyanov <v.abdulmyanov@adguard.com>
Date:   Wed Mar 22 13:52:02 2023 +0200

    client: add safe search extended settings to clients

commit eb5d8a499a
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Mar 22 18:50:23 2023 +0700

    all: docs

commit 2043a8fba7f664ef365ccc5abac14a85035eb4b7
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Mar 22 09:42:50 2023 +0700

    all: docs

commit bb1d2f6c0252891ccac3d3727eb23288a24d4bda
Merge: 95f9fd3d c3edab43
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Mar 22 09:42:00 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1163-safesearch-1-3

commit 95f9fd3dd1e8abcdf1a156e81aff8e52f320f4c3
Author: Vladislav Abdulmyanov <v.abdulmyanov@adguard.com>
Date:   Tue Mar 21 15:25:39 2023 +0200

    client: move to new safe search api

commit ac823a911f0d6ab6f1813d11a0ca082d54cc9131
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 20 22:40:29 2023 +0700

    all: docs

commit aaa287b125c7c7a775b821e0dd272199229a7538
Merge: 16fa7031 48431f8b
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 20 22:39:14 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1163-safesearch-1-3

commit 16fa7031ab2aec31139ace54ffa0155cde8e9135
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 20 22:39:03 2023 +0700

    all: docs

commit 498f7d3cbb842eda218b0fd06fc3bb3601b81f80
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 20 18:59:47 2023 +0700

    filtering: imp code

commit aab7b70e2355ba86577e5156c1d5569b21b4b358
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 20 18:40:18 2023 +0700

    filtering: imp code

commit d2870a18ffdb1d293993487073912168d6b75a38
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Mar 17 21:57:58 2023 +0700

    filtering: imp code

commit 868f5d1ed29c3af702114079e7ffe46e136eb901
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Mar 17 19:06:36 2023 +0700

    all: imp docs

commit f6d70b06ed873684501ce17f647ccf07a85dd50b
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Mar 17 19:05:40 2023 +0700

    filtering: imp code

commit 7cd9a37dde6262a8cf4f0f13f9946e011cc0e2cf
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 16 14:56:51 2023 +0700

    home: imp code

commit 84d8817512e47a517ed2880ffa9dde5ffda1d288
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 16 09:39:41 2023 +0700

    all: safesearch http api
This commit is contained in:
Dimitry Kolyshev 2023-03-23 15:25:58 +03:00
parent 143616ca6e
commit df61741f60
15 changed files with 315 additions and 83 deletions

View File

@ -36,7 +36,7 @@ NOTE: Add new changes BELOW THIS COMMENT.
- The ability to manage safesearch for each service by using the new - The ability to manage safesearch for each service by using the new
`safe_search` field ([#1163]). `safe_search` field ([#1163]).
### Changed ### Changed
- ARPA domain names containing a subnet within private networks now also - ARPA domain names containing a subnet within private networks now also
considered private, behaving closer to [RFC 6761][rfc6761] ([#5567]). considered private, behaving closer to [RFC 6761][rfc6761] ([#5567]).
@ -90,6 +90,17 @@ In this release, the schema version has changed from 17 to 20.
### Deprecated ### Deprecated
- The `POST /control/safesearch/enable` HTTP API is deprecated. Use the new
`PUT /control/safesearch/settings` API.
- The `POST /control/safesearch/disable` HTTP API is deprecated. Use the new
`PUT /control/safesearch/settings` API
- The `safesearch_enabled` field is deprecated in the following HTTP APIs:
- `GET /control/clients`
- `POST /control/clients/add`
- `POST /control/clients/update`
- `GET /control/clients/find?ip0=...&ip1=...&ip2=...`
Check `openapi/openapi.yaml` for more details.
- The `GET /control/stats_info` HTTP API; use the new `GET - The `GET /control/stats_info` HTTP API; use the new `GET
/control/stats/config` API instead. /control/stats/config` API instead.

View File

@ -167,6 +167,7 @@
"enabled_parental_toast": "Enabled Parental Control", "enabled_parental_toast": "Enabled Parental Control",
"disabled_safe_search_toast": "Disabled Safe Search", "disabled_safe_search_toast": "Disabled Safe Search",
"enabled_save_search_toast": "Enabled Safe Search", "enabled_save_search_toast": "Enabled Safe Search",
"updated_save_search_toast": "Safe Search settings updated",
"enabled_table_header": "Enabled", "enabled_table_header": "Enabled",
"name_table_header": "Name", "name_table_header": "Name",
"list_url_table_header": "List URL", "list_url_table_header": "List URL",

View File

@ -24,6 +24,12 @@ import { getFilteringStatus, setRules } from './filtering';
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE'); export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW'); export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
/**
*
* @param {*} settingKey = SETTINGS_NAMES
* @param {*} status: boolean | SafeSearchConfig
* @returns
*/
export const toggleSetting = (settingKey, status) => async (dispatch) => { export const toggleSetting = (settingKey, status) => async (dispatch) => {
let successMessage = ''; let successMessage = '';
try { try {
@ -49,14 +55,9 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => {
dispatch(toggleSettingStatus({ settingKey })); dispatch(toggleSettingStatus({ settingKey }));
break; break;
case SETTINGS_NAMES.safesearch: case SETTINGS_NAMES.safesearch:
if (status) { successMessage = 'updated_save_search_toast';
successMessage = 'disabled_safe_search_toast'; await apiClient.updateSafesearch(status);
await apiClient.disableSafesearch(); dispatch(toggleSettingStatus({ settingKey, value: status }));
} else {
successMessage = 'enabled_save_search_toast';
await apiClient.enableSafesearch();
}
dispatch(toggleSettingStatus({ settingKey }));
break; break;
default: default:
break; break;
@ -71,7 +72,9 @@ export const initSettingsRequest = createAction('SETTINGS_INIT_REQUEST');
export const initSettingsFailure = createAction('SETTINGS_INIT_FAILURE'); export const initSettingsFailure = createAction('SETTINGS_INIT_FAILURE');
export const initSettingsSuccess = createAction('SETTINGS_INIT_SUCCESS'); export const initSettingsSuccess = createAction('SETTINGS_INIT_SUCCESS');
export const initSettings = (settingsList) => async (dispatch) => { export const initSettings = (settingsList = {
safebrowsing: {}, parental: {},
}) => async (dispatch) => {
dispatch(initSettingsRequest()); dispatch(initSettingsRequest());
try { try {
const safebrowsingStatus = await apiClient.getSafebrowsingStatus(); const safebrowsingStatus = await apiClient.getSafebrowsingStatus();
@ -80,7 +83,6 @@ export const initSettings = (settingsList) => async (dispatch) => {
const { const {
safebrowsing, safebrowsing,
parental, parental,
safesearch,
} = settingsList; } = settingsList;
const newSettingsList = { const newSettingsList = {
safebrowsing: { safebrowsing: {
@ -92,8 +94,7 @@ export const initSettings = (settingsList) => async (dispatch) => {
enabled: parentalStatus.enabled, enabled: parentalStatus.enabled,
}, },
safesearch: { safesearch: {
...safesearch, ...safesearchStatus,
enabled: safesearchStatus.enabled,
}, },
}; };
dispatch(initSettingsSuccess({ settingsList: newSettingsList })); dispatch(initSettingsSuccess({ settingsList: newSettingsList }));

View File

@ -208,24 +208,40 @@ class Api {
// Safesearch // Safesearch
SAFESEARCH_STATUS = { path: 'safesearch/status', method: 'GET' }; SAFESEARCH_STATUS = { path: 'safesearch/status', method: 'GET' };
SAFESEARCH_ENABLE = { path: 'safesearch/enable', method: 'POST' }; SAFESEARCH_UPDATE = { path: 'safesearch/settings', method: 'PUT' };
SAFESEARCH_DISABLE = { path: 'safesearch/disable', method: 'POST' };
getSafesearchStatus() { getSafesearchStatus() {
const { path, method } = this.SAFESEARCH_STATUS; const { path, method } = this.SAFESEARCH_STATUS;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
enableSafesearch() { /**
const { path, method } = this.SAFESEARCH_ENABLE; * interface SafeSearchConfig {
return this.makeRequest(path, method); "enabled": boolean,
"bing": boolean,
"duckduckgo": boolean,
"google": boolean,
"pixabay": boolean,
"yandex": boolean,
"youtube": boolean
* }
* @param {*} data - SafeSearchConfig
* @returns 200 ok
*/
updateSafesearch(data) {
const { path, method } = this.SAFESEARCH_UPDATE;
return this.makeRequest(path, method, { data });
} }
disableSafesearch() { // enableSafesearch() {
const { path, method } = this.SAFESEARCH_DISABLE; // const { path, method } = this.SAFESEARCH_ENABLE;
return this.makeRequest(path, method); // return this.makeRequest(path, method);
} // }
// disableSafesearch() {
// const { path, method } = this.SAFESEARCH_DISABLE;
// return this.makeRequest(path, method);
// }
// Language // Language

View File

@ -7,6 +7,7 @@ import { useDispatch, useSelector } from 'react-redux';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import { getAllBlockedServices } from '../../../../actions/services'; import { getAllBlockedServices } from '../../../../actions/services';
import { initSettings } from '../../../../actions';
import { import {
splitByNewLine, splitByNewLine,
countClientsStatistics, countClientsStatistics,
@ -38,9 +39,13 @@ const ClientsTable = ({
const [t] = useTranslation(); const [t] = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const services = useSelector((store) => store?.services); const services = useSelector((store) => store?.services);
const globalSettings = useSelector((store) => store?.settings.settingsList) || {};
const { safesearch } = globalSettings;
useEffect(() => { useEffect(() => {
dispatch(getAllBlockedServices()); dispatch(getAllBlockedServices());
dispatch(initSettings());
}, []); }, []);
const handleFormAdd = (values) => { const handleFormAdd = (values) => {
@ -107,6 +112,7 @@ const ClientsTable = ({
tags: [], tags: [],
use_global_settings: true, use_global_settings: true,
use_global_blocked_services: true, use_global_blocked_services: true,
safe_search: { ...(safesearch || {}) },
}; };
}; };

View File

@ -11,7 +11,7 @@ import Select from 'react-select';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import Tabs from '../../ui/Tabs'; import Tabs from '../../ui/Tabs';
import Examples from '../Dns/Upstream/Examples'; import Examples from '../Dns/Upstream/Examples';
import { toggleAllServices, trimLinesAndRemoveEmpty } from '../../../helpers/helpers'; import { toggleAllServices, trimLinesAndRemoveEmpty, captitalizeWords } from '../../../helpers/helpers';
import { import {
renderInputField, renderInputField,
renderGroupField, renderGroupField,
@ -40,10 +40,6 @@ const settingsCheckboxes = [
name: 'parental_enabled', name: 'parental_enabled',
placeholder: 'use_adguard_parental', placeholder: 'use_adguard_parental',
}, },
{
name: 'safesearch_enabled',
placeholder: 'enforce_safe_search',
},
]; ];
const validate = (values) => { const validate = (values) => {
const errors = {}; const errors = {};
@ -139,8 +135,12 @@ let Form = (props) => {
processingUpdating, processingUpdating,
invalid, invalid,
tagsOptions, tagsOptions,
initialValues,
} = props; } = props;
const services = useSelector((store) => store?.services); const services = useSelector((store) => store?.services);
const { safe_search } = initialValues;
const safeSearchServices = { ...safe_search };
delete safeSearchServices.enabled;
const [activeTabLabel, setActiveTabLabel] = useState('settings'); const [activeTabLabel, setActiveTabLabel] = useState('settings');
@ -163,6 +163,28 @@ let Form = (props) => {
/> />
</div> </div>
))} ))}
<div className="form__group">
<Field
name="safe_search.enabled"
type="checkbox"
component={CheckboxField}
placeholder={t('enforce_safe_search')}
disabled={useGlobalSettings}
/>
</div>
<div className='form__group--inner'>
{Object.keys(safeSearchServices).map((searchKey) => (
<div key={searchKey}>
<Field
name={`safe_search.${searchKey}`}
type="checkbox"
component={CheckboxField}
placeholder={captitalizeWords(searchKey)}
disabled={useGlobalSettings}
/>
</div>
))}
</div>
</div>, </div>,
}, },
block_services: { block_services: {
@ -358,6 +380,7 @@ Form.propTypes = {
processingUpdating: PropTypes.bool.isRequired, processingUpdating: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired, invalid: PropTypes.bool.isRequired,
tagsOptions: PropTypes.array.isRequired, tagsOptions: PropTypes.array.isRequired,
initialValues: PropTypes.object,
}; };
const selector = formValueSelector(FORM_NAME.CLIENT); const selector = formValueSelector(FORM_NAME.CLIENT);

View File

@ -10,7 +10,7 @@ import Checkbox from '../ui/Checkbox';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import PageTitle from '../ui/PageTitle'; import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card'; import Card from '../ui/Card';
import { getObjectKeysSorted } from '../../helpers/helpers'; import { getObjectKeysSorted, captitalizeWords } from '../../helpers/helpers';
import './Settings.css'; import './Settings.css';
const ORDER_KEY = 'order'; const ORDER_KEY = 'order';
@ -28,12 +28,6 @@ const SETTINGS = {
subtitle: 'use_adguard_parental_hint', subtitle: 'use_adguard_parental_hint',
[ORDER_KEY]: 1, [ORDER_KEY]: 1,
}, },
safesearch: {
enabled: false,
title: 'enforce_safe_search',
subtitle: 'enforce_save_search_hint',
[ORDER_KEY]: 2,
},
}; };
class Settings extends Component { class Settings extends Component {
@ -44,7 +38,7 @@ class Settings extends Component {
this.props.getFilteringStatus(); this.props.getFilteringStatus();
} }
renderSettings = (settings) => getObjectKeysSorted(settings, ORDER_KEY) renderSettings = (settings) => getObjectKeysSorted(SETTINGS, ORDER_KEY)
.map((key) => { .map((key) => {
const setting = settings[key]; const setting = settings[key];
const { enabled } = setting; const { enabled } = setting;
@ -55,6 +49,35 @@ class Settings extends Component {
/>; />;
}); });
renderSafeSearch = () => {
const { settings: { settingsList: { safesearch } } } = this.props;
const { enabled } = safesearch || {};
const searches = { ...(safesearch || {}) };
delete searches.enabled;
return (
<>
<Checkbox
enabled={enabled}
title='enforce_safe_search'
subtitle='enforce_save_search_hint'
handleChange={({ target: { checked: enabled } }) => this.props.toggleSetting('safesearch', { ...safesearch, enabled })}
/>
<div className='form__group--inner'>
{Object.keys(searches).map((searchKey) => (
<Checkbox
key={searchKey}
enabled={searches[searchKey]}
title={captitalizeWords(searchKey)}
subtitle=''
disabled={!safesearch.enabled}
handleChange={({ target: { checked } }) => this.props.toggleSetting('safesearch', { ...safesearch, [searchKey]: checked })}
/>
))}
</div>
</>
);
};
render() { render() {
const { const {
settings, settings,
@ -92,6 +115,7 @@ class Settings extends Component {
setFiltersConfig={setFiltersConfig} setFiltersConfig={setFiltersConfig}
/> />
{this.renderSettings(settings.settingsList)} {this.renderSettings(settings.settingsList)}
{this.renderSafeSearch()}
</div> </div>
</Card> </Card>
</div> </div>

View File

@ -11,13 +11,14 @@ class Checkbox extends Component {
subtitle, subtitle,
enabled, enabled,
handleChange, handleChange,
disabled,
t, t,
} = this.props; } = this.props;
return ( return (
<div className="form__group form__group--checkbox"> <div className="form__group form__group--checkbox">
<label className="checkbox checkbox--settings"> <label className="checkbox checkbox--settings">
<span className="checkbox__marker"/> <span className="checkbox__marker"/>
<input type="checkbox" className="checkbox__input" onChange={handleChange} checked={enabled}/> <input type="checkbox" className="checkbox__input" onChange={handleChange} checked={enabled} disabled={disabled}/>
<span className="checkbox__label"> <span className="checkbox__label">
<span className="checkbox__label-text"> <span className="checkbox__label-text">
<span className="checkbox__label-title">{ t(title) }</span> <span className="checkbox__label-title">{ t(title) }</span>
@ -35,6 +36,7 @@ Checkbox.propTypes = {
subtitle: PropTypes.string.isRequired, subtitle: PropTypes.string.isRequired,
enabled: PropTypes.bool.isRequired, enabled: PropTypes.bool.isRequired,
handleChange: PropTypes.func.isRequired, handleChange: PropTypes.func.isRequired,
disabled: PropTypes.bool,
t: PropTypes.func, t: PropTypes.func,
}; };

View File

@ -22,11 +22,11 @@ const settings = handleActions(
}, },
[actions.toggleSettingStatus]: (state, { payload }) => { [actions.toggleSettingStatus]: (state, { payload }) => {
const { settingsList } = state; const { settingsList } = state;
const { settingKey } = payload; const { settingKey, value } = payload;
const setting = settingsList[settingKey]; const setting = settingsList[settingKey];
const newSetting = { const newSetting = value || {
...setting, ...setting,
enabled: !setting.enabled, enabled: !setting.enabled,
}; };

View File

@ -461,6 +461,7 @@ func (d *DNSFilter) RegisterFilteringHandlers() {
registerHTTP(http.MethodPost, "/control/safesearch/enable", d.handleSafeSearchEnable) registerHTTP(http.MethodPost, "/control/safesearch/enable", d.handleSafeSearchEnable)
registerHTTP(http.MethodPost, "/control/safesearch/disable", d.handleSafeSearchDisable) registerHTTP(http.MethodPost, "/control/safesearch/disable", d.handleSafeSearchDisable)
registerHTTP(http.MethodGet, "/control/safesearch/status", d.handleSafeSearchStatus) registerHTTP(http.MethodGet, "/control/safesearch/status", d.handleSafeSearchStatus)
registerHTTP(http.MethodPut, "/control/safesearch/settings", d.handleSafeSearchSettings)
registerHTTP(http.MethodGet, "/control/rewrite/list", d.handleRewriteList) registerHTTP(http.MethodGet, "/control/rewrite/list", d.handleRewriteList)
registerHTTP(http.MethodPost, "/control/rewrite/add", d.handleRewriteAdd) registerHTTP(http.MethodPost, "/control/rewrite/add", d.handleRewriteAdd)

View File

@ -17,7 +17,7 @@ type SafeSearch interface {
// SafeSearchConfig is a struct with safe search related settings. // SafeSearchConfig is a struct with safe search related settings.
type SafeSearchConfig struct { type SafeSearchConfig struct {
// CustomResolver is the resolver used by safe search. // CustomResolver is the resolver used by safe search.
CustomResolver Resolver `yaml:"-"` CustomResolver Resolver `yaml:"-" json:"-"`
// Enabled indicates if safe search is enabled entirely. // Enabled indicates if safe search is enabled entirely.
Enabled bool `yaml:"enabled" json:"enabled"` Enabled bool `yaml:"enabled" json:"enabled"`

View File

@ -1,29 +1,63 @@
package filtering package filtering
import ( import (
"encoding/json"
"net/http" "net/http"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
) )
// TODO(d.kolyshev): Replace handlers below with the new API. // handleSafeSearchEnable is the handler for POST /control/safesearch/enable
// HTTP API.
//
// Deprecated: Use handleSafeSearchSettings.
func (d *DNSFilter) handleSafeSearchEnable(w http.ResponseWriter, r *http.Request) { func (d *DNSFilter) handleSafeSearchEnable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.SafeSearchConf.Enabled, true) setProtectedBool(&d.confLock, &d.Config.SafeSearchConf.Enabled, true)
d.Config.ConfigModified() d.Config.ConfigModified()
} }
// handleSafeSearchDisable is the handler for POST /control/safesearch/disable
// HTTP API.
//
// Deprecated: Use handleSafeSearchSettings.
func (d *DNSFilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Request) { func (d *DNSFilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.SafeSearchConf.Enabled, false) setProtectedBool(&d.confLock, &d.Config.SafeSearchConf.Enabled, false)
d.Config.ConfigModified() d.Config.ConfigModified()
} }
// handleSafeSearchStatus is the handler for GET /control/safesearch/status
// HTTP API.
func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) { func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
resp := &struct { var resp SafeSearchConfig
Enabled bool `json:"enabled"` func() {
}{ d.confLock.RLock()
Enabled: protectedBool(&d.confLock, &d.Config.SafeSearchConf.Enabled), defer d.confLock.RUnlock()
}
resp = d.Config.SafeSearchConf
}()
_ = aghhttp.WriteJSONResponse(w, r, resp) _ = aghhttp.WriteJSONResponse(w, r, resp)
} }
// handleSafeSearchSettings is the handler for PUT /control/safesearch/settings
// HTTP API.
func (d *DNSFilter) handleSafeSearchSettings(w http.ResponseWriter, r *http.Request) {
req := &SafeSearchConfig{}
err := json.NewDecoder(r.Body).Decode(req)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "reading req: %s", err)
return
}
func() {
d.confLock.Lock()
defer d.confLock.Unlock()
d.Config.SafeSearchConf = *req
}()
d.Config.ConfigModified()
aghhttp.OK(w)
}

View File

@ -27,7 +27,8 @@ type clientJSON struct {
// the allowlist. // the allowlist.
DisallowedRule *string `json:"disallowed_rule,omitempty"` DisallowedRule *string `json:"disallowed_rule,omitempty"`
WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info,omitempty"` WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info,omitempty"`
SafeSearchConf *filtering.SafeSearchConfig `json:"safe_search"`
Name string `json:"name"` Name string `json:"name"`
@ -59,7 +60,7 @@ type clientListJSON struct {
Tags []string `json:"supported_tags"` Tags []string `json:"supported_tags"`
} }
// respond with information about configured clients // handleGetClients is the handler for GET /control/clients HTTP API.
func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http.Request) { func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http.Request) {
data := clientListJSON{} data := clientListJSON{}
@ -88,32 +89,36 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
_ = aghhttp.WriteJSONResponse(w, r, data) _ = aghhttp.WriteJSONResponse(w, r, data)
} }
// Convert JSON object to Client object // jsonToClient converts JSON object to Client object.
func jsonToClient(cj clientJSON) (c *Client) { func jsonToClient(cj clientJSON) (c *Client) {
// TODO(d.kolyshev): Remove after cleaning the deprecated var safeSearchConf filtering.SafeSearchConfig
// [clientJSON.SafeSearchEnabled] field. if cj.SafeSearchConf != nil {
safeSearchConf := filtering.SafeSearchConfig{Enabled: cj.SafeSearchEnabled} safeSearchConf = *cj.SafeSearchConf
} else {
// TODO(d.kolyshev): Remove after cleaning the deprecated
// [clientJSON.SafeSearchEnabled] field.
safeSearchConf = filtering.SafeSearchConfig{Enabled: cj.SafeSearchEnabled}
// Set default service flags for enabled safesearch. // Set default service flags for enabled safesearch.
if safeSearchConf.Enabled { if safeSearchConf.Enabled {
safeSearchConf.Bing = true safeSearchConf.Bing = true
safeSearchConf.DuckDuckGo = true safeSearchConf.DuckDuckGo = true
safeSearchConf.Google = true safeSearchConf.Google = true
safeSearchConf.Pixabay = true safeSearchConf.Pixabay = true
safeSearchConf.Yandex = true safeSearchConf.Yandex = true
safeSearchConf.YouTube = true safeSearchConf.YouTube = true
}
} }
return &Client{ return &Client{
Name: cj.Name, Name: cj.Name,
IDs: cj.IDs, IDs: cj.IDs,
Tags: cj.Tags, Tags: cj.Tags,
UseOwnSettings: !cj.UseGlobalSettings, UseOwnSettings: !cj.UseGlobalSettings,
FilteringEnabled: cj.FilteringEnabled, FilteringEnabled: cj.FilteringEnabled,
ParentalEnabled: cj.ParentalEnabled, ParentalEnabled: cj.ParentalEnabled,
safeSearchConf: safeSearchConf, SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
SafeBrowsingEnabled: cj.SafeBrowsingEnabled, safeSearchConf: safeSearchConf,
UseOwnBlockedServices: !cj.UseGlobalBlockedServices, UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
BlockedServices: cj.BlockedServices, BlockedServices: cj.BlockedServices,
@ -121,7 +126,7 @@ func jsonToClient(cj clientJSON) (c *Client) {
} }
} }
// Convert Client object to JSON // clientToJSON converts Client object to JSON.
func clientToJSON(c *Client) (cj *clientJSON) { func clientToJSON(c *Client) (cj *clientJSON) {
// TODO(d.kolyshev): Remove after cleaning the deprecated // TODO(d.kolyshev): Remove after cleaning the deprecated
// [clientJSON.SafeSearchEnabled] field. // [clientJSON.SafeSearchEnabled] field.
@ -136,6 +141,7 @@ func clientToJSON(c *Client) (cj *clientJSON) {
FilteringEnabled: c.FilteringEnabled, FilteringEnabled: c.FilteringEnabled,
ParentalEnabled: c.ParentalEnabled, ParentalEnabled: c.ParentalEnabled,
SafeSearchEnabled: safeSearchConf.Enabled, SafeSearchEnabled: safeSearchConf.Enabled,
SafeSearchConf: safeSearchConf,
SafeBrowsingEnabled: c.SafeBrowsingEnabled, SafeBrowsingEnabled: c.SafeBrowsingEnabled,
UseGlobalBlockedServices: !c.UseOwnBlockedServices, UseGlobalBlockedServices: !c.UseOwnBlockedServices,
@ -145,7 +151,7 @@ func clientToJSON(c *Client) (cj *clientJSON) {
} }
} }
// Add a new client // handleAddClient is the handler for POST /control/clients/add HTTP API.
func (clients *clientsContainer) handleAddClient(w http.ResponseWriter, r *http.Request) { func (clients *clientsContainer) handleAddClient(w http.ResponseWriter, r *http.Request) {
cj := clientJSON{} cj := clientJSON{}
err := json.NewDecoder(r.Body).Decode(&cj) err := json.NewDecoder(r.Body).Decode(&cj)
@ -172,7 +178,7 @@ func (clients *clientsContainer) handleAddClient(w http.ResponseWriter, r *http.
onConfigModified() onConfigModified()
} }
// Remove client // handleDelClient is the handler for POST /control/clients/delete HTTP API.
func (clients *clientsContainer) handleDelClient(w http.ResponseWriter, r *http.Request) { func (clients *clientsContainer) handleDelClient(w http.ResponseWriter, r *http.Request) {
cj := clientJSON{} cj := clientJSON{}
err := json.NewDecoder(r.Body).Decode(&cj) err := json.NewDecoder(r.Body).Decode(&cj)
@ -202,7 +208,7 @@ type updateJSON struct {
Data clientJSON `json:"data"` Data clientJSON `json:"data"`
} }
// Update client's properties // handleUpdateClient is the handler for POST /control/clients/update HTTP API.
func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *http.Request) { func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *http.Request) {
dj := updateJSON{} dj := updateJSON{}
err := json.NewDecoder(r.Body).Decode(&dj) err := json.NewDecoder(r.Body).Decode(&dj)
@ -229,7 +235,7 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht
onConfigModified() onConfigModified()
} }
// Get the list of clients by IP address list // handleFindClient is the handler for GET /control/clients/find HTTP API.
func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http.Request) { func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query() q := r.URL.Query()
data := []map[string]*clientJSON{} data := []map[string]*clientJSON{}

View File

@ -83,6 +83,78 @@ accept and return a JSON object with the following format:
## v0.107.27: API changes
### Deprecated HTTP APIs
The following HTTP APIs are deprecated:
* `POST /control/safesearch/enable` is deprecated. Use the new
`PUT /control/safesearch/settings`.
* `POST /control/safesearch/disable` is deprecated. Use the new
`PUT /control/safesearch/settings`.
### New HTTP API `PUT /control/safesearch/settings`
* The new `PUT /control/safesearch/settings` HTTP API allows safesearch
settings updates. It accepts a JSON object with the following format:
```json
{
"enabled": true,
"bing": false,
"duckduckgo": true,
"google": false,
"pixabay": false,
"yandex": true,
"youtube": false
}
```
### `GET /control/safesearch/status`
* The `control/safesearch/status` HTTP API has been changed. It now returns a
JSON object with the following format:
```json
{
"enabled": true,
"bing": false,
"duckduckgo": true,
"google": false,
"pixabay": false,
"yandex": true,
"youtube": false
}
```
### `/control/clients` HTTP APIs
The following HTTP APIs have been changed:
* `GET /control/clients`;
* `GET /control/clients/find?ip0=...&ip1=...&ip2=...`;
* `POST /control/clients/add`;
* `POST /control/clients/update`;
The `safesearch_enabled` field is deprecated. The new field `safe_search` has
been added to JSON objects. It has the following format:
```json
{
"enabled": true,
"bing": false,
"duckduckgo": true,
"google": false,
"pixabay": false,
"yandex": true,
"youtube": false
}
```
## v0.107.23: API changes ## v0.107.23: API changes
### Experimental “beta” APIs removed ### Experimental “beta” APIs removed

View File

@ -795,6 +795,7 @@
'sensitivity': 13 'sensitivity': 13
'/safesearch/enable': '/safesearch/enable':
'post': 'post':
'deprecated': true
'tags': 'tags':
- 'safesearch' - 'safesearch'
'operationId': 'safesearchEnable' 'operationId': 'safesearchEnable'
@ -804,6 +805,7 @@
'description': 'OK.' 'description': 'OK.'
'/safesearch/disable': '/safesearch/disable':
'post': 'post':
'deprecated': true
'tags': 'tags':
- 'safesearch' - 'safesearch'
'operationId': 'safesearchDisable' 'operationId': 'safesearchDisable'
@ -811,6 +813,20 @@
'responses': 'responses':
'200': '200':
'description': 'OK.' 'description': 'OK.'
'/safesearch/settings':
'put':
'tags':
- 'safesearch'
'operationId': 'safesearchSettings'
'summary': 'Update safesearch settings'
'requestBody':
'content':
'application/json':
'schema':
'$ref': '#/components/schemas/SafeSearchConfig'
'responses':
'200':
'description': 'OK.'
'/safesearch/status': '/safesearch/status':
'get': 'get':
'tags': 'tags':
@ -823,14 +839,7 @@
'content': 'content':
'application/json': 'application/json':
'schema': 'schema':
'type': 'object' '$ref': '#/components/schemas/SafeSearchConfig'
'properties':
'enabled':
'type': 'boolean'
'examples':
'response':
'value':
'enabled': false
'/clients': '/clients':
'get': 'get':
'tags': 'tags':
@ -2394,6 +2403,24 @@
- 'name' - 'name'
- 'language' - 'language'
- 'theme' - 'theme'
'SafeSearchConfig':
'type': 'object'
'description': 'Safe search settings.'
'properties':
'enabled':
'type': 'boolean'
'bing':
'type': 'boolean'
'duckduckgo':
'type': 'boolean'
'google':
'type': 'boolean'
'pixabay':
'type': 'boolean'
'yandex':
'type': 'boolean'
'youtube':
'type': 'boolean'
'Client': 'Client':
'type': 'object' 'type': 'object'
'description': 'Client information.' 'description': 'Client information.'
@ -2416,7 +2443,10 @@
'safebrowsing_enabled': 'safebrowsing_enabled':
'type': 'boolean' 'type': 'boolean'
'safesearch_enabled': 'safesearch_enabled':
'deprecated': true
'type': 'boolean' 'type': 'boolean'
'safe_search':
'$ref': '#/components/schemas/SafeSearchConfig'
'use_global_blocked_services': 'use_global_blocked_services':
'type': 'boolean' 'type': 'boolean'
'blocked_services': 'blocked_services':
@ -2477,6 +2507,7 @@
'parental_enabled': true 'parental_enabled': true
'safebrowsing_enabled': true 'safebrowsing_enabled': true
'safesearch_enabled': true 'safesearch_enabled': true
'safe_search': {}
'use_global_blocked_services': true 'use_global_blocked_services': true
'blocked_services': null 'blocked_services': null
'upstreams': null 'upstreams': null
@ -2491,6 +2522,7 @@
'parental_enabled': true 'parental_enabled': true
'safebrowsing_enabled': true 'safebrowsing_enabled': true
'safesearch_enabled': true 'safesearch_enabled': true
'safe_search': {}
'use_global_blocked_services': true 'use_global_blocked_services': true
'blocked_services': null 'blocked_services': null
'upstreams': null 'upstreams': null
@ -2551,7 +2583,10 @@
'safebrowsing_enabled': 'safebrowsing_enabled':
'type': 'boolean' 'type': 'boolean'
'safesearch_enabled': 'safesearch_enabled':
'deprecated': true
'type': 'boolean' 'type': 'boolean'
'safe_search':
'$ref': '#/components/schemas/SafeSearchConfig'
'use_global_blocked_services': 'use_global_blocked_services':
'type': 'boolean' 'type': 'boolean'
'blocked_services': 'blocked_services':