diff --git a/client/.eslintrc.json b/client/.eslintrc.json
index 0098e76a..e7dd1e89 100644
--- a/client/.eslintrc.json
+++ b/client/.eslintrc.json
@@ -81,6 +81,19 @@
}
],
"import/prefer-default-export": "off",
- "no-alert": "off"
+ "no-alert": "off",
+ "arrow-body-style": "off",
+ "max-len": [
+ "error",
+ 120,
+ 2,
+ {
+ "ignoreUrls": true,
+ "ignoreComments": false,
+ "ignoreRegExpLiterals": true,
+ "ignoreStrings": true,
+ "ignoreTemplateLiterals": true
+ }
+ ]
}
}
diff --git a/client/dev.eslintrc b/client/dev.eslintrc
index 27341caf..9d7e5493 100644
--- a/client/dev.eslintrc
+++ b/client/dev.eslintrc
@@ -1,6 +1,6 @@
{
"extends": ".eslintrc",
"rules": {
- "no-debugger":"warn",
+ "no-debugger":"warn"
}
}
diff --git a/client/package-lock.json b/client/package-lock.json
index ba1d3772..cc6c55ea 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -15094,6 +15094,11 @@
"setimmediate": "^1.0.4"
}
},
+ "timezones-list": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/timezones-list/-/timezones-list-3.0.2.tgz",
+ "integrity": "sha512-I698hm6Jp/xxkwyTSOr39pZkYKETL8LDJeSIhjxXBfPUAHM5oZNuQ4o9UK3PSkDBOkjATecSOBb3pR1IkIBUsg=="
+ },
"tiny-invariant": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
diff --git a/client/package.json b/client/package.json
index 22b83010..134b98b5 100644
--- a/client/package.json
+++ b/client/package.json
@@ -43,6 +43,7 @@
"redux-form": "^8.3.5",
"redux-thunk": "^2.3.0",
"string-length": "^5.0.1",
+ "timezones-list": "^3.0.2",
"url-polyfill": "^1.1.9"
},
"devDependencies": {
diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index 85313bb3..b8e72b3f 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -680,5 +680,37 @@
"protection_section_label": "Protection",
"log_and_stats_section_label": "Query log and statistics",
"ignore_query_log": "Ignore this client in query log",
- "ignore_statistics": "Ignore this client in statistics"
+ "ignore_statistics": "Ignore this client in statistics",
+ "schedule_services": "Pause service blocking",
+ "schedule_services_desc": "Configure the pause schedule of the service-blocking filter",
+ "schedule_services_desc_client": "Configure the pause schedule of the service-blocking filter for this client",
+ "schedule_desc": "Set inactivity periods for blocked services",
+ "schedule_invalid_select": "Start time must be before end time",
+ "schedule_select_days": "Select days",
+ "schedule_timezone": "Select a time zone",
+ "schedule_current_timezone": "Current time zone: {{value}}",
+ "schedule_time_all_day": "All day",
+ "schedule_modal_description": "This schedule will replace any existing schedules for the same day of the week. Each day of the week can have only one inactivity period.",
+ "schedule_modal_time_off": "No service blocking:",
+ "schedule_new": "New schedule",
+ "schedule_edit": "Edit schedule",
+ "schedule_save": "Save schedule",
+ "schedule_add": "Add schedule",
+ "schedule_remove": "Remove schedule",
+ "schedule_from": "From",
+ "schedule_to": "To",
+ "sunday": "Sunday",
+ "monday": "Monday",
+ "tuesday": "Tuesday",
+ "wednesday": "Wednesday",
+ "thursday": "Thursday",
+ "friday": "Friday",
+ "saturday": "Saturday",
+ "sunday_short": "Sun",
+ "monday_short": "Mon",
+ "tuesday_short": "Tue",
+ "wednesday_short": "Wed",
+ "thursday_short": "Thu",
+ "friday_short": "Fri",
+ "saturday_short": "Sat"
}
diff --git a/client/src/actions/services.js b/client/src/actions/services.js
index 2f5f20db..28ae8837 100644
--- a/client/src/actions/services.js
+++ b/client/src/actions/services.js
@@ -32,19 +32,19 @@ export const getAllBlockedServices = () => async (dispatch) => {
}
};
-export const setBlockedServicesRequest = createAction('SET_BLOCKED_SERVICES_REQUEST');
-export const setBlockedServicesFailure = createAction('SET_BLOCKED_SERVICES_FAILURE');
-export const setBlockedServicesSuccess = createAction('SET_BLOCKED_SERVICES_SUCCESS');
+export const updateBlockedServicesRequest = createAction('UPDATE_BLOCKED_SERVICES_REQUEST');
+export const updateBlockedServicesFailure = createAction('UPDATE_BLOCKED_SERVICES_FAILURE');
+export const updateBlockedServicesSuccess = createAction('UPDATE_BLOCKED_SERVICES_SUCCESS');
-export const setBlockedServices = (values) => async (dispatch) => {
- dispatch(setBlockedServicesRequest());
+export const updateBlockedServices = (values) => async (dispatch) => {
+ dispatch(updateBlockedServicesRequest());
try {
- await apiClient.setBlockedServices(values);
- dispatch(setBlockedServicesSuccess());
+ await apiClient.updateBlockedServices(values);
+ dispatch(updateBlockedServicesSuccess());
dispatch(getBlockedServices());
dispatch(addSuccessToast('blocked_services_saved'));
} catch (error) {
dispatch(addErrorToast({ error }));
- dispatch(setBlockedServicesFailure());
+ dispatch(updateBlockedServicesFailure());
}
};
diff --git a/client/src/api/Api.js b/client/src/api/Api.js
index a01c9d04..077c794e 100644
--- a/client/src/api/Api.js
+++ b/client/src/api/Api.js
@@ -489,9 +489,9 @@ class Api {
}
// Blocked services
- BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' };
+ BLOCKED_SERVICES_GET = { path: 'blocked_services/get', method: 'GET' };
- BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' };
+ BLOCKED_SERVICES_UPDATE = { path: 'blocked_services/update', method: 'PUT' };
BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' };
@@ -501,12 +501,12 @@ class Api {
}
getBlockedServices() {
- const { path, method } = this.BLOCKED_SERVICES_LIST;
+ const { path, method } = this.BLOCKED_SERVICES_GET;
return this.makeRequest(path, method);
}
- setBlockedServices(config) {
- const { path, method } = this.BLOCKED_SERVICES_SET;
+ updateBlockedServices(config) {
+ const { path, method } = this.BLOCKED_SERVICES_UPDATE;
const parameters = {
data: config,
};
diff --git a/client/src/components/Filters/Services/ScheduleForm/Modal.js b/client/src/components/Filters/Services/ScheduleForm/Modal.js
new file mode 100644
index 00000000..429db9be
--- /dev/null
+++ b/client/src/components/Filters/Services/ScheduleForm/Modal.js
@@ -0,0 +1,220 @@
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+import ReactModal from 'react-modal';
+
+import { Timezone } from './Timezone';
+import { TimeSelect } from './TimeSelect';
+import { TimePeriod } from './TimePeriod';
+import { getFullDayName, getShortDayName } from './helpers';
+import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
+
+export const DAYS_OF_WEEK = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
+
+const INITIAL_START_TIME_MS = 0;
+const INITIAL_END_TIME_MS = 86340000;
+
+export const Modal = ({
+ isOpen,
+ currentDay,
+ schedule,
+ onClose,
+ onSubmit,
+}) => {
+ const [t] = useTranslation();
+
+ const intialTimezone = schedule.time_zone === LOCAL_TIMEZONE_VALUE
+ ? Intl.DateTimeFormat().resolvedOptions().timeZone
+ : schedule.time_zone;
+
+ const [timezone, setTimezone] = useState(intialTimezone);
+ const [days, setDays] = useState(new Set());
+
+ const [startTime, setStartTime] = useState(INITIAL_START_TIME_MS);
+ const [endTime, setEndTime] = useState(INITIAL_END_TIME_MS);
+
+ const [wrongPeriod, setWrongPeriod] = useState(true);
+
+ useEffect(() => {
+ if (currentDay) {
+ const newDays = new Set([currentDay]);
+ setDays(newDays);
+
+ setStartTime(schedule[currentDay].start);
+ setEndTime(schedule[currentDay].end);
+ }
+ }, [currentDay]);
+
+ useEffect(() => {
+ if (startTime >= endTime) {
+ setWrongPeriod(true);
+ } else {
+ setWrongPeriod(false);
+ }
+ }, [startTime, endTime]);
+
+ const addDays = (day) => {
+ const newDays = new Set(days);
+
+ if (newDays.has(day)) {
+ newDays.delete(day);
+ } else {
+ newDays.add(day);
+ }
+
+ setDays(newDays);
+ };
+
+ const activeDay = (day) => {
+ return days.has(day);
+ };
+
+ const onFormSubmit = (e) => {
+ e.preventDefault();
+
+ if (currentDay) {
+ const newSchedule = schedule;
+
+ Array.from(days).forEach((day) => {
+ newSchedule[day] = {
+ start: startTime,
+ end: endTime,
+ };
+ });
+
+ onSubmit(newSchedule);
+ } else {
+ const newSchedule = {
+ time_zone: timezone,
+ };
+
+ Array.from(days).forEach((day) => {
+ newSchedule[day] = {
+ start: startTime,
+ end: endTime,
+ };
+ });
+
+ onSubmit(newSchedule);
+ }
+ };
+
+ return (
+
+
+
+
+ {currentDay ? t('schedule_edit') : t('schedule_new')}
+
+
+
+
+
+
+ );
+};
+
+Modal.propTypes = {
+ schedule: PropTypes.object.isRequired,
+ currentDay: PropTypes.string,
+ isOpen: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+};
diff --git a/client/src/components/Filters/Services/ScheduleForm/TimePeriod.js b/client/src/components/Filters/Services/ScheduleForm/TimePeriod.js
new file mode 100644
index 00000000..69ef2293
--- /dev/null
+++ b/client/src/components/Filters/Services/ScheduleForm/TimePeriod.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { getTimeFromMs } from './helpers';
+
+export const TimePeriod = ({
+ startTimeMs,
+ endTimeMs,
+}) => {
+ const startTime = getTimeFromMs(startTimeMs);
+ const endTime = getTimeFromMs(endTimeMs);
+
+ return (
+
+
+ –
+
+
+ );
+};
+
+TimePeriod.propTypes = {
+ startTimeMs: PropTypes.number.isRequired,
+ endTimeMs: PropTypes.number.isRequired,
+};
diff --git a/client/src/components/Filters/Services/ScheduleForm/TimeSelect.js b/client/src/components/Filters/Services/ScheduleForm/TimeSelect.js
new file mode 100644
index 00000000..35998437
--- /dev/null
+++ b/client/src/components/Filters/Services/ScheduleForm/TimeSelect.js
@@ -0,0 +1,60 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+import { getTimeFromMs, convertTimeToMs } from './helpers';
+
+export const TimeSelect = ({
+ value,
+ onChange,
+}) => {
+ const { hours: initialHours, minutes: initialMinutes } = getTimeFromMs(value);
+
+ const [hours, setHours] = useState(initialHours);
+ const [minutes, setMinutes] = useState(initialMinutes);
+
+ const hourOptions = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
+ const minuteOptions = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'));
+
+ const onHourChange = (event) => {
+ setHours(event.target.value);
+ onChange(convertTimeToMs(event.target.value, minutes));
+ };
+
+ const onMinuteChange = (event) => {
+ setMinutes(event.target.value);
+ onChange(convertTimeToMs(hours, event.target.value));
+ };
+
+ return (
+
+
+ :
+
+
+ );
+};
+
+TimeSelect.propTypes = {
+ value: PropTypes.number.isRequired,
+ onChange: PropTypes.func.isRequired,
+};
diff --git a/client/src/components/Filters/Services/ScheduleForm/Timezone.js b/client/src/components/Filters/Services/ScheduleForm/Timezone.js
new file mode 100644
index 00000000..71d017d6
--- /dev/null
+++ b/client/src/components/Filters/Services/ScheduleForm/Timezone.js
@@ -0,0 +1,46 @@
+import React from 'react';
+import timezones from 'timezones-list';
+import { useTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
+
+import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
+
+export const Timezone = ({
+ timezone,
+ setTimezone,
+}) => {
+ const [t] = useTranslation();
+
+ const onTimeZoneChange = (event) => {
+ setTimezone(event.target.value);
+ };
+
+ return (
+
+
+
+
+
+ );
+};
+
+Timezone.propTypes = {
+ timezone: PropTypes.string.isRequired,
+ setTimezone: PropTypes.func.isRequired,
+};
diff --git a/client/src/components/Filters/Services/ScheduleForm/helpers.js b/client/src/components/Filters/Services/ScheduleForm/helpers.js
new file mode 100644
index 00000000..c3986ed4
--- /dev/null
+++ b/client/src/components/Filters/Services/ScheduleForm/helpers.js
@@ -0,0 +1,46 @@
+export const getFullDayName = (t, abbreviation) => {
+ const dayMap = {
+ sun: t('sunday'),
+ mon: t('monday'),
+ tue: t('tuesday'),
+ wed: t('wednesday'),
+ thu: t('thursday'),
+ fri: t('friday'),
+ sat: t('saturday'),
+ };
+
+ return dayMap[abbreviation] || '';
+};
+
+export const getShortDayName = (t, abbreviation) => {
+ const dayMap = {
+ sun: t('sunday_short'),
+ mon: t('monday_short'),
+ tue: t('tuesday_short'),
+ wed: t('wednesday_short'),
+ thu: t('thursday_short'),
+ fri: t('friday_short'),
+ sat: t('saturday_short'),
+ };
+
+ return dayMap[abbreviation] || '';
+};
+
+export const getTimeFromMs = (value) => {
+ const selectedTime = new Date(value);
+ const hours = selectedTime.getUTCHours();
+ const minutes = selectedTime.getUTCMinutes();
+
+ return {
+ hours: hours.toString().padStart(2, '0'),
+ minutes: minutes.toString().padStart(2, '0'),
+ };
+};
+
+export const convertTimeToMs = (hours, minutes) => {
+ const selectedTime = new Date(0);
+ selectedTime.setUTCHours(parseInt(hours, 10));
+ selectedTime.setUTCMinutes(parseInt(minutes, 10));
+
+ return selectedTime.getTime();
+};
diff --git a/client/src/components/Filters/Services/ScheduleForm/index.js b/client/src/components/Filters/Services/ScheduleForm/index.js
new file mode 100644
index 00000000..f7bf605b
--- /dev/null
+++ b/client/src/components/Filters/Services/ScheduleForm/index.js
@@ -0,0 +1,140 @@
+import React, { useState, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
+import cn from 'classnames';
+
+import { Modal } from './Modal';
+import { getFullDayName, getShortDayName } from './helpers';
+import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
+import { TimePeriod } from './TimePeriod';
+import './styles.css';
+
+export const ScheduleForm = ({
+ schedule,
+ onScheduleSubmit,
+ clientForm,
+}) => {
+ const [t] = useTranslation();
+ const [modalOpen, setModalOpen] = useState(false);
+ const [currentDay, setCurrentDay] = useState();
+
+ const onModalOpen = () => setModalOpen(true);
+ const onModalClose = () => setModalOpen(false);
+
+ const filteredScheduleKeys = useMemo(() => (
+ schedule ? Object.keys(schedule).filter((v) => v !== 'time_zone') : []
+ ), [schedule]);
+
+ const scheduleMap = new Map();
+ filteredScheduleKeys.forEach((day) => scheduleMap.set(day, schedule[day]));
+
+ const onSubmit = (values) => {
+ onScheduleSubmit(values);
+ onModalClose();
+ };
+
+ const onDelete = (day) => {
+ scheduleMap.delete(day);
+
+ const scheduleWeek = Object.fromEntries(Array.from(scheduleMap.entries()));
+
+ onScheduleSubmit({
+ time_zone: schedule.time_zone,
+ ...scheduleWeek,
+ });
+ };
+
+ const onEdit = (day) => {
+ setCurrentDay(day);
+ onModalOpen();
+ };
+
+ const onAdd = () => {
+ setCurrentDay(undefined);
+ onModalOpen();
+ };
+
+ return (
+
+
+ {t('schedule_current_timezone', { value: schedule?.time_zone || LOCAL_TIMEZONE_VALUE })}
+
+
+
+ {filteredScheduleKeys.map((day) => {
+ const data = schedule[day];
+
+ if (!data) {
+ return undefined;
+ }
+
+ return (
+
+
+ {getFullDayName(t, day)}
+
+
+ {getShortDayName(t, day)}
+
+
+
+
+
+
+
+
+ );
+ })}
+
+
+
+
+ {modalOpen && (
+
+ )}
+
+ );
+};
+
+ScheduleForm.propTypes = {
+ schedule: PropTypes.object,
+ onScheduleSubmit: PropTypes.func.isRequired,
+ clientForm: PropTypes.bool,
+};
diff --git a/client/src/components/Filters/Services/ScheduleForm/styles.css b/client/src/components/Filters/Services/ScheduleForm/styles.css
new file mode 100644
index 00000000..f839f92a
--- /dev/null
+++ b/client/src/components/Filters/Services/ScheduleForm/styles.css
@@ -0,0 +1,134 @@
+.schedule__row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+}
+
+.schedule__row:last-child {
+ margin-bottom: 0;
+}
+
+.schedule__rows {
+ margin-bottom: 24px;
+}
+
+.schedule__day {
+ display: none;
+ min-width: 110px;
+}
+
+.schedule__day--mobile {
+ display: block;
+ min-width: 50px;
+}
+
+@media screen and (min-width: 767px) {
+ .schedule__row {
+ justify-content: flex-start;
+ }
+
+ .schedule__day {
+ display: block;
+ }
+
+ .schedule__day--mobile {
+ display: none;
+ }
+
+ .schedule__actions {
+ margin-left: 32px;
+ white-space: nowrap;
+ }
+}
+
+.schedule__time {
+ min-width: 110px;
+}
+
+.schedule__button {
+ border: 0;
+}
+
+.schedule__days {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 24px;
+}
+
+.schedule__button-day {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-width: 60px;
+ height: 32px;
+ font-size: 14px;
+ line-height: 14px;
+ color: #495057;
+ background-color: transparent;
+ border: 1px solid var(--card-border-color);
+ border-radius: 4px;
+ cursor: pointer;
+ outline: 0;
+}
+
+.schedule__button-day[data-active="true"] {
+ color: var(--btn-success-bgcolor);
+ border-color: var(--btn-success-bgcolor);
+}
+
+.schedule__time-wrap {
+ margin-bottom: 24px;
+}
+
+.schedule__time-row {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.schedule__time-select {
+ display: flex;
+ align-items: center;
+}
+
+.schedule__error {
+ margin-top: 4px;
+ font-size: 13px;
+ color: #cd201f;
+}
+
+.schedule__timezone {
+ margin-bottom: 24px;
+}
+
+.schedule__current-timezone {
+ margin-bottom: 16px;
+}
+
+.schedule__info {
+ margin-bottom: 24px;
+}
+
+.schedule__notice {
+ font-size: 13px;
+}
+
+.schedule__info-title {
+ margin-bottom: 8px;
+}
+
+.schedule__info-row {
+ display: flex;
+ align-items: center;
+ margin-bottom: 4px;
+}
+
+.schedule__info-icon {
+ width: 24px;
+ height: 24px;
+ margin-right: 8px;
+ color: #495057;
+ flex-shrink: 0;
+}
diff --git a/client/src/components/Filters/Services/index.js b/client/src/components/Filters/Services/index.js
index 09cdd7c8..3a0a12fd 100644
--- a/client/src/components/Filters/Services/index.js
+++ b/client/src/components/Filters/Services/index.js
@@ -4,9 +4,11 @@ import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import Form from './Form';
import Card from '../../ui/Card';
-import { getBlockedServices, getAllBlockedServices, setBlockedServices } from '../../../actions/services';
+import { getBlockedServices, getAllBlockedServices, updateBlockedServices } from '../../../actions/services';
import PageTitle from '../../ui/PageTitle';
+import { ScheduleForm } from './ScheduleForm';
+
const getInitialDataForServices = (initial) => (initial ? initial.reduce(
(acc, service) => {
acc.blocked_services[service] = true;
@@ -33,10 +35,24 @@ const Services = () => {
.keys(values.blocked_services)
.filter((service) => values.blocked_services[service]);
- dispatch(setBlockedServices(blocked_services));
+ dispatch(updateBlockedServices({
+ ids: blocked_services,
+ schedule: services.list.schedule,
+ }));
};
- const initialValues = getInitialDataForServices(services.list);
+ const handleScheduleSubmit = (values) => {
+ dispatch(updateBlockedServices({
+ ids: services.list.ids,
+ schedule: values,
+ }));
+ };
+
+ const initialValues = getInitialDataForServices(services.list.ids);
+
+ if (!initialValues) {
+ return null;
+ }
return (
<>
@@ -57,6 +73,17 @@ const Services = () => {
/>
+
+
+
+
>
);
};
diff --git a/client/src/components/Settings/Clients/ClientsTable/ClientsTable.js b/client/src/components/Settings/Clients/ClientsTable/ClientsTable.js
index dab8994c..96f835c3 100644
--- a/client/src/components/Settings/Clients/ClientsTable/ClientsTable.js
+++ b/client/src/components/Settings/Clients/ClientsTable/ClientsTable.js
@@ -6,7 +6,7 @@ import { Trans, useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import ReactTable from 'react-table';
-import { getAllBlockedServices } from '../../../../actions/services';
+import { getAllBlockedServices, getBlockedServices } from '../../../../actions/services';
import { initSettings } from '../../../../actions';
import {
splitByNewLine,
@@ -14,7 +14,7 @@ import {
sortIp,
getService,
} from '../../../../helpers/helpers';
-import { MODAL_TYPE } from '../../../../helpers/constants';
+import { MODAL_TYPE, LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
import Card from '../../../ui/Card';
import CellWrap from '../../../ui/CellWrap';
import LogsSearchLink from '../../../ui/LogsSearchLink';
@@ -45,6 +45,7 @@ const ClientsTable = ({
useEffect(() => {
dispatch(getAllBlockedServices());
+ dispatch(getBlockedServices());
dispatch(initSettings());
}, []);
@@ -112,6 +113,9 @@ const ClientsTable = ({
tags: [],
use_global_settings: true,
use_global_blocked_services: true,
+ blocked_services_schedule: {
+ time_zone: LOCAL_TIMEZONE_VALUE,
+ },
safe_search: { ...(safesearch || {}) },
};
};
diff --git a/client/src/components/Settings/Clients/Form.js b/client/src/components/Settings/Clients/Form.js
index 190996e4..652957d0 100644
--- a/client/src/components/Settings/Clients/Form.js
+++ b/client/src/components/Settings/Clients/Form.js
@@ -11,6 +11,7 @@ import Select from 'react-select';
import i18n from '../../../i18n';
import Tabs from '../../ui/Tabs';
import Examples from '../Dns/Upstream/Examples';
+import { ScheduleForm } from '../../Filters/Services/ScheduleForm';
import { toggleAllServices, trimLinesAndRemoveEmpty, captitalizeWords } from '../../../helpers/helpers';
import {
renderInputField,
@@ -137,10 +138,10 @@ let Form = (props) => {
handleSubmit,
reset,
change,
- pristine,
submitting,
useGlobalSettings,
useGlobalServices,
+ blockedServicesSchedule,
toggleClientModal,
processingAdding,
processingUpdating,
@@ -155,6 +156,10 @@ let Form = (props) => {
const [activeTabLabel, setActiveTabLabel] = useState('settings');
+ const handleScheduleSubmit = (values) => {
+ change('blocked_services_schedule', values);
+ };
+
const tabs = {
settings: {
title: 'settings',
@@ -269,6 +274,21 @@ let Form = (props) => {
,
},
+ schedule_services: {
+ title: 'schedule_services',
+ component: (
+ <>
+
+ schedule_services_desc_client
+
+
+ >
+ ),
+ },
upstream_dns: {
title: 'upstream_dns',
component:
@@ -355,8 +375,12 @@ let Form = (props) => {
-
+
{activeTab}
@@ -380,7 +404,6 @@ let Form = (props) => {
disabled={
submitting
|| invalid
- || pristine
|| processingAdding
|| processingUpdating
}
@@ -402,6 +425,7 @@ Form.propTypes = {
toggleClientModal: PropTypes.func.isRequired,
useGlobalSettings: PropTypes.bool,
useGlobalServices: PropTypes.bool,
+ blockedServicesSchedule: PropTypes.object,
t: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
@@ -415,9 +439,11 @@ const selector = formValueSelector(FORM_NAME.CLIENT);
Form = connect((state) => {
const useGlobalSettings = selector(state, 'use_global_settings');
const useGlobalServices = selector(state, 'use_global_blocked_services');
+ const blockedServicesSchedule = selector(state, 'blocked_services_schedule');
return {
useGlobalSettings,
useGlobalServices,
+ blockedServicesSchedule,
};
})(Form);
diff --git a/client/src/components/ui/Icons.js b/client/src/components/ui/Icons.js
index b38da489..288eadfc 100644
--- a/client/src/components/ui/Icons.js
+++ b/client/src/components/ui/Icons.js
@@ -225,6 +225,20 @@ const Icons = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/client/src/components/ui/Modal.css b/client/src/components/ui/Modal.css
index b22b9709..13adb794 100644
--- a/client/src/components/ui/Modal.css
+++ b/client/src/components/ui/Modal.css
@@ -41,7 +41,8 @@
}
@media (min-width: 576px) {
- .modal-dialog--clients {
+ .modal-dialog--clients,
+ .modal-dialog--schedule {
max-width: 650px;
}
}
diff --git a/client/src/components/ui/Tabler.css b/client/src/components/ui/Tabler.css
index 69b0e1c4..d49810e2 100644
--- a/client/src/components/ui/Tabler.css
+++ b/client/src/components/ui/Tabler.css
@@ -470,6 +470,10 @@ hr {
border-top: 1px solid rgba(0, 40, 100, 0.12);
}
+[data-theme=dark] hr {
+ border-color: var(--card-border-color);
+}
+
small,
.small {
font-size: 87.5%;
diff --git a/client/src/containers/Settings.js b/client/src/containers/Settings.js
index 866765de..6222f8eb 100644
--- a/client/src/containers/Settings.js
+++ b/client/src/containers/Settings.js
@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { initSettings, toggleSetting } from '../actions';
-import { getBlockedServices, setBlockedServices } from '../actions/services';
+import { getBlockedServices, updateBlockedServices } from '../actions/services';
import { getStatsConfig, setStatsConfig, resetStats } from '../actions/stats';
import { clearLogs, getLogsConfig, setLogsConfig } from '../actions/queryLogs';
import { getFilteringStatus, setFiltersConfig } from '../actions/filtering';
@@ -24,7 +24,7 @@ const mapDispatchToProps = {
initSettings,
toggleSetting,
getBlockedServices,
- setBlockedServices,
+ updateBlockedServices,
getStatsConfig,
setStatsConfig,
resetStats,
diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js
index ec1e437e..e436d77f 100644
--- a/client/src/helpers/constants.js
+++ b/client/src/helpers/constants.js
@@ -552,3 +552,5 @@ export const DISABLE_PROTECTION_TIMINGS = {
};
export const LOCAL_STORAGE_THEME_KEY = 'account_theme';
+
+export const LOCAL_TIMEZONE_VALUE = 'Local';
diff --git a/client/src/reducers/services.js b/client/src/reducers/services.js
index c0663c8e..07b9947c 100644
--- a/client/src/reducers/services.js
+++ b/client/src/reducers/services.js
@@ -20,9 +20,9 @@ const services = handleActions(
processingAll: false,
}),
- [actions.setBlockedServicesRequest]: (state) => ({ ...state, processingSet: true }),
- [actions.setBlockedServicesFailure]: (state) => ({ ...state, processingSet: false }),
- [actions.setBlockedServicesSuccess]: (state) => ({
+ [actions.updateBlockedServicesRequest]: (state) => ({ ...state, processingSet: true }),
+ [actions.updateBlockedServicesFailure]: (state) => ({ ...state, processingSet: false }),
+ [actions.updateBlockedServicesSuccess]: (state) => ({
...state,
processingSet: false,
}),
@@ -31,7 +31,7 @@ const services = handleActions(
processing: true,
processingAll: true,
processingSet: false,
- list: [],
+ list: {},
allServices: [],
},
);
diff --git a/internal/aghhttp/aghhttp.go b/internal/aghhttp/aghhttp.go
index 6cb2c670..88dfe7a7 100644
--- a/internal/aghhttp/aghhttp.go
+++ b/internal/aghhttp/aghhttp.go
@@ -2,7 +2,6 @@
package aghhttp
import (
- "encoding/json"
"fmt"
"io"
"net/http"
@@ -61,23 +60,3 @@ func WriteTextPlainDeprecated(w http.ResponseWriter, r *http.Request) (isPlainTe
return true
}
-
-// WriteJSONResponse sets the content-type header in w.Header() to
-// "application/json", writes a header with a "200 OK" status, encodes resp to
-// w, calls [Error] on any returned error, and returns it as well.
-func WriteJSONResponse(w http.ResponseWriter, r *http.Request, resp any) (err error) {
- return WriteJSONResponseCode(w, r, http.StatusOK, resp)
-}
-
-// WriteJSONResponseCode is like [WriteJSONResponse] but adds the ability to
-// redefine the status code.
-func WriteJSONResponseCode(w http.ResponseWriter, r *http.Request, code int, resp any) (err error) {
- w.Header().Set(httphdr.ContentType, HdrValApplicationJSON)
- w.WriteHeader(code)
- err = json.NewEncoder(w).Encode(resp)
- if err != nil {
- Error(r, w, http.StatusInternalServerError, "encoding resp: %s", err)
- }
-
- return err
-}
diff --git a/internal/next/websvc/json.go b/internal/aghhttp/json.go
similarity index 75%
rename from internal/next/websvc/json.go
rename to internal/aghhttp/json.go
index f7622b63..b7eca767 100644
--- a/internal/next/websvc/json.go
+++ b/internal/aghhttp/json.go
@@ -1,4 +1,4 @@
-package websvc
+package aghhttp
import (
"encoding/json"
@@ -7,7 +7,6 @@ import (
"strconv"
"time"
- "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log"
)
@@ -87,30 +86,29 @@ func (t *JSONTime) UnmarshalJSON(b []byte) (err error) {
return nil
}
-// writeJSONOKResponse writes headers with the code 200 OK, encodes v into w,
-// and logs any errors it encounters. r is used to get additional information
-// from the request.
-func writeJSONOKResponse(w http.ResponseWriter, r *http.Request, v any) {
- writeJSONResponse(w, r, v, http.StatusOK)
-}
-
-// writeJSONResponse writes headers with code, encodes v into w, and logs any
-// errors it encounters. r is used to get additional information from the
+// WriteJSONResponse writes headers with the code, encodes resp into w, and logs
+// any errors it encounters. r is used to get additional information from the
// request.
-func writeJSONResponse(w http.ResponseWriter, r *http.Request, v any, code int) {
- // TODO(a.garipov): Put some of these to a middleware.
+func WriteJSONResponse(w http.ResponseWriter, r *http.Request, code int, resp any) {
h := w.Header()
- h.Set(httphdr.ContentType, aghhttp.HdrValApplicationJSON)
- h.Set(httphdr.Server, aghhttp.UserAgent())
+ h.Set(httphdr.ContentType, HdrValApplicationJSON)
+ h.Set(httphdr.Server, UserAgent())
w.WriteHeader(code)
- err := json.NewEncoder(w).Encode(v)
+ err := json.NewEncoder(w).Encode(resp)
if err != nil {
- log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
+ log.Error("aghhttp: writing json resp to %s %s: %s", r.Method, r.URL.Path, err)
}
}
+// WriteJSONResponseOK writes headers with the code 200 OK, encodes v into w,
+// and logs any errors it encounters. r is used to get additional information
+// from the request.
+func WriteJSONResponseOK(w http.ResponseWriter, r *http.Request, v any) {
+ WriteJSONResponse(w, r, http.StatusOK, v)
+}
+
// ErrorCode is the error code as used by the HTTP API. See the ErrorCode
// definition in the OpenAPI specification.
type ErrorCode string
@@ -131,14 +129,14 @@ type HTTPAPIErrorResp struct {
Msg string `json:"msg"`
}
-// writeJSONErrorResponse encodes err as a JSON error into w, and logs any
+// WriteJSONResponseError encodes err as a JSON error into w, and logs any
// errors it encounters. r is used to get additional information from the
// request.
-func writeJSONErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
- log.Error("websvc: %s %s: %s", r.Method, r.URL.Path, err)
+func WriteJSONResponseError(w http.ResponseWriter, r *http.Request, err error) {
+ log.Error("aghhttp: writing json error to %s %s: %s", r.Method, r.URL.Path, err)
- writeJSONResponse(w, r, &HTTPAPIErrorResp{
+ WriteJSONResponse(w, r, http.StatusUnprocessableEntity, &HTTPAPIErrorResp{
Code: ErrorCodeTMP000,
Msg: err.Error(),
- }, http.StatusUnprocessableEntity)
+ })
}
diff --git a/internal/next/websvc/json_test.go b/internal/aghhttp/json_test.go
similarity index 81%
rename from internal/next/websvc/json_test.go
rename to internal/aghhttp/json_test.go
index 90874958..9a2d33f1 100644
--- a/internal/next/websvc/json_test.go
+++ b/internal/aghhttp/json_test.go
@@ -1,18 +1,18 @@
-package websvc_test
+package aghhttp_test
import (
"encoding/json"
"testing"
"time"
- "github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testJSONTime is the JSON time for tests.
-var testJSONTime = websvc.JSONTime(time.Unix(1_234_567_890, 123_456_000).UTC())
+var testJSONTime = aghhttp.JSONTime(time.Unix(1_234_567_890, 123_456_000).UTC())
// testJSONTimeStr is the string with the JSON encoding of testJSONTime.
const testJSONTimeStr = "1234567890123.456"
@@ -21,17 +21,17 @@ func TestJSONTime_MarshalJSON(t *testing.T) {
testCases := []struct {
name string
wantErrMsg string
- in websvc.JSONTime
+ in aghhttp.JSONTime
want []byte
}{{
name: "unix_zero",
wantErrMsg: "",
- in: websvc.JSONTime(time.Unix(0, 0)),
+ in: aghhttp.JSONTime(time.Unix(0, 0)),
want: []byte("0"),
}, {
name: "empty",
wantErrMsg: "",
- in: websvc.JSONTime{},
+ in: aghhttp.JSONTime{},
want: []byte("-6795364578871.345"),
}, {
name: "time",
@@ -51,7 +51,7 @@ func TestJSONTime_MarshalJSON(t *testing.T) {
t.Run("json", func(t *testing.T) {
in := &struct {
- A websvc.JSONTime
+ A aghhttp.JSONTime
}{
A: testJSONTime,
}
@@ -67,7 +67,7 @@ func TestJSONTime_UnmarshalJSON(t *testing.T) {
testCases := []struct {
name string
wantErrMsg string
- want websvc.JSONTime
+ want aghhttp.JSONTime
data []byte
}{{
name: "time",
@@ -78,13 +78,13 @@ func TestJSONTime_UnmarshalJSON(t *testing.T) {
name: "bad",
wantErrMsg: `parsing json time: strconv.ParseFloat: parsing "{}": ` +
`invalid syntax`,
- want: websvc.JSONTime{},
+ want: aghhttp.JSONTime{},
data: []byte(`{}`),
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- var got websvc.JSONTime
+ var got aghhttp.JSONTime
err := got.UnmarshalJSON(tc.data)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
@@ -93,7 +93,7 @@ func TestJSONTime_UnmarshalJSON(t *testing.T) {
}
t.Run("nil", func(t *testing.T) {
- err := (*websvc.JSONTime)(nil).UnmarshalJSON([]byte("0"))
+ err := (*aghhttp.JSONTime)(nil).UnmarshalJSON([]byte("0"))
require.Error(t, err)
msg := err.Error()
@@ -103,7 +103,7 @@ func TestJSONTime_UnmarshalJSON(t *testing.T) {
t.Run("json", func(t *testing.T) {
want := testJSONTime
var got struct {
- A websvc.JSONTime
+ A aghhttp.JSONTime
}
err := json.Unmarshal([]byte(`{"A":`+testJSONTimeStr+`}`), &got)
diff --git a/internal/dhcpd/http_unix.go b/internal/dhcpd/http_unix.go
index b07f9543..2ba693cc 100644
--- a/internal/dhcpd/http_unix.go
+++ b/internal/dhcpd/http_unix.go
@@ -146,7 +146,7 @@ func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
status.Leases = leasesToDynamic(s.Leases(LeasesDynamic))
status.StaticLeases = leasesToStatic(s.Leases(LeasesStatic))
- _ = aghhttp.WriteJSONResponse(w, r, status)
+ aghhttp.WriteJSONResponseOK(w, r, status)
}
func (s *server) enableDHCP(ifaceName string) (code int, err error) {
@@ -395,7 +395,7 @@ func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
}
}
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
// newNetInterfaceJSON creates a JSON object from a [net.Interface] iface.
@@ -547,7 +547,7 @@ func (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
setOtherDHCPResult(ifaceName, result)
- _ = aghhttp.WriteJSONResponse(w, r, result)
+ aghhttp.WriteJSONResponseOK(w, r, result)
}
// setOtherDHCPResult sets the results of the check for another DHCP server in
diff --git a/internal/dhcpd/http_windows.go b/internal/dhcpd/http_windows.go
index fda72d48..eb82b861 100644
--- a/internal/dhcpd/http_windows.go
+++ b/internal/dhcpd/http_windows.go
@@ -24,7 +24,7 @@ type jsonError struct {
// TODO(a.garipov): Either take the logger from the server after we've
// refactored logging or make this not a method of *Server.
func (s *server) notImplemented(w http.ResponseWriter, r *http.Request) {
- _ = aghhttp.WriteJSONResponseCode(w, r, http.StatusNotImplemented, &jsonError{
+ aghhttp.WriteJSONResponse(w, r, http.StatusNotImplemented, &jsonError{
Message: aghos.Unsupported("dhcp").Error(),
})
}
diff --git a/internal/dnsforward/access.go b/internal/dnsforward/access.go
index d29afbde..ba9300c2 100644
--- a/internal/dnsforward/access.go
+++ b/internal/dnsforward/access.go
@@ -182,7 +182,7 @@ func (s *Server) accessListJSON() (j accessListJSON) {
}
func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) {
- _ = aghhttp.WriteJSONResponse(w, r, s.accessListJSON())
+ aghhttp.WriteJSONResponseOK(w, r, s.accessListJSON())
}
// validateAccessSet checks the internal accessListJSON lists. To search for
diff --git a/internal/dnsforward/http.go b/internal/dnsforward/http.go
index 184e5e7c..3be25372 100644
--- a/internal/dnsforward/http.go
+++ b/internal/dnsforward/http.go
@@ -169,7 +169,7 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
// handleGetConfig handles requests to the GET /control/dns_info endpoint.
func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
resp := s.getDNSConfig()
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
func (req *jsonDNSConfig) checkBlockingMode() (err error) {
@@ -758,7 +758,7 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
}
}
- _ = aghhttp.WriteJSONResponse(w, r, result)
+ aghhttp.WriteJSONResponseOK(w, r, result)
}
// handleCacheClear is the handler for the POST /control/cache_clear HTTP API.
diff --git a/internal/filtering/blocked.go b/internal/filtering/blocked.go
index f403d0ab..2dba12da 100644
--- a/internal/filtering/blocked.go
+++ b/internal/filtering/blocked.go
@@ -50,10 +50,10 @@ func initBlockedServices() {
// BlockedServices is the configuration of blocked services.
type BlockedServices struct {
// Schedule is blocked services schedule for every day of the week.
- Schedule *schedule.Weekly `yaml:"schedule"`
+ Schedule *schedule.Weekly `json:"schedule" yaml:"schedule"`
// IDs is the names of blocked services.
- IDs []string `yaml:"ids"`
+ IDs []string `json:"ids" yaml:"ids"`
}
// Clone returns a deep copy of blocked services.
@@ -114,25 +114,33 @@ func (d *DNSFilter) ApplyBlockedServicesList(setts *Settings, list []string) {
}
func (d *DNSFilter) handleBlockedServicesIDs(w http.ResponseWriter, r *http.Request) {
- _ = aghhttp.WriteJSONResponse(w, r, serviceIDs)
+ aghhttp.WriteJSONResponseOK(w, r, serviceIDs)
}
func (d *DNSFilter) handleBlockedServicesAll(w http.ResponseWriter, r *http.Request) {
- _ = aghhttp.WriteJSONResponse(w, r, struct {
+ aghhttp.WriteJSONResponseOK(w, r, struct {
BlockedServices []blockedService `json:"blocked_services"`
}{
BlockedServices: blockedServices,
})
}
+// handleBlockedServicesList is the handler for the GET
+// /control/blocked_services/list HTTP API.
+//
+// Deprecated: Use handleBlockedServicesGet.
func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) {
d.confLock.RLock()
list := d.Config.BlockedServices.IDs
d.confLock.RUnlock()
- _ = aghhttp.WriteJSONResponse(w, r, list)
+ aghhttp.WriteJSONResponseOK(w, r, list)
}
+// handleBlockedServicesSet is the handler for the POST
+// /control/blocked_services/set HTTP API.
+//
+// Deprecated: Use handleBlockedServicesUpdate.
func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) {
list := []string{}
err := json.NewDecoder(r.Body).Decode(&list)
@@ -150,3 +158,51 @@ func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Requ
d.Config.ConfigModified()
}
+
+// handleBlockedServicesGet is the handler for the GET
+// /control/blocked_services/get HTTP API.
+func (d *DNSFilter) handleBlockedServicesGet(w http.ResponseWriter, r *http.Request) {
+ var bsvc *BlockedServices
+ func() {
+ d.confLock.RLock()
+ defer d.confLock.RUnlock()
+
+ bsvc = d.Config.BlockedServices.Clone()
+ }()
+
+ aghhttp.WriteJSONResponseOK(w, r, bsvc)
+}
+
+// handleBlockedServicesUpdate is the handler for the PUT
+// /control/blocked_services/update HTTP API.
+func (d *DNSFilter) handleBlockedServicesUpdate(w http.ResponseWriter, r *http.Request) {
+ bsvc := &BlockedServices{}
+ err := json.NewDecoder(r.Body).Decode(bsvc)
+ if err != nil {
+ aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
+
+ return
+ }
+
+ err = bsvc.Validate()
+ if err != nil {
+ aghhttp.Error(r, w, http.StatusUnprocessableEntity, "validating: %s", err)
+
+ return
+ }
+
+ if bsvc.Schedule == nil {
+ bsvc.Schedule = schedule.EmptyWeekly()
+ }
+
+ func() {
+ d.confLock.Lock()
+ defer d.confLock.Unlock()
+
+ d.Config.BlockedServices = bsvc
+ }()
+
+ log.Debug("updated blocked services schedule: %d", len(bsvc.IDs))
+
+ d.Config.ConfigModified()
+}
diff --git a/internal/filtering/http.go b/internal/filtering/http.go
index 43191ca6..df3fe95e 100644
--- a/internal/filtering/http.go
+++ b/internal/filtering/http.go
@@ -301,7 +301,7 @@ func (d *DNSFilter) handleFilteringRefresh(w http.ResponseWriter, r *http.Reques
return
}
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
type filterJSON struct {
@@ -354,7 +354,7 @@ func (d *DNSFilter) handleFilteringStatus(w http.ResponseWriter, r *http.Request
resp.UserRules = d.UserRules
d.filtersMu.RUnlock()
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
// Set filtering configuration
@@ -456,7 +456,7 @@ func (d *DNSFilter) handleCheckHost(w http.ResponseWriter, r *http.Request) {
}
}
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
// setProtectedBool sets the value of a boolean pointer under a lock. l must
@@ -504,7 +504,7 @@ func (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Requ
Enabled: protectedBool(&d.confLock, &d.Config.SafeBrowsingEnabled),
}
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
// handleParentalEnable is the handler for the POST /control/parental/enable
@@ -530,7 +530,7 @@ func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request)
Enabled: protectedBool(&d.confLock, &d.Config.ParentalEnabled),
}
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
// RegisterFilteringHandlers - register handlers
@@ -560,9 +560,14 @@ func (d *DNSFilter) RegisterFilteringHandlers() {
registerHTTP(http.MethodGet, "/control/blocked_services/services", d.handleBlockedServicesIDs)
registerHTTP(http.MethodGet, "/control/blocked_services/all", d.handleBlockedServicesAll)
+
+ // Deprecated handlers.
registerHTTP(http.MethodGet, "/control/blocked_services/list", d.handleBlockedServicesList)
registerHTTP(http.MethodPost, "/control/blocked_services/set", d.handleBlockedServicesSet)
+ registerHTTP(http.MethodGet, "/control/blocked_services/get", d.handleBlockedServicesGet)
+ registerHTTP(http.MethodPut, "/control/blocked_services/update", d.handleBlockedServicesUpdate)
+
registerHTTP(http.MethodGet, "/control/filtering/status", d.handleFilteringStatus)
registerHTTP(http.MethodPost, "/control/filtering/config", d.handleFilteringConfig)
registerHTTP(http.MethodPost, "/control/filtering/add_url", d.handleFilteringAddURL)
diff --git a/internal/filtering/rewritehttp.go b/internal/filtering/rewritehttp.go
index fea5a650..b3dc275a 100644
--- a/internal/filtering/rewritehttp.go
+++ b/internal/filtering/rewritehttp.go
@@ -28,7 +28,7 @@ func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {
}
d.confLock.Unlock()
- _ = aghhttp.WriteJSONResponse(w, r, arr)
+ aghhttp.WriteJSONResponseOK(w, r, arr)
}
func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
diff --git a/internal/filtering/safesearchhttp.go b/internal/filtering/safesearchhttp.go
index 6048cfea..d1358a2f 100644
--- a/internal/filtering/safesearchhttp.go
+++ b/internal/filtering/safesearchhttp.go
@@ -36,7 +36,7 @@ func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Reques
resp = d.Config.SafeSearchConf
}()
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
// handleSafeSearchSettings is the handler for PUT /control/safesearch/settings
diff --git a/internal/home/clientshttp.go b/internal/home/clientshttp.go
index 38ea666d..240ef4de 100644
--- a/internal/home/clientshttp.go
+++ b/internal/home/clientshttp.go
@@ -34,8 +34,12 @@ type clientJSON struct {
WHOIS *whois.Info `json:"whois_info,omitempty"`
SafeSearchConf *filtering.SafeSearchConfig `json:"safe_search"`
+ // Schedule is blocked services schedule for every day of the week.
+ Schedule *schedule.Weekly `json:"blocked_services_schedule"`
+
Name string `json:"name"`
+ // BlockedServices is the names of blocked services.
BlockedServices []string `json:"blocked_services"`
IDs []string `json:"ids"`
Tags []string `json:"tags"`
@@ -53,6 +57,34 @@ type clientJSON struct {
IgnoreStatistics aghalg.NullBool `json:"ignore_statistics"`
}
+// copySettings returns a copy of specific settings from JSON or a previous
+// client.
+func (j *clientJSON) copySettings(
+ prev *Client,
+) (weekly *schedule.Weekly, ignoreQueryLog, ignoreStatistics bool) {
+ if j.Schedule != nil {
+ weekly = j.Schedule.Clone()
+ } else if prev != nil && prev.BlockedServices != nil {
+ weekly = prev.BlockedServices.Schedule.Clone()
+ } else {
+ weekly = schedule.EmptyWeekly()
+ }
+
+ if j.IgnoreQueryLog != aghalg.NBNull {
+ ignoreQueryLog = j.IgnoreQueryLog == aghalg.NBTrue
+ } else if prev != nil {
+ ignoreQueryLog = prev.IgnoreQueryLog
+ }
+
+ if j.IgnoreStatistics != aghalg.NBNull {
+ ignoreStatistics = j.IgnoreStatistics == aghalg.NBTrue
+ } else if prev != nil {
+ ignoreStatistics = prev.IgnoreStatistics
+ }
+
+ return weekly, ignoreQueryLog, ignoreStatistics
+}
+
type runtimeClientJSON struct {
WHOIS *whois.Info `json:"whois_info"`
@@ -93,7 +125,7 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
data.Tags = clientTags
- _ = aghhttp.WriteJSONResponse(w, r, data)
+ aghhttp.WriteJSONResponseOK(w, r, data)
}
// jsonToClient converts JSON object to Client object.
@@ -119,9 +151,15 @@ func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *C
}
}
- weekly := schedule.EmptyWeekly()
- if prev != nil {
- weekly = prev.BlockedServices.Schedule.Clone()
+ weekly, ignoreQueryLog, ignoreStatistics := cj.copySettings(prev)
+
+ bs := &filtering.BlockedServices{
+ Schedule: weekly,
+ IDs: cj.BlockedServices,
+ }
+ err = bs.Validate()
+ if err != nil {
+ return nil, fmt.Errorf("validating blocked services: %w", err)
}
c = &Client{
@@ -129,10 +167,7 @@ func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *C
Name: cj.Name,
- BlockedServices: &filtering.BlockedServices{
- Schedule: weekly,
- IDs: cj.BlockedServices,
- },
+ BlockedServices: bs,
IDs: cj.IDs,
Tags: cj.Tags,
@@ -143,18 +178,8 @@ func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *C
ParentalEnabled: cj.ParentalEnabled,
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
- }
-
- if cj.IgnoreQueryLog != aghalg.NBNull {
- c.IgnoreQueryLog = cj.IgnoreQueryLog == aghalg.NBTrue
- } else if prev != nil {
- c.IgnoreQueryLog = prev.IgnoreQueryLog
- }
-
- if cj.IgnoreStatistics != aghalg.NBNull {
- c.IgnoreStatistics = cj.IgnoreStatistics == aghalg.NBTrue
- } else if prev != nil {
- c.IgnoreStatistics = prev.IgnoreStatistics
+ IgnoreQueryLog: ignoreQueryLog,
+ IgnoreStatistics: ignoreStatistics,
}
if safeSearchConf.Enabled {
@@ -191,6 +216,7 @@ func clientToJSON(c *Client) (cj *clientJSON) {
UseGlobalBlockedServices: !c.UseOwnBlockedServices,
+ Schedule: c.BlockedServices.Schedule,
BlockedServices: c.BlockedServices.IDs,
Upstreams: c.Upstreams,
@@ -338,7 +364,7 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
})
}
- _ = aghhttp.WriteJSONResponse(w, r, data)
+ aghhttp.WriteJSONResponseOK(w, r, data)
}
// findRuntime looks up the IP in runtime and temporary storages, like
diff --git a/internal/home/control.go b/internal/home/control.go
index a2414b35..f7286aed 100644
--- a/internal/home/control.go
+++ b/internal/home/control.go
@@ -170,7 +170,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
resp.IsDHCPAvailable = Context.dhcpServer != nil
}
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
// ------------------------
diff --git a/internal/home/controlinstall.go b/internal/home/controlinstall.go
index a5be3354..d6985eab 100644
--- a/internal/home/controlinstall.go
+++ b/internal/home/controlinstall.go
@@ -59,7 +59,7 @@ func (web *webAPI) handleInstallGetAddresses(w http.ResponseWriter, r *http.Requ
data.Interfaces[iface.Name] = iface
}
- _ = aghhttp.WriteJSONResponse(w, r, data)
+ aghhttp.WriteJSONResponseOK(w, r, data)
}
type checkConfReqEnt struct {
@@ -190,7 +190,7 @@ func (web *webAPI) handleInstallCheckConfig(w http.ResponseWriter, r *http.Reque
resp.StaticIP = handleStaticIP(req.DNS.IP, req.SetStaticIP)
}
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
// handleStaticIP - handles static IP request
diff --git a/internal/home/controlupdate.go b/internal/home/controlupdate.go
index 5238134c..50a1a6f3 100644
--- a/internal/home/controlupdate.go
+++ b/internal/home/controlupdate.go
@@ -33,7 +33,7 @@ func (web *webAPI) handleVersionJSON(w http.ResponseWriter, r *http.Request) {
resp := &versionResponse{}
if web.conf.disableUpdate {
resp.Disabled = true
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
return
}
@@ -68,7 +68,7 @@ func (web *webAPI) handleVersionJSON(w http.ResponseWriter, r *http.Request) {
return
}
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
// requestVersionInfo sets the VersionInfo field of resp if it can reach the
diff --git a/internal/home/i18n.go b/internal/home/i18n.go
index d9e3435c..267205d3 100644
--- a/internal/home/i18n.go
+++ b/internal/home/i18n.go
@@ -58,7 +58,7 @@ type languageJSON struct {
func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {
log.Printf("home: language is %s", config.Language)
- _ = aghhttp.WriteJSONResponse(w, r, &languageJSON{
+ aghhttp.WriteJSONResponseOK(w, r, &languageJSON{
Language: config.Language,
})
}
diff --git a/internal/home/profilehttp.go b/internal/home/profilehttp.go
index 12e036c3..91e36f28 100644
--- a/internal/home/profilehttp.go
+++ b/internal/home/profilehttp.go
@@ -61,7 +61,7 @@ func handleGetProfile(w http.ResponseWriter, r *http.Request) {
}
}()
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
// handlePutProfile is the handler for PUT /control/profile/update endpoint.
diff --git a/internal/home/tls.go b/internal/home/tls.go
index c42d5175..004e9412 100644
--- a/internal/home/tls.go
+++ b/internal/home/tls.go
@@ -770,7 +770,7 @@ func marshalTLS(w http.ResponseWriter, r *http.Request, data tlsConfig) {
data.PrivateKey = ""
}
- _ = aghhttp.WriteJSONResponse(w, r, data)
+ aghhttp.WriteJSONResponseOK(w, r, data)
}
// registerWebHandlers registers HTTP handlers for TLS configuration.
diff --git a/internal/next/websvc/dns.go b/internal/next/websvc/dns.go
index 34cf9a15..39f05d22 100644
--- a/internal/next/websvc/dns.go
+++ b/internal/next/websvc/dns.go
@@ -7,6 +7,7 @@ import (
"net/netip"
"time"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
)
@@ -17,13 +18,13 @@ import (
type ReqPatchSettingsDNS struct {
// TODO(a.garipov): Add more as we go.
- Addresses []netip.AddrPort `json:"addresses"`
- BootstrapServers []string `json:"bootstrap_servers"`
- UpstreamServers []string `json:"upstream_servers"`
- DNS64Prefixes []netip.Prefix `json:"dns64_prefixes"`
- UpstreamTimeout JSONDuration `json:"upstream_timeout"`
- BootstrapPreferIPv6 bool `json:"bootstrap_prefer_ipv6"`
- UseDNS64 bool `json:"use_dns64"`
+ Addresses []netip.AddrPort `json:"addresses"`
+ BootstrapServers []string `json:"bootstrap_servers"`
+ UpstreamServers []string `json:"upstream_servers"`
+ DNS64Prefixes []netip.Prefix `json:"dns64_prefixes"`
+ UpstreamTimeout aghhttp.JSONDuration `json:"upstream_timeout"`
+ BootstrapPreferIPv6 bool `json:"bootstrap_prefer_ipv6"`
+ UseDNS64 bool `json:"use_dns64"`
}
// HTTPAPIDNSSettings are the DNS settings as used by the HTTP API. See the
@@ -31,13 +32,13 @@ type ReqPatchSettingsDNS struct {
type HTTPAPIDNSSettings struct {
// TODO(a.garipov): Add more as we go.
- Addresses []netip.AddrPort `json:"addresses"`
- BootstrapServers []string `json:"bootstrap_servers"`
- UpstreamServers []string `json:"upstream_servers"`
- DNS64Prefixes []netip.Prefix `json:"dns64_prefixes"`
- UpstreamTimeout JSONDuration `json:"upstream_timeout"`
- BootstrapPreferIPv6 bool `json:"bootstrap_prefer_ipv6"`
- UseDNS64 bool `json:"use_dns64"`
+ Addresses []netip.AddrPort `json:"addresses"`
+ BootstrapServers []string `json:"bootstrap_servers"`
+ UpstreamServers []string `json:"upstream_servers"`
+ DNS64Prefixes []netip.Prefix `json:"dns64_prefixes"`
+ UpstreamTimeout aghhttp.JSONDuration `json:"upstream_timeout"`
+ BootstrapPreferIPv6 bool `json:"bootstrap_prefer_ipv6"`
+ UseDNS64 bool `json:"use_dns64"`
}
// handlePatchSettingsDNS is the handler for the PATCH /api/v1/settings/dns HTTP
@@ -53,7 +54,7 @@ func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Reques
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
- writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
+ aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("decoding: %w", err))
return
}
@@ -71,7 +72,7 @@ func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Reques
ctx := r.Context()
err = svc.confMgr.UpdateDNS(ctx, newConf)
if err != nil {
- writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", err))
+ aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("updating: %w", err))
return
}
@@ -79,17 +80,17 @@ func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Reques
newSvc := svc.confMgr.DNS()
err = newSvc.Start()
if err != nil {
- writeJSONErrorResponse(w, r, fmt.Errorf("starting new service: %w", err))
+ aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("starting new service: %w", err))
return
}
- writeJSONOKResponse(w, r, &HTTPAPIDNSSettings{
+ aghhttp.WriteJSONResponseOK(w, r, &HTTPAPIDNSSettings{
Addresses: newConf.Addresses,
BootstrapServers: newConf.BootstrapServers,
UpstreamServers: newConf.UpstreamServers,
DNS64Prefixes: newConf.DNS64Prefixes,
- UpstreamTimeout: JSONDuration(newConf.UpstreamTimeout),
+ UpstreamTimeout: aghhttp.JSONDuration(newConf.UpstreamTimeout),
BootstrapPreferIPv6: newConf.BootstrapPreferIPv6,
UseDNS64: newConf.UseDNS64,
})
diff --git a/internal/next/websvc/dns_test.go b/internal/next/websvc/dns_test.go
index c39ba1ab..5f6e3d44 100644
--- a/internal/next/websvc/dns_test.go
+++ b/internal/next/websvc/dns_test.go
@@ -10,6 +10,7 @@ import (
"testing"
"time"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
@@ -24,7 +25,7 @@ func TestService_HandlePatchSettingsDNS(t *testing.T) {
BootstrapServers: []string{"1.0.0.1"},
UpstreamServers: []string{"1.1.1.1"},
DNS64Prefixes: []netip.Prefix{netip.MustParsePrefix("1234::/64")},
- UpstreamTimeout: websvc.JSONDuration(2 * time.Second),
+ UpstreamTimeout: aghhttp.JSONDuration(2 * time.Second),
BootstrapPreferIPv6: true,
UseDNS64: true,
}
diff --git a/internal/next/websvc/http.go b/internal/next/websvc/http.go
index 7e4785cd..db32372d 100644
--- a/internal/next/websvc/http.go
+++ b/internal/next/websvc/http.go
@@ -8,6 +8,7 @@ import (
"net/netip"
"time"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/golibs/log"
)
@@ -21,9 +22,9 @@ type ReqPatchSettingsHTTP struct {
//
// TODO(a.garipov): Add wait time.
- Addresses []netip.AddrPort `json:"addresses"`
- SecureAddresses []netip.AddrPort `json:"secure_addresses"`
- Timeout JSONDuration `json:"timeout"`
+ Addresses []netip.AddrPort `json:"addresses"`
+ SecureAddresses []netip.AddrPort `json:"secure_addresses"`
+ Timeout aghhttp.JSONDuration `json:"timeout"`
}
// HTTPAPIHTTPSettings are the HTTP settings as used by the HTTP API. See the
@@ -31,10 +32,10 @@ type ReqPatchSettingsHTTP struct {
type HTTPAPIHTTPSettings struct {
// TODO(a.garipov): Add more as we go.
- Addresses []netip.AddrPort `json:"addresses"`
- SecureAddresses []netip.AddrPort `json:"secure_addresses"`
- Timeout JSONDuration `json:"timeout"`
- ForceHTTPS bool `json:"force_https"`
+ Addresses []netip.AddrPort `json:"addresses"`
+ SecureAddresses []netip.AddrPort `json:"secure_addresses"`
+ Timeout aghhttp.JSONDuration `json:"timeout"`
+ ForceHTTPS bool `json:"force_https"`
}
// handlePatchSettingsHTTP is the handler for the PATCH /api/v1/settings/http
@@ -46,7 +47,7 @@ func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Reque
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
- writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
+ aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("decoding: %w", err))
return
}
@@ -65,10 +66,10 @@ func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Reque
ForceHTTPS: svc.forceHTTPS,
}
- writeJSONOKResponse(w, r, &HTTPAPIHTTPSettings{
+ aghhttp.WriteJSONResponseOK(w, r, &HTTPAPIHTTPSettings{
Addresses: newConf.Addresses,
SecureAddresses: newConf.SecureAddresses,
- Timeout: JSONDuration(newConf.Timeout),
+ Timeout: aghhttp.JSONDuration(newConf.Timeout),
ForceHTTPS: newConf.ForceHTTPS,
})
diff --git a/internal/next/websvc/http_test.go b/internal/next/websvc/http_test.go
index c04921f6..3168ec03 100644
--- a/internal/next/websvc/http_test.go
+++ b/internal/next/websvc/http_test.go
@@ -10,6 +10,7 @@ import (
"testing"
"time"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
"github.com/stretchr/testify/assert"
@@ -20,7 +21,7 @@ func TestService_HandlePatchSettingsHTTP(t *testing.T) {
wantWeb := &websvc.HTTPAPIHTTPSettings{
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:80")},
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:443")},
- Timeout: websvc.JSONDuration(10 * time.Second),
+ Timeout: aghhttp.JSONDuration(10 * time.Second),
ForceHTTPS: false,
}
diff --git a/internal/next/websvc/settings.go b/internal/next/websvc/settings.go
index ab308836..44364ca3 100644
--- a/internal/next/websvc/settings.go
+++ b/internal/next/websvc/settings.go
@@ -2,6 +2,8 @@ package websvc
import (
"net/http"
+
+ "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
)
// All Settings Handlers
@@ -25,20 +27,20 @@ func (svc *Service) handleGetSettingsAll(w http.ResponseWriter, r *http.Request)
httpConf := webSvc.Config()
// TODO(a.garipov): Add all currently supported parameters.
- writeJSONOKResponse(w, r, &RespGetV1SettingsAll{
+ aghhttp.WriteJSONResponseOK(w, r, &RespGetV1SettingsAll{
DNS: &HTTPAPIDNSSettings{
Addresses: dnsConf.Addresses,
BootstrapServers: dnsConf.BootstrapServers,
UpstreamServers: dnsConf.UpstreamServers,
DNS64Prefixes: dnsConf.DNS64Prefixes,
- UpstreamTimeout: JSONDuration(dnsConf.UpstreamTimeout),
+ UpstreamTimeout: aghhttp.JSONDuration(dnsConf.UpstreamTimeout),
BootstrapPreferIPv6: dnsConf.BootstrapPreferIPv6,
UseDNS64: dnsConf.UseDNS64,
},
HTTP: &HTTPAPIHTTPSettings{
Addresses: httpConf.Addresses,
SecureAddresses: httpConf.SecureAddresses,
- Timeout: JSONDuration(httpConf.Timeout),
+ Timeout: aghhttp.JSONDuration(httpConf.Timeout),
ForceHTTPS: httpConf.ForceHTTPS,
},
})
diff --git a/internal/next/websvc/settings_test.go b/internal/next/websvc/settings_test.go
index cacf28a9..f07d8b3e 100644
--- a/internal/next/websvc/settings_test.go
+++ b/internal/next/websvc/settings_test.go
@@ -9,6 +9,7 @@ import (
"testing"
"time"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
@@ -23,14 +24,14 @@ func TestService_HandleGetSettingsAll(t *testing.T) {
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:53")},
BootstrapServers: []string{"94.140.14.140", "94.140.14.141"},
UpstreamServers: []string{"94.140.14.14", "1.1.1.1"},
- UpstreamTimeout: websvc.JSONDuration(1 * time.Second),
+ UpstreamTimeout: aghhttp.JSONDuration(1 * time.Second),
BootstrapPreferIPv6: true,
}
wantWeb := &websvc.HTTPAPIHTTPSettings{
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
- Timeout: websvc.JSONDuration(5 * time.Second),
+ Timeout: aghhttp.JSONDuration(5 * time.Second),
ForceHTTPS: true,
}
diff --git a/internal/next/websvc/system.go b/internal/next/websvc/system.go
index fbf60fe4..d4ca34ad 100644
--- a/internal/next/websvc/system.go
+++ b/internal/next/websvc/system.go
@@ -4,6 +4,7 @@ import (
"net/http"
"runtime"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/version"
)
@@ -12,24 +13,24 @@ import (
// RespGetV1SystemInfo describes the response of the GET /api/v1/system/info
// HTTP API.
type RespGetV1SystemInfo struct {
- Arch string `json:"arch"`
- Channel string `json:"channel"`
- OS string `json:"os"`
- NewVersion string `json:"new_version,omitempty"`
- Start JSONTime `json:"start"`
- Version string `json:"version"`
+ Arch string `json:"arch"`
+ Channel string `json:"channel"`
+ OS string `json:"os"`
+ NewVersion string `json:"new_version,omitempty"`
+ Start aghhttp.JSONTime `json:"start"`
+ Version string `json:"version"`
}
// handleGetV1SystemInfo is the handler for the GET /api/v1/system/info HTTP
// API.
func (svc *Service) handleGetV1SystemInfo(w http.ResponseWriter, r *http.Request) {
- writeJSONOKResponse(w, r, &RespGetV1SystemInfo{
+ aghhttp.WriteJSONResponseOK(w, r, &RespGetV1SystemInfo{
Arch: runtime.GOARCH,
Channel: version.Channel(),
OS: runtime.GOOS,
// TODO(a.garipov): Fill this when we have an updater.
NewVersion: "",
- Start: JSONTime(svc.start),
+ Start: aghhttp.JSONTime(svc.start),
Version: version.Version(),
})
}
diff --git a/internal/querylog/http.go b/internal/querylog/http.go
index 1a29e23d..7a5c24a9 100644
--- a/internal/querylog/http.go
+++ b/internal/querylog/http.go
@@ -93,7 +93,7 @@ func (l *queryLog) handleQueryLog(w http.ResponseWriter, r *http.Request) {
resp := entriesToJSON(entries, oldest, l.anonymizer.Load())
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
// handleQueryLogClear is the handler for the POST /control/querylog/clear HTTP
@@ -118,7 +118,7 @@ func (l *queryLog) handleQueryLogInfo(w http.ResponseWriter, r *http.Request) {
ivl = timeutil.Day * 90
}
- _ = aghhttp.WriteJSONResponse(w, r, configJSON{
+ aghhttp.WriteJSONResponseOK(w, r, configJSON{
Enabled: aghalg.BoolToNullBool(l.conf.Enabled),
Interval: ivl.Hours() / 24,
AnonymizeClientIP: aghalg.BoolToNullBool(l.conf.AnonymizeClientIP),
@@ -143,7 +143,7 @@ func (l *queryLog) handleGetQueryLogConfig(w http.ResponseWriter, r *http.Reques
slices.Sort(resp.Ignored)
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
// AnonymizeIP masks ip to anonymize the client if the ip is a valid one.
diff --git a/internal/schedule/schedule.go b/internal/schedule/schedule.go
index 1bf96016..fe5aeb82 100644
--- a/internal/schedule/schedule.go
+++ b/internal/schedule/schedule.go
@@ -2,9 +2,11 @@
package schedule
import (
+ "encoding/json"
"fmt"
"time"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/timeutil"
"gopkg.in/yaml.v3"
@@ -50,6 +52,10 @@ func FullWeekly() (w *Weekly) {
// Clone returns a deep copy of a weekly.
func (w *Weekly) Clone() (c *Weekly) {
+ if w == nil {
+ return nil
+ }
+
// NOTE: Do not use time.LoadLocation, because the results will be
// different on time zone database update.
return &Weekly{
@@ -75,12 +81,62 @@ func (w *Weekly) Contains(t time.Time) (ok bool) {
return dr.contains(offset)
}
+// type check
+var _ json.Unmarshaler = (*Weekly)(nil)
+
+// UnmarshalJSON implements the [json.Unmarshaler] interface for *Weekly.
+func (w *Weekly) UnmarshalJSON(data []byte) (err error) {
+ conf := &weeklyConfigJSON{}
+ err = json.Unmarshal(data, conf)
+ if err != nil {
+ return err
+ }
+
+ weekly := Weekly{}
+
+ weekly.location, err = time.LoadLocation(conf.TimeZone)
+ if err != nil {
+ return err
+ }
+
+ days := []*dayConfigJSON{
+ time.Sunday: conf.Sunday,
+ time.Monday: conf.Monday,
+ time.Tuesday: conf.Tuesday,
+ time.Wednesday: conf.Wednesday,
+ time.Thursday: conf.Thursday,
+ time.Friday: conf.Friday,
+ time.Saturday: conf.Saturday,
+ }
+ for i, d := range days {
+ var r dayRange
+
+ if d != nil {
+ r = dayRange{
+ start: time.Duration(d.Start),
+ end: time.Duration(d.End),
+ }
+ }
+
+ err = w.validate(r)
+ if err != nil {
+ return fmt.Errorf("weekday %s: %w", time.Weekday(i), err)
+ }
+
+ weekly.days[i] = r
+ }
+
+ *w = weekly
+
+ return nil
+}
+
// type check
var _ yaml.Unmarshaler = (*Weekly)(nil)
// UnmarshalYAML implements the [yaml.Unmarshaler] interface for *Weekly.
func (w *Weekly) UnmarshalYAML(value *yaml.Node) (err error) {
- conf := &weeklyConfig{}
+ conf := &weeklyConfigYAML{}
err = value.Decode(conf)
if err != nil {
@@ -96,7 +152,7 @@ func (w *Weekly) UnmarshalYAML(value *yaml.Node) (err error) {
return err
}
- days := []dayConfig{
+ days := []dayConfigYAML{
time.Sunday: conf.Sunday,
time.Monday: conf.Monday,
time.Tuesday: conf.Tuesday,
@@ -124,24 +180,24 @@ func (w *Weekly) UnmarshalYAML(value *yaml.Node) (err error) {
return nil
}
-// weeklyConfig is the YAML configuration structure of Weekly.
-type weeklyConfig struct {
+// weeklyConfigYAML is the YAML configuration structure of Weekly.
+type weeklyConfigYAML struct {
// TimeZone is the local time zone.
TimeZone string `yaml:"time_zone"`
// Days of the week.
- Sunday dayConfig `yaml:"sun,omitempty"`
- Monday dayConfig `yaml:"mon,omitempty"`
- Tuesday dayConfig `yaml:"tue,omitempty"`
- Wednesday dayConfig `yaml:"wed,omitempty"`
- Thursday dayConfig `yaml:"thu,omitempty"`
- Friday dayConfig `yaml:"fri,omitempty"`
- Saturday dayConfig `yaml:"sat,omitempty"`
+ Sunday dayConfigYAML `yaml:"sun,omitempty"`
+ Monday dayConfigYAML `yaml:"mon,omitempty"`
+ Tuesday dayConfigYAML `yaml:"tue,omitempty"`
+ Wednesday dayConfigYAML `yaml:"wed,omitempty"`
+ Thursday dayConfigYAML `yaml:"thu,omitempty"`
+ Friday dayConfigYAML `yaml:"fri,omitempty"`
+ Saturday dayConfigYAML `yaml:"sat,omitempty"`
}
-// dayConfig is the YAML configuration structure of dayRange.
-type dayConfig struct {
+// dayConfigYAML is the YAML configuration structure of dayRange.
+type dayConfigYAML struct {
Start timeutil.Duration `yaml:"start"`
End timeutil.Duration `yaml:"end"`
}
@@ -172,38 +228,57 @@ func (w *Weekly) validate(r dayRange) (err error) {
}
}
+// type check
+var _ json.Marshaler = (*Weekly)(nil)
+
+// MarshalJSON implements the [json.Marshaler] interface for *Weekly.
+func (w *Weekly) MarshalJSON() (data []byte, err error) {
+ c := &weeklyConfigJSON{
+ TimeZone: w.location.String(),
+ Sunday: w.days[time.Sunday].toDayConfigJSON(),
+ Monday: w.days[time.Monday].toDayConfigJSON(),
+ Tuesday: w.days[time.Tuesday].toDayConfigJSON(),
+ Wednesday: w.days[time.Wednesday].toDayConfigJSON(),
+ Thursday: w.days[time.Thursday].toDayConfigJSON(),
+ Friday: w.days[time.Friday].toDayConfigJSON(),
+ Saturday: w.days[time.Saturday].toDayConfigJSON(),
+ }
+
+ return json.Marshal(c)
+}
+
// type check
var _ yaml.Marshaler = (*Weekly)(nil)
// MarshalYAML implements the [yaml.Marshaler] interface for *Weekly.
func (w *Weekly) MarshalYAML() (v any, err error) {
- return weeklyConfig{
+ return weeklyConfigYAML{
TimeZone: w.location.String(),
- Sunday: dayConfig{
+ Sunday: dayConfigYAML{
Start: timeutil.Duration{Duration: w.days[time.Sunday].start},
End: timeutil.Duration{Duration: w.days[time.Sunday].end},
},
- Monday: dayConfig{
+ Monday: dayConfigYAML{
Start: timeutil.Duration{Duration: w.days[time.Monday].start},
End: timeutil.Duration{Duration: w.days[time.Monday].end},
},
- Tuesday: dayConfig{
+ Tuesday: dayConfigYAML{
Start: timeutil.Duration{Duration: w.days[time.Tuesday].start},
End: timeutil.Duration{Duration: w.days[time.Tuesday].end},
},
- Wednesday: dayConfig{
+ Wednesday: dayConfigYAML{
Start: timeutil.Duration{Duration: w.days[time.Wednesday].start},
End: timeutil.Duration{Duration: w.days[time.Wednesday].end},
},
- Thursday: dayConfig{
+ Thursday: dayConfigYAML{
Start: timeutil.Duration{Duration: w.days[time.Thursday].start},
End: timeutil.Duration{Duration: w.days[time.Thursday].end},
},
- Friday: dayConfig{
+ Friday: dayConfigYAML{
Start: timeutil.Duration{Duration: w.days[time.Friday].start},
End: timeutil.Duration{Duration: w.days[time.Friday].end},
},
- Saturday: dayConfig{
+ Saturday: dayConfigYAML{
Start: timeutil.Duration{Duration: w.days[time.Saturday].start},
End: timeutil.Duration{Duration: w.days[time.Saturday].end},
},
@@ -248,3 +323,38 @@ func (r dayRange) validate() (err error) {
func (r *dayRange) contains(offset time.Duration) (ok bool) {
return r.start <= offset && offset < r.end
}
+
+// toDayConfigJSON returns nil if the day range is empty, otherwise returns
+// initialized JSON configuration of the day range.
+func (r dayRange) toDayConfigJSON() (j *dayConfigJSON) {
+ if (r == dayRange{}) {
+ return nil
+ }
+
+ return &dayConfigJSON{
+ Start: aghhttp.JSONDuration(r.start),
+ End: aghhttp.JSONDuration(r.end),
+ }
+}
+
+// weeklyConfigJSON is the JSON configuration structure of Weekly.
+type weeklyConfigJSON struct {
+ // TimeZone is the local time zone.
+ TimeZone string `json:"time_zone"`
+
+ // Days of the week.
+
+ Sunday *dayConfigJSON `json:"sun,omitempty"`
+ Monday *dayConfigJSON `json:"mon,omitempty"`
+ Tuesday *dayConfigJSON `json:"tue,omitempty"`
+ Wednesday *dayConfigJSON `json:"wed,omitempty"`
+ Thursday *dayConfigJSON `json:"thu,omitempty"`
+ Friday *dayConfigJSON `json:"fri,omitempty"`
+ Saturday *dayConfigJSON `json:"sat,omitempty"`
+}
+
+// dayConfigJSON is the JSON configuration structure of dayRange.
+type dayConfigJSON struct {
+ Start aghhttp.JSONDuration `json:"start"`
+ End aghhttp.JSONDuration `json:"end"`
+}
diff --git a/internal/schedule/schedule_internal_test.go b/internal/schedule/schedule_internal_test.go
index f500524e..0d9aa705 100644
--- a/internal/schedule/schedule_internal_test.go
+++ b/internal/schedule/schedule_internal_test.go
@@ -1,6 +1,7 @@
package schedule
import (
+ "encoding/json"
"testing"
"time"
@@ -122,7 +123,7 @@ func TestWeekly_Contains(t *testing.T) {
}
}
-const brusselsSunday = `
+const brusselsSundayYAML = `
sun:
start: 12h
end: 14h
@@ -179,7 +180,7 @@ yaml: "bad"
}, {
name: "brussels_sunday",
wantErrMsg: "",
- data: []byte(brusselsSunday),
+ data: []byte(brusselsSundayYAML),
want: brusselsWeekly,
}, {
name: "start_equal_end",
@@ -240,7 +241,7 @@ func TestWeekly_MarshalYAML(t *testing.T) {
want: &Weekly{},
}, {
name: "brussels_sunday",
- data: []byte(brusselsSunday),
+ data: []byte(brusselsSundayYAML),
want: brusselsWeekly,
}}
@@ -369,3 +370,142 @@ func TestDayRange_Validate(t *testing.T) {
})
}
}
+
+const brusselsSundayJSON = `{
+ "sun": {
+ "end": 50400000,
+ "start": 43200000
+ },
+ "time_zone": "Europe/Brussels"
+}`
+
+func TestWeekly_UnmarshalJSON(t *testing.T) {
+ const (
+ sameTime = `{
+ "sun": {
+ "end": 32400000,
+ "start": 32400000
+ }
+}`
+ negativeStart = `{
+ "sun": {
+ "end": 3600000,
+ "start": -3600000
+ }
+}`
+ badTZ = `{
+ "time_zone": "bad_timezone"
+}`
+ badJSON = `{
+ "bad": "json",
+}`
+ )
+
+ brusseltsTZ, err := time.LoadLocation("Europe/Brussels")
+ require.NoError(t, err)
+
+ brusselsWeekly := &Weekly{
+ days: [7]dayRange{{
+ start: time.Hour * 12,
+ end: time.Hour * 14,
+ }},
+ location: brusseltsTZ,
+ }
+
+ testCases := []struct {
+ name string
+ wantErrMsg string
+ data []byte
+ want *Weekly
+ }{{
+ name: "empty",
+ wantErrMsg: "unexpected end of JSON input",
+ data: []byte(""),
+ want: &Weekly{},
+ }, {
+ name: "null",
+ wantErrMsg: "",
+ data: []byte("null"),
+ want: &Weekly{location: time.UTC},
+ }, {
+ name: "brussels_sunday",
+ wantErrMsg: "",
+ data: []byte(brusselsSundayJSON),
+ want: brusselsWeekly,
+ }, {
+ name: "start_equal_end",
+ wantErrMsg: "weekday Sunday: bad day range: start 9h0m0s is greater or equal to end 9h0m0s",
+ data: []byte(sameTime),
+ want: &Weekly{},
+ }, {
+ name: "start_negative",
+ wantErrMsg: "weekday Sunday: bad day range: start -1h0m0s is negative",
+ data: []byte(negativeStart),
+ want: &Weekly{},
+ }, {
+ name: "bad_time_zone",
+ wantErrMsg: "unknown time zone bad_timezone",
+ data: []byte(badTZ),
+ want: &Weekly{},
+ }, {
+ name: "bad_json",
+ wantErrMsg: "invalid character '}' looking for beginning of object key string",
+ data: []byte(badJSON),
+ want: &Weekly{},
+ }}
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ w := &Weekly{}
+ err = json.Unmarshal(tc.data, w)
+ testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
+
+ assert.Equal(t, tc.want, w)
+ })
+ }
+}
+
+func TestWeekly_MarshalJSON(t *testing.T) {
+ brusselsTZ, err := time.LoadLocation("Europe/Brussels")
+ require.NoError(t, err)
+
+ brusselsWeekly := &Weekly{
+ days: [7]dayRange{time.Sunday: {
+ start: time.Hour * 12,
+ end: time.Hour * 14,
+ }},
+ location: brusselsTZ,
+ }
+
+ testCases := []struct {
+ name string
+ data []byte
+ want *Weekly
+ }{{
+ name: "empty",
+ data: []byte(""),
+ want: &Weekly{},
+ }, {
+ name: "null",
+ data: []byte("null"),
+ want: &Weekly{},
+ }, {
+ name: "brussels_sunday",
+ data: []byte(brusselsSundayJSON),
+ want: brusselsWeekly,
+ }}
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ var data []byte
+ data, err = json.Marshal(brusselsWeekly)
+ require.NoError(t, err)
+
+ w := &Weekly{}
+ err = json.Unmarshal(data, w)
+ require.NoError(t, err)
+
+ assert.Equal(t, brusselsWeekly, w)
+ })
+ }
+}
diff --git a/internal/stats/http.go b/internal/stats/http.go
index 764579b1..3eee5bbb 100644
--- a/internal/stats/http.go
+++ b/internal/stats/http.go
@@ -73,7 +73,7 @@ func (s *StatsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
return
}
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
// configResp is the response to the GET /control/stats_info.
@@ -122,7 +122,7 @@ func (s *StatsCtx) handleStatsInfo(w http.ResponseWriter, r *http.Request) {
resp.IntervalDays = 0
}
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
// handleGetStatsConfig is the handler for the GET /control/stats/config HTTP
@@ -142,7 +142,7 @@ func (s *StatsCtx) handleGetStatsConfig(w http.ResponseWriter, r *http.Request)
slices.Sort(resp.Ignored)
- _ = aghhttp.WriteJSONResponse(w, r, resp)
+ aghhttp.WriteJSONResponseOK(w, r, resp)
}
// handleStatsConfig is the handler for the POST /control/stats_config HTTP API.
diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md
index fe35e4c9..667dff8c 100644
--- a/openapi/CHANGELOG.md
+++ b/openapi/CHANGELOG.md
@@ -4,6 +4,86 @@
## v0.108.0: API changes
+## v0.107.37: API changes
+
+### Deprecated blocked services APIs
+
+* The `GET /control/blocked_services/list` HTTP API; use the new `GET
+ /control/blocked_services/get` API instead.
+
+* The `POST /control/blocked_services/set` HTTP API; use the new `PUT
+ /control/blocked_services/update` API instead.
+
+### New blocked services APIs
+
+* The new `GET /control/blocked_services/get` HTTP API.
+
+* The new `PUT /control/blocked_services/update` HTTP API allows config
+ updates.
+
+These APIs accept and return a JSON object with the following format:
+
+```json
+{
+ "schedule": {
+ "time_zone": "Local",
+ "sun": {
+ "start": 46800000,
+ "end": 82800000
+ }
+ },
+ "ids": [
+ "vk"
+ ]
+}
+```
+
+### `/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 new field `blocked_services_schedule` has been added to JSON objects. It
+has the following format:
+
+```json
+{
+ "time_zone": "Local",
+ "sun": {
+ "start": 0,
+ "end": 86400000
+ },
+ "mon": {
+ "start": 60000,
+ "end": 82800000
+ },
+ "thu": {
+ "start": 120000,
+ "end": 79200000
+ },
+ "tue": {
+ "start": 180000,
+ "end": 75600000
+ },
+ "wed": {
+ "start": 240000,
+ "end": 72000000
+ },
+ "fri": {
+ "start": 300000,
+ "end": 68400000
+ },
+ "sat": {
+ "start": 360000,
+ "end": 64800000
+ }
+}
+```
+
## v0.107.36: API changes
### The new fields `"top_upstreams_responses"` and `"top_upstreams_avg_time"` in `Stats` object
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 64f376e4..89a1a1c1 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -1001,6 +1001,9 @@
'$ref': '#/components/schemas/BlockedServicesAll'
'/blocked_services/list':
'get':
+ 'deprecated': true
+ 'description': >
+ Deprecated: Use `GET /blocked_services/get` instead.
'tags':
- 'blocked_services'
'operationId': 'blockedServicesList'
@@ -1014,6 +1017,9 @@
'$ref': '#/components/schemas/BlockedServicesArray'
'/blocked_services/set':
'post':
+ 'deprecated': true
+ 'description': >
+ Deprecated: Use `PUT /blocked_services/update` instead.
'tags':
- 'blocked_services'
'operationId': 'blockedServicesSet'
@@ -1026,6 +1032,34 @@
'responses':
'200':
'description': 'OK.'
+ '/blocked_services/get':
+ 'get':
+ 'tags':
+ - 'blocked_services'
+ 'operationId': 'blockedServicesSchedule'
+ 'summary': 'Get blocked services'
+ 'responses':
+ '200':
+ 'description': 'OK.'
+ 'content':
+ 'application/json':
+ 'schema':
+ '$ref': '#/components/schemas/BlockedServicesSchedule'
+ '/blocked_services/update':
+ 'put':
+ 'tags':
+ - 'blocked_services'
+ 'operationId': 'blockedServicesScheduleUpdate'
+ 'summary': 'Update blocked services'
+ 'requestBody':
+ 'content':
+ 'application/json':
+ 'schema':
+ '$ref': '#/components/schemas/BlockedServicesSchedule'
+ 'required': true
+ 'responses':
+ '200':
+ 'description': 'OK.'
'/rewrite/list':
'get':
'tags':
@@ -2485,6 +2519,54 @@
'type': 'boolean'
'youtube':
'type': 'boolean'
+ 'Schedule':
+ 'type': 'object'
+ 'description': >
+ Sets periods of inactivity for filtering blocked services. The
+ schedule contains 7 days (Sunday to Saturday) and a time zone.
+ 'properties':
+ 'time_zone':
+ 'description': >
+ Time zone name according to IANA time zone database. For example
+ `Europe/Brussels`. `Local` represents the system's local time
+ zone.
+ 'type': 'string'
+ 'sun':
+ '$ref': '#/components/schemas/DayRange'
+ 'mon':
+ '$ref': '#/components/schemas/DayRange'
+ 'tue':
+ '$ref': '#/components/schemas/DayRange'
+ 'wed':
+ '$ref': '#/components/schemas/DayRange'
+ 'thu':
+ '$ref': '#/components/schemas/DayRange'
+ 'fri':
+ '$ref': '#/components/schemas/DayRange'
+ 'sat':
+ '$ref': '#/components/schemas/DayRange'
+ 'DayRange':
+ 'type': 'object'
+ 'description': >
+ The single interval within a day. It begins at the `start` and ends
+ before the `end`.
+ 'properties':
+ 'start':
+ 'type': 'number'
+ 'description': >
+ The number of milliseconds elapsed from the start of a day. It
+ must be less than `end` and is expected to be rounded to minutes.
+ So the maximum value is `86340000` (23 hours and 59 minutes).
+ 'minimum': 0
+ 'maximum': 86340000
+ 'end':
+ 'type': 'number'
+ 'description': >
+ The number of milliseconds elapsed from the start of a day. It is
+ expected to be rounded to minutes. The maximum value is `86400000`
+ (24 hours).
+ 'minimum': 0
+ 'maximum': 86400000
'Client':
'type': 'object'
'description': 'Client information.'
@@ -2513,6 +2595,8 @@
'$ref': '#/components/schemas/SafeSearchConfig'
'use_global_blocked_services':
'type': 'boolean'
+ 'blocked_services_schedule':
+ '$ref': '#/components/schemas/Schedule'
'blocked_services':
'type': 'array'
'items':
@@ -2793,6 +2877,17 @@
- 'name'
- 'rules'
'type': 'object'
+ 'BlockedServicesSchedule':
+ 'type': 'object'
+ 'properties':
+ 'schedule':
+ '$ref': '#/components/schemas/Schedule'
+ 'ids':
+ 'description': >
+ The names of the blocked services.
+ 'type': 'array'
+ 'items':
+ 'type': 'string'
'CheckConfigRequest':
'type': 'object'
'description': 'Configuration to be checked'