From 7c0ca44727cfa789a2e7147ae9424a95536f447c Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 24 Apr 2026 16:55:42 -0500 Subject: [PATCH] Update podcast create/update endpoints to validate autoDownloadSchedule cron expression, validate cron expression before starting in CronManager --- client/components/modals/item/tabs/Schedule.vue | 2 ++ server/controllers/LibraryItemController.js | 11 +++++++++++ server/controllers/MiscController.js | 12 +++++------- server/controllers/PodcastController.js | 6 ++++++ server/managers/CronManager.js | 5 +++++ server/models/Podcast.js | 2 ++ 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/client/components/modals/item/tabs/Schedule.vue b/client/components/modals/item/tabs/Schedule.vue index 2cd3af514..0faa29630 100644 --- a/client/components/modals/item/tabs/Schedule.vue +++ b/client/components/modals/item/tabs/Schedule.vue @@ -158,6 +158,8 @@ export default { this.isProcessing = true var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, updatePayload).catch((error) => { console.error('Failed to update', error) + const errorMessage = typeof error?.response?.data === 'string' ? error?.response?.data : null + this.$toast.error(errorMessage || this.$strings.ToastFailedToUpdate) return false }) this.isProcessing = false diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index e524ebcfd..07b4ee67f 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -1,6 +1,7 @@ const { Request, Response, NextFunction } = require('express') const Path = require('path') const fs = require('../libs/fsExtra') +const cron = require('../libs/nodeCron') const uaParserJs = require('../libs/uaParser') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') @@ -220,6 +221,11 @@ class LibraryItemController { } else if (mediaPayload.autoDownloadSchedule !== undefined && req.libraryItem.media.autoDownloadSchedule !== mediaPayload.autoDownloadSchedule) { isPodcastAutoDownloadUpdated = true } + + if (mediaPayload.autoDownloadSchedule && !cron.validate(mediaPayload.autoDownloadSchedule)) { + Logger.error(`[LibraryItemController] Invalid auto download schedule cron expression "${mediaPayload.autoDownloadSchedule}" for library item "${req.libraryItem.media.title}"`) + return res.status(400).send('Invalid auto download schedule cron expression') + } } let hasUpdates = (await req.libraryItem.media.updateFromRequest(mediaPayload)) || mediaPayload.url @@ -659,6 +665,11 @@ class LibraryItemController { const mediaPayload = updatePayload.mediaPayload const libraryItem = libraryItems.find((li) => li.id === updatePayload.id) + if (libraryItem.isPodcast && mediaPayload.autoDownloadSchedule && !cron.validate(mediaPayload.autoDownloadSchedule)) { + Logger.warn(`[LibraryItemController] Invalid auto download schedule cron expression "${mediaPayload.autoDownloadSchedule}" for library item "${libraryItem.media.title}" - skipping update`) + continue + } + let hasUpdates = await libraryItem.media.updateFromRequest(mediaPayload) if (libraryItem.isBook && Array.isArray(mediaPayload.metadata?.series)) { diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 490cb27d2..591f8ccf2 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -8,7 +8,7 @@ const Database = require('../Database') const Watcher = require('../Watcher') const libraryItemFilters = require('../utils/queries/libraryItemFilters') -const patternValidation = require('../libs/nodeCron/pattern-validation') +const cron = require('../libs/nodeCron') const { isObject, getTitleIgnorePrefix } = require('../utils/index') const { sanitizeFilename } = require('../utils/fileUtils') @@ -605,13 +605,11 @@ class MiscController { return res.sendStatus(400) } - try { - patternValidation(expression) - res.sendStatus(200) - } catch (error) { - Logger.warn(`[MiscController] Invalid cron expression ${expression}`, error.message) - res.status(400).send(error.message) + if (!cron.validate(expression)) { + Logger.warn(`[MiscController] Invalid cron expression ${expression}`) + return res.status(400).send('Invalid cron expression') } + res.sendStatus(200) } /** diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index f099d05ed..85173e622 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -5,6 +5,7 @@ const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const fs = require('../libs/fsExtra') +const cron = require('../libs/nodeCron') const { getPodcastFeed, findMatchingEpisodes } = require('../utils/podcastUtils') const { getFileTimestampsWithIno, filePathToPOSIX, isSameOrSubPath } = require('../utils/fileUtils') @@ -46,6 +47,11 @@ class PodcastController { return res.status(400).send('Invalid request body. "media" and "media.metadata" are required') } + if (payload.media.autoDownloadSchedule && !cron.validate(payload.media.autoDownloadSchedule)) { + Logger.error(`[PodcastController] Invalid auto download schedule cron expression "${payload.media.autoDownloadSchedule}"`) + return res.status(400).send('Invalid auto download schedule cron expression') + } + const library = await Database.libraryModel.findByIdWithFolders(payload.libraryId) if (!library) { Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`) diff --git a/server/managers/CronManager.js b/server/managers/CronManager.js index b000413c5..7138d26a8 100644 --- a/server/managers/CronManager.js +++ b/server/managers/CronManager.js @@ -153,6 +153,11 @@ class CronManager { startPodcastCron(expression, libraryItemIds) { try { + if (!cron.validate(expression)) { + Logger.error(`[CronManager] Invalid auto download schedule cron expression "${expression}" - not starting podcast episode check cron`) + return + } + Logger.debug(`[CronManager] Scheduling podcast episode check cron "${expression}" for ${libraryItemIds.length} item(s)`) const task = cron.schedule(expression, () => { if (this.podcastCronExpressionsExecuting.includes(expression)) { diff --git a/server/models/Podcast.js b/server/models/Podcast.js index 02f8981c8..6dbe1cd19 100644 --- a/server/models/Podcast.js +++ b/server/models/Podcast.js @@ -78,6 +78,7 @@ class Podcast extends Model { */ static async createFromRequest(payload, transaction) { const title = typeof payload.metadata.title === 'string' ? payload.metadata.title : null + // cron expression validated in controller const autoDownloadSchedule = typeof payload.autoDownloadSchedule === 'string' ? payload.autoDownloadSchedule : null const genres = Array.isArray(payload.metadata.genres) && payload.metadata.genres.every((g) => typeof g === 'string' && g.length) ? payload.metadata.genres : [] const tags = Array.isArray(payload.tags) && payload.tags.every((t) => typeof t === 'string' && t.length) ? payload.tags : [] @@ -273,6 +274,7 @@ class Podcast extends Model { hasUpdates = true } if (typeof payload.autoDownloadSchedule === 'string' && payload.autoDownloadSchedule !== this.autoDownloadSchedule) { + // cron expression validated in controller this.autoDownloadSchedule = payload.autoDownloadSchedule hasUpdates = true }