Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions app/client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,12 @@ export interface FeedsSetting {
* @memberof FeedsSetting
*/
'crawlFullContent'?: boolean;
/**
*
* @type {number}
* @memberof FeedsSetting
*/
'defaultFetchIntervalMinutes'?: number;
/**
*
* @type {boolean}
Expand Down Expand Up @@ -1361,6 +1367,12 @@ export interface GlobalSetting {
* @memberof GlobalSetting
*/
'coldDataKeepDays'?: number;
/**
*
* @type {number}
* @memberof GlobalSetting
*/
'defaultFeedFetchIntervalMinutes'?: number;
/**
*
* @type {string}
Expand Down Expand Up @@ -10974,5 +10986,3 @@ export class TweetControllerApi extends BaseAPI {
return TweetControllerApiFp(this.configuration).trackReadUsingPOST(tweetId, options).then((request) => request(this.axios, this.basePath));
}
}


36 changes: 28 additions & 8 deletions app/client/src/components/SettingModal/FeedsFormDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
Dialog,
DialogActions,
DialogContent, DialogContentText,
DialogTitle, FormControl, FormControlLabel, InputLabel, MenuItem, Select,
DialogTitle, FormControl, FormControlLabel, InputAdornment, InputLabel, MenuItem, Select,
TextField
} from "@mui/material";
import {
Expand All @@ -26,7 +26,7 @@ export default function FeedsFormDialog({feedsId, onClose}: Readonly<{ feedsId:
name: '',
enabled: false,
crawlFullContent: false,
fetchIntervalMinutes: 0,
fetchIntervalMinutes: undefined,
folderId: 0
});
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
Expand Down Expand Up @@ -59,7 +59,10 @@ export default function FeedsFormDialog({feedsId, onClose}: Readonly<{ feedsId:
crawlFullContent: yup.boolean().nullable(),
folderId: yup.number().nullable(),
subscribeUrl: yup.string().required(t('settings:rssLinkRequired')),
fetchIntervalMinutes: yup.number().min(1, t('settings:fetchIntervalMin')).required(t('settings:fetchIntervalRequired'))
fetchIntervalMinutes: yup.number()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app/client/src/components/SettingModal/FeedsFormDialog.tsx:62: fetchIntervalMinutes validation currently allows non-integer values (e.g., 1.5), which may be truncated or rejected when deserializing into a Java Integer on the backend. Consider ensuring minutes are validated as whole numbers consistently for both per-feed and global default intervals.

Other locations where this applies: app/client/src/components/SettingModal/FeedsSetting.tsx:121

Severity: medium

Other Locations
  • app/client/src/components/SettingModal/FeedsSetting.tsx:121

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

.transform((value, originalValue) => originalValue === '' || Number.isNaN(value) ? null : value)
.nullable()
.min(1, t('settings:fetchIntervalMin'))
}),
onSubmit: (values) => {
api.updateFeedsSettingUsingPOST(values).then(() => {
Expand All @@ -75,7 +78,18 @@ export default function FeedsFormDialog({feedsId, onClose}: Readonly<{ feedsId:
});
});
}
})
});

const fetchIntervalHelperText = formikFeeds.touched.fetchIntervalMinutes && formikFeeds.errors.fetchIntervalMinutes
? formikFeeds.errors.fetchIntervalMinutes
: feedsSetting.defaultFetchIntervalMinutes == null
? t('settings:feedFetchIntervalHintGeneric')
: t('settings:feedFetchIntervalHint', {minutes: feedsSetting.defaultFetchIntervalMinutes});

function handleFetchIntervalChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = event.target.value;
formikFeeds.setFieldValue('fetchIntervalMinutes', value === '' ? undefined : Number(value));
}

function handleCloseDeleteDialog() {
setOpenDeleteDialog(false);
Expand Down Expand Up @@ -126,12 +140,18 @@ export default function FeedsFormDialog({feedsId, onClose}: Readonly<{ feedsId:
margin="dense"
id="fetchIntervalMinutes"
label={t('settings:fetchInterval')}
value={formikFeeds.values.fetchIntervalMinutes}
onChange={formikFeeds.handleChange}
value={formikFeeds.values.fetchIntervalMinutes ?? ''}
onChange={handleFetchIntervalChange}
onBlur={formikFeeds.handleBlur}
error={formikFeeds.touched.fetchIntervalMinutes && Boolean(formikFeeds.errors.fetchIntervalMinutes)}
helperText={formikFeeds.touched.fetchIntervalMinutes && formikFeeds.errors.fetchIntervalMinutes}
helperText={fetchIntervalHelperText}
type="number"
variant="standard"
placeholder={feedsSetting.defaultFetchIntervalMinutes?.toString()}
inputProps={{min: 1, step: 1}}
InputProps={{
endAdornment: <InputAdornment position="end">{t('settings:minutesUnit')}</InputAdornment>
}}
/>
<FormControl className={'w-[200px]'} margin={"normal"}>
<InputLabel size={'small'}>{t('settings:folder')}</InputLabel>
Expand Down Expand Up @@ -192,4 +212,4 @@ export default function FeedsFormDialog({feedsId, onClose}: Readonly<{ feedsId:
</DialogActions>
</Dialog>
</React.Fragment>;
}
}
97 changes: 96 additions & 1 deletion app/client/src/components/SettingModal/FeedsSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import UploadFileIcon from '@mui/icons-material/UploadFile';
import DownloadIcon from '@mui/icons-material/Download';
import SettingsIcon from '@mui/icons-material/Settings';
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
import ScheduleIcon from '@mui/icons-material/Schedule';
import { styled } from "@mui/material/styles";
import FolderFormDialog from "./FolderFormDialog";
import FeedsFormDialog from "./FeedsFormDialog";
Expand Down Expand Up @@ -106,6 +107,68 @@ function FeedsTabContent() {
const { enqueueSnackbar } = useSnackbar();
const api = SettingControllerApiFactory();
const { markReadOnScroll, setMarkReadOnScroll } = useGlobalSettings();
const {
data: globalSetting,
refetch: refetchGlobalSetting
} = useQuery(["global-setting-for-feeds"], async () => (await api.getGlobalSettingUsingGET()).data);

const formikDefaultFeedSetting = useFormik({
enableReinitialize: true,
initialValues: {
defaultFeedFetchIntervalMinutes: globalSetting?.defaultFeedFetchIntervalMinutes || 10
},
validationSchema: yup.object({
defaultFeedFetchIntervalMinutes: yup.number()
.typeError(t('settings:defaultFeedFetchIntervalRequired'))
.required(t('settings:defaultFeedFetchIntervalRequired'))
.min(1, t('settings:defaultFeedFetchIntervalMin'))
}),
onSubmit: async (values) => {
if (!globalSetting) {
return;
}

try {
await api.saveGlobalSettingUsingPOST({
...globalSetting,
defaultFeedFetchIntervalMinutes: values.defaultFeedFetchIntervalMinutes
});
await refetchGlobalSetting();
enqueueSnackbar(t('settings:defaultFeedFetchIntervalSaved'), {
variant: "success",
anchorOrigin: { vertical: "bottom", horizontal: "center" }
});
} catch (err) {
enqueueSnackbar(t('settings:defaultFeedFetchIntervalSaveFailed'), {
variant: "error",
anchorOrigin: { vertical: "bottom", horizontal: "center" }
});
}
}
});

async function handleDefaultFeedFetchIntervalBlur(event: React.FocusEvent<HTMLInputElement>) {
formikDefaultFeedSetting.handleBlur(event);

if (!globalSetting || !formikDefaultFeedSetting.dirty) {
return;
}

const errors = await formikDefaultFeedSetting.validateForm();
if (errors.defaultFeedFetchIntervalMinutes) {
return;
}

await formikDefaultFeedSetting.submitForm();
}

function handleDefaultFeedFetchIntervalChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = event.target.value;
formikDefaultFeedSetting.setFieldValue(
'defaultFeedFetchIntervalMinutes',
value === '' ? '' : Number(value)
);
}

async function handleMarkReadOnScrollChange(event: React.ChangeEvent<HTMLInputElement>) {
const newValue = event.target.checked;
Expand Down Expand Up @@ -229,7 +292,39 @@ function FeedsTabContent() {
return (
<div>
<div>
<SettingSectionTitle first icon={RssFeedIcon}>{t('settings:feeds')}</SettingSectionTitle>
<SettingSectionTitle
first
icon={ScheduleIcon}
>
{t('settings:globalSettings')}
</SettingSectionTitle>
<form onSubmit={formikDefaultFeedSetting.handleSubmit}>
<div className="flex flex-wrap items-start gap-3">
<TextField
size={'small'}
margin={'normal'}
id={'defaultFeedFetchIntervalMinutes'}
name={'defaultFeedFetchIntervalMinutes'}
label={t('settings:defaultUpdateInterval')}
value={formikDefaultFeedSetting.values.defaultFeedFetchIntervalMinutes ?? ''}
onChange={handleDefaultFeedFetchIntervalChange}
onBlur={handleDefaultFeedFetchIntervalBlur}
error={formikDefaultFeedSetting.touched.defaultFeedFetchIntervalMinutes && Boolean(formikDefaultFeedSetting.errors.defaultFeedFetchIntervalMinutes)}
helperText={formikDefaultFeedSetting.touched.defaultFeedFetchIntervalMinutes && formikDefaultFeedSetting.errors.defaultFeedFetchIntervalMinutes}
type={'number'}
variant={'outlined'}
inputProps={{ min: 1, step: 1 }}
InputProps={{
endAdornment: <InputAdornment position="end">{t('settings:minutesUnit')}</InputAdornment>,
}}
className="w-full sm:w-[260px]"
disabled={!globalSetting || formikDefaultFeedSetting.isSubmitting}
/>
</div>
</form>
</div>
<div>
<SettingSectionTitle icon={RssFeedIcon}>{t('settings:feeds')}</SettingSectionTitle>
<form onSubmit={formikFeeds.handleSubmit}>
<TextField fullWidth={true} size={'small'} margin={'normal'}
label={t('settings:rssLink')}
Expand Down
9 changes: 9 additions & 0 deletions app/client/src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,17 @@
"feedNameRequired": "Feed name is required.",
"rssLinkRequired": "RSS link is required.",
"invalidUrl": "Must be a valid URL",
"globalSettings": "Global Settings",
"defaultUpdateInterval": "Default update interval",
"defaultFeedFetchIntervalMin": "Default update interval can't be less than 1.",
"defaultFeedFetchIntervalRequired": "Default update interval is required.",
"defaultFeedFetchIntervalSaved": "Default feed update interval saved.",
"defaultFeedFetchIntervalSaveFailed": "Failed to save default feed update interval.",
"fetchIntervalMin": "Fetch interval can't be less than 1.",
"fetchIntervalRequired": "Fetch interval is required.",
"feedFetchIntervalHintGeneric": "Leave blank to use the system default.",
"feedFetchIntervalHint": "Leave blank to use the system default ({{minutes}} min).",
"minutesUnit": "min",
"addFeed": "Add Feed",
"editFeed": "Edit Feed",
"deleteFeed": "Delete Feed",
Expand Down
9 changes: 9 additions & 0 deletions app/client/src/i18n/locales/zh-CN/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,17 @@
"feedNameRequired": "请输入订阅名称。",
"rssLinkRequired": "请输入 RSS 链接。",
"invalidUrl": "请输入有效的 URL",
"globalSettings": "全局设置",
"defaultUpdateInterval": "默认更新间隔",
"defaultFeedFetchIntervalMin": "默认更新间隔不能小于 1。",
"defaultFeedFetchIntervalRequired": "请输入默认更新间隔。",
"defaultFeedFetchIntervalSaved": "默认订阅更新间隔已保存。",
"defaultFeedFetchIntervalSaveFailed": "保存默认订阅更新间隔失败。",
"fetchIntervalMin": "拉取间隔不能小于 1。",
"fetchIntervalRequired": "请输入拉取间隔。",
"feedFetchIntervalHintGeneric": "留空则使用系统默认值。",
"feedFetchIntervalHint": "留空则使用系统默认值({{minutes}} 分钟)。",
"minutesUnit": "分钟",
"addFeed": "添加订阅",
"editFeed": "编辑订阅",
"deleteFeed": "删除订阅",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public class FeedsSetting {

private String subscribeUrl;

private Integer defaultFetchIntervalMinutes;

private Integer fetchIntervalMinutes;

private Boolean enabled;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public class GlobalSetting implements Serializable {
@Column(name = "mark_read_on_scroll")
private Boolean markReadOnScroll;

@Column(name = "default_feed_fetch_interval_minutes")
private Integer defaultFeedFetchIntervalMinutes;

@Column(name = "mcp_token")
private String mcpToken;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ private boolean isAtFetchTime(Connector connector) {
return false;
}
Integer fetchIntervalSeconds = ObjectUtils.defaultIfNull(connector.getFetchIntervalSeconds(),
huntlyProperties.getDefaultFeedFetchIntervalSeconds());
globalSettingService.getDefaultFeedFetchIntervalSeconds());
return connector.getLastFetchBeginAt() == null
|| connector.getLastFetchBeginAt().plusSeconds(fetchIntervalSeconds).isBefore(Instant.now());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public Connector updateFeedsSetting(FeedsSetting feedsSetting) {
connector.setEnabled(feedsSetting.getEnabled());
connector.setSubscribeUrl(feedsSetting.getSubscribeUrl());
connector.setFolderId(feedsSetting.getFolderId() == null || feedsSetting.getFolderId().equals(0) ? null : feedsSetting.getFolderId());
connector.setFetchIntervalSeconds(feedsSetting.getFetchIntervalMinutes() * 60);
connector.setFetchIntervalSeconds(feedsSetting.getFetchIntervalMinutes() == null ? null : feedsSetting.getFetchIntervalMinutes() * 60);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app/server/huntly-server/src/main/java/com/huntly/server/service/FeedsService.java:116: fetchIntervalMinutes is converted to seconds without guarding against <= 0 (or very large) values, which could cause tight fetch loops or integer overflow in fetchIntervalSeconds. Even if the UI validates, API clients can still submit invalid values, so server-side validation would help protect background fetch scheduling.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

var result = connectorRepository.save(connector);
if (!Objects.equals(rawFolderId, connector.getFolderId())) {
pageRepository.updateFolderIdByConnectorId(connector.getId(), connector.getFolderId());
Expand All @@ -136,11 +136,8 @@ public FeedsSetting getFeedsSetting(Integer connectorId) {
feedsSetting.setEnabled(connector.getEnabled());
feedsSetting.setFolderId(connector.getFolderId());
feedsSetting.setSubscribeUrl(connector.getSubscribeUrl());
int fetchIntervalMinutes = huntlyProperties.getDefaultFeedFetchIntervalSeconds() / 60;
if (connector.getFetchIntervalSeconds() != null) {
fetchIntervalMinutes = connector.getFetchIntervalSeconds() / 60;
}
feedsSetting.setFetchIntervalMinutes(fetchIntervalMinutes);
feedsSetting.setDefaultFetchIntervalMinutes(globalSettingService.getDefaultFeedFetchIntervalMinutes());
feedsSetting.setFetchIntervalMinutes(connector.getFetchIntervalSeconds() == null ? null : connector.getFetchIntervalSeconds() / 60);
return feedsSetting;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.huntly.server.domain.constant.AppConstants;
import com.huntly.server.domain.entity.GlobalSetting;
import com.huntly.server.domain.model.ProxySetting;
import com.huntly.server.config.HuntlyProperties;
import com.huntly.server.repository.GlobalSettingRepository;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
Expand All @@ -16,6 +17,7 @@
@Service
public class GlobalSettingService {
private final GlobalSettingRepository settingRepository;
private final HuntlyProperties huntlyProperties;

/**
* Cache for autoSaveTweetMinLikes value to avoid frequent database reads.
Expand All @@ -24,18 +26,24 @@ public class GlobalSettingService {
*/
private final AtomicReference<Integer> cachedAutoSaveTweetMinLikes = new AtomicReference<>(null);

public GlobalSettingService(GlobalSettingRepository settingRepository) {
public GlobalSettingService(GlobalSettingRepository settingRepository, HuntlyProperties huntlyProperties) {
this.settingRepository = settingRepository;
this.huntlyProperties = huntlyProperties;
}

public GlobalSetting getGlobalSetting() {
int defaultFeedFetchIntervalMinutes = Math.max(1, huntlyProperties.getDefaultFeedFetchIntervalSeconds() / 60);
var defaultSetting = new GlobalSetting();
defaultSetting.setColdDataKeepDays(AppConstants.DEFAULT_COLD_DATA_KEEP_DAYS);
defaultSetting.setArticleSummaryPrompt(getDefaultArticleSummaryPrompt());
defaultSetting.setDefaultFeedFetchIntervalMinutes(defaultFeedFetchIntervalMinutes);
var setting = settingRepository.findAll().stream().findFirst().orElse(defaultSetting);
if (setting.getColdDataKeepDays() == null || setting.getColdDataKeepDays() <= 0) {
setting.setColdDataKeepDays(AppConstants.DEFAULT_COLD_DATA_KEEP_DAYS);
}
if (setting.getDefaultFeedFetchIntervalMinutes() == null || setting.getDefaultFeedFetchIntervalMinutes() <= 0) {
setting.setDefaultFeedFetchIntervalMinutes(defaultFeedFetchIntervalMinutes);
}
// API key masking has been moved to controller layer
// set default article summary prompt if not set
if (StringUtils.isBlank(setting.getArticleSummaryPrompt())) {
Expand All @@ -44,6 +52,14 @@ public GlobalSetting getGlobalSetting() {
return setting;
}

public int getDefaultFeedFetchIntervalMinutes() {
return getGlobalSetting().getDefaultFeedFetchIntervalMinutes();
}

public int getDefaultFeedFetchIntervalSeconds() {
return getDefaultFeedFetchIntervalMinutes() * 60;
}

/**
* Get the autoSaveTweetMinLikes setting with caching for performance.
* This method is called frequently from TweetController, so it uses an in-memory cache
Expand Down Expand Up @@ -118,6 +134,7 @@ public GlobalSetting saveGlobalSetting(GlobalSetting globalSetting) {
dbSetting.setOpenApiModel(globalSetting.getOpenApiModel());
dbSetting.setArticleSummaryPrompt(globalSetting.getArticleSummaryPrompt());
dbSetting.setMarkReadOnScroll(globalSetting.getMarkReadOnScroll());
dbSetting.setDefaultFeedFetchIntervalMinutes(globalSetting.getDefaultFeedFetchIntervalMinutes());
if (Boolean.TRUE.equals(globalSetting.getChangedOpenApiKey())) {
dbSetting.setOpenApiKey(globalSetting.getOpenApiKey());
}
Expand Down
Loading
Loading