From 9b8e059efe68bb21500f2b84de36f54d5750ba97 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sat, 7 Dec 2024 19:27:37 +0200 Subject: [PATCH 1/5] Remove serverAddress from Feeds and FeedEpisodes URLs --- .../modals/rssfeed/OpenCloseModal.vue | 9 +- .../modals/rssfeed/ViewFeedModal.vue | 7 +- client/pages/config/rss-feeds.vue | 2 +- server/Server.js | 4 + server/managers/RssFeedManager.js | 2 +- .../v2.17.5-remove-host-from-feed-urls.js | 74 +++++++ server/objects/Feed.js | 30 +-- server/objects/FeedEpisode.js | 16 +- server/objects/FeedMeta.js | 32 ++- ...v2.17.5-remove-host-from-feed-urls.test.js | 202 ++++++++++++++++++ 10 files changed, 331 insertions(+), 47 deletions(-) create mode 100644 server/migrations/v2.17.5-remove-host-from-feed-urls.js create mode 100644 test/server/migrations/v2.17.5-remove-host-from-feed-urls.test.js diff --git a/client/components/modals/rssfeed/OpenCloseModal.vue b/client/components/modals/rssfeed/OpenCloseModal.vue index 53542cf5..4eff9401 100644 --- a/client/components/modals/rssfeed/OpenCloseModal.vue +++ b/client/components/modals/rssfeed/OpenCloseModal.vue @@ -10,9 +10,9 @@

{{ $strings.HeaderRSSFeedIsOpen }}

- + - content_copy + content_copy
@@ -111,8 +111,11 @@ export default { userIsAdminOrUp() { return this.$store.getters['user/getIsAdminOrUp'] }, + feedUrl() { + return this.currentFeed ? `${window.origin}${this.$config.routerBasePath}${this.currentFeed.feedUrl}` : '' + }, demoFeedUrl() { - return `${window.origin}/feed/${this.newFeedSlug}` + return `${window.origin}${this.$config.routerBasePath}/feed/${this.newFeedSlug}` }, isHttp() { return window.origin.startsWith('http://') diff --git a/client/components/modals/rssfeed/ViewFeedModal.vue b/client/components/modals/rssfeed/ViewFeedModal.vue index cd06350b..70412517 100644 --- a/client/components/modals/rssfeed/ViewFeedModal.vue +++ b/client/components/modals/rssfeed/ViewFeedModal.vue @@ -5,8 +5,8 @@

{{ $strings.HeaderRSSFeedGeneral }}

- - content_copy + + content_copy
@@ -70,6 +70,9 @@ export default { }, _feed() { return this.feed || {} + }, + feedUrl() { + return this.feed ? `${window.origin}${this.$config.routerBasePath}${this.feed.feedUrl}` : '' } }, methods: { diff --git a/client/pages/config/rss-feeds.vue b/client/pages/config/rss-feeds.vue index 68117a85..039e9a0d 100644 --- a/client/pages/config/rss-feeds.vue +++ b/client/pages/config/rss-feeds.vue @@ -126,7 +126,7 @@ export default { }, coverUrl(feed) { if (!feed.coverPath) return `${this.$config.routerBasePath}/Logo.png` - return `${feed.feedUrl}/cover` + return `${this.$config.routerBasePath}${feed.feedUrl}/cover` }, async loadFeeds() { const data = await this.$axios.$get(`/api/feeds`).catch((err) => { diff --git a/server/Server.js b/server/Server.js index cd96733e..dfcb474a 100644 --- a/server/Server.js +++ b/server/Server.js @@ -253,6 +253,10 @@ class Server { // if RouterBasePath is set, modify all requests to include the base path if (global.RouterBasePath) { app.use((req, res, next) => { + const host = req.get('host') + const protocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http' + const prefix = req.url.startsWith(global.RouterBasePath) ? global.RouterBasePath : '' + req.originalHostPrefix = `${protocol}://${host}${prefix}` if (!req.url.startsWith(global.RouterBasePath)) { req.url = `${global.RouterBasePath}${req.url}` } diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index 7716440d..8984a39b 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -162,7 +162,7 @@ class RssFeedManager { } } - const xml = feed.buildXml() + const xml = feed.buildXml(req.originalHostPrefix) res.set('Content-Type', 'text/xml') res.send(xml) } diff --git a/server/migrations/v2.17.5-remove-host-from-feed-urls.js b/server/migrations/v2.17.5-remove-host-from-feed-urls.js new file mode 100644 index 00000000..e08877f2 --- /dev/null +++ b/server/migrations/v2.17.5-remove-host-from-feed-urls.js @@ -0,0 +1,74 @@ +/** + * @typedef MigrationContext + * @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object. + * @property {import('../Logger')} logger - a Logger object. + * + * @typedef MigrationOptions + * @property {MigrationContext} context - an object containing the migration context. + */ + +const migrationVersion = '2.17.5' +const migrationName = `${migrationVersion}-remove-host-from-feed-urls` +const loggerPrefix = `[${migrationVersion} migration]` + +/** + * This upward migration removes the host (serverAddress) from URL columns in the feeds and feedEpisodes tables. + * + * @param {MigrationOptions} options - an object containing the migration context. + * @returns {Promise} - A promise that resolves when the migration is complete. + */ +async function up({ context: { queryInterface, logger } }) { + // Upwards migration script + logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`) + + logger.info(`${loggerPrefix} Removing serverAddress from Feeds table URLs`) + await queryInterface.sequelize.query(` + UPDATE Feeds + SET feedUrl = REPLACE(feedUrl, COALESCE(serverAddress, ''), ''), + imageUrl = REPLACE(imageUrl, COALESCE(serverAddress, ''), ''), + siteUrl = REPLACE(siteUrl, COALESCE(serverAddress, ''), ''); + `) + logger.info(`${loggerPrefix} Removed serverAddress from Feeds table URLs`) + + logger.info(`${loggerPrefix} Removing serverAddress from FeedEpisodes table URLs`) + await queryInterface.sequelize.query(` + UPDATE FeedEpisodes + SET siteUrl = REPLACE(siteUrl, (SELECT COALESCE(serverAddress, '') FROM Feeds WHERE Feeds.id = FeedEpisodes.feedId), ''), + enclosureUrl = REPLACE(enclosureUrl, (SELECT COALESCE(serverAddress, '') FROM Feeds WHERE Feeds.id = FeedEpisodes.feedId), ''); + `) + logger.info(`${loggerPrefix} Removed serverAddress from FeedEpisodes table URLs`) + + logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`) +} + +/** + * This downward migration script adds the host (serverAddress) back to URL columns in the feeds and feedEpisodes tables. + * + * @param {MigrationOptions} options - an object containing the migration context. + * @returns {Promise} - A promise that resolves when the migration is complete. + */ +async function down({ context: { queryInterface, logger } }) { + // Downward migration script + logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`) + + logger.info(`${loggerPrefix} Adding serverAddress back to Feeds table URLs`) + await queryInterface.sequelize.query(` + UPDATE Feeds + SET feedUrl = COALESCE(serverAddress, '') || feedUrl, + imageUrl = COALESCE(serverAddress, '') || imageUrl, + siteUrl = COALESCE(serverAddress, '') || siteUrl; + `) + logger.info(`${loggerPrefix} Added serverAddress back to Feeds table URLs`) + + logger.info(`${loggerPrefix} Adding serverAddress back to FeedEpisodes table URLs`) + await queryInterface.sequelize.query(` + UPDATE FeedEpisodes + SET siteUrl = (SELECT COALESCE(serverAddress, '') || FeedEpisodes.siteUrl FROM Feeds WHERE Feeds.id = FeedEpisodes.feedId), + enclosureUrl = (SELECT COALESCE(serverAddress, '') || FeedEpisodes.enclosureUrl FROM Feeds WHERE Feeds.id = FeedEpisodes.feedId); + `) + logger.info(`${loggerPrefix} Added serverAddress back to FeedEpisodes table URLs`) + + logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`) +} + +module.exports = { up, down } diff --git a/server/objects/Feed.js b/server/objects/Feed.js index 74a220e3..da76067d 100644 --- a/server/objects/Feed.js +++ b/server/objects/Feed.js @@ -109,7 +109,7 @@ class Feed { const mediaMetadata = media.metadata const isPodcast = libraryItem.mediaType === 'podcast' - const feedUrl = `${serverAddress}/feed/${slug}` + const feedUrl = `/feed/${slug}` const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName this.id = uuidv4() @@ -128,9 +128,9 @@ class Feed { this.meta.title = mediaMetadata.title this.meta.description = mediaMetadata.description this.meta.author = author - this.meta.imageUrl = media.coverPath ? `${serverAddress}/feed/${slug}/cover${coverFileExtension}` : `${serverAddress}/Logo.png` + this.meta.imageUrl = media.coverPath ? `/feed/${slug}/cover${coverFileExtension}` : `/Logo.png` this.meta.feedUrl = feedUrl - this.meta.link = `${serverAddress}/item/${libraryItem.id}` + this.meta.link = `/item/${libraryItem.id}` this.meta.explicit = !!mediaMetadata.explicit this.meta.type = mediaMetadata.type this.meta.language = mediaMetadata.language @@ -176,7 +176,7 @@ class Feed { this.meta.title = mediaMetadata.title this.meta.description = mediaMetadata.description this.meta.author = author - this.meta.imageUrl = media.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover${coverFileExtension}` : `${this.serverAddress}/Logo.png` + this.meta.imageUrl = media.coverPath ? `/feed/${this.slug}/cover${coverFileExtension}` : `/Logo.png` this.meta.explicit = !!mediaMetadata.explicit this.meta.type = mediaMetadata.type this.meta.language = mediaMetadata.language @@ -206,7 +206,7 @@ class Feed { } setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) { - const feedUrl = `${serverAddress}/feed/${slug}` + const feedUrl = `/feed/${slug}` const itemsWithTracks = collectionExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length) const firstItemWithCover = itemsWithTracks.find((item) => item.media.coverPath) @@ -227,9 +227,9 @@ class Feed { this.meta.title = collectionExpanded.name this.meta.description = collectionExpanded.description || '' this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks) - this.meta.imageUrl = this.coverPath ? `${serverAddress}/feed/${slug}/cover${coverFileExtension}` : `${serverAddress}/Logo.png` + this.meta.imageUrl = this.coverPath ? `/feed/${slug}/cover${coverFileExtension}` : `/Logo.png` this.meta.feedUrl = feedUrl - this.meta.link = `${serverAddress}/collection/${collectionExpanded.id}` + this.meta.link = `/collection/${collectionExpanded.id}` this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit this.meta.preventIndexing = preventIndexing this.meta.ownerName = ownerName @@ -272,7 +272,7 @@ class Feed { this.meta.title = collectionExpanded.name this.meta.description = collectionExpanded.description || '' this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks) - this.meta.imageUrl = this.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover${coverFileExtension}` : `${this.serverAddress}/Logo.png` + this.meta.imageUrl = this.coverPath ? `/feed/${this.slug}/cover${coverFileExtension}` : `/Logo.png` this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit this.episodes = [] @@ -301,7 +301,7 @@ class Feed { } setFromSeries(userId, slug, seriesExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) { - const feedUrl = `${serverAddress}/feed/${slug}` + const feedUrl = `/feed/${slug}` let itemsWithTracks = seriesExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length) // Sort series items by series sequence @@ -326,9 +326,9 @@ class Feed { this.meta.title = seriesExpanded.name this.meta.description = seriesExpanded.description || '' this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks) - this.meta.imageUrl = this.coverPath ? `${serverAddress}/feed/${slug}/cover${coverFileExtension}` : `${serverAddress}/Logo.png` + this.meta.imageUrl = this.coverPath ? `/feed/${slug}/cover${coverFileExtension}` : `/Logo.png` this.meta.feedUrl = feedUrl - this.meta.link = `${serverAddress}/library/${libraryId}/series/${seriesExpanded.id}` + this.meta.link = `/library/${libraryId}/series/${seriesExpanded.id}` this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit this.meta.preventIndexing = preventIndexing this.meta.ownerName = ownerName @@ -374,7 +374,7 @@ class Feed { this.meta.title = seriesExpanded.name this.meta.description = seriesExpanded.description || '' this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks) - this.meta.imageUrl = this.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover${coverFileExtension}` : `${this.serverAddress}/Logo.png` + this.meta.imageUrl = this.coverPath ? `/feed/${this.slug}/cover${coverFileExtension}` : `/Logo.png` this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit this.episodes = [] @@ -402,12 +402,12 @@ class Feed { this.xml = null } - buildXml() { + buildXml(originalHostPrefix) { if (this.xml) return this.xml - var rssfeed = new RSS(this.meta.getRSSData()) + var rssfeed = new RSS(this.meta.getRSSData(originalHostPrefix)) this.episodes.forEach((ep) => { - rssfeed.item(ep.getRSSData()) + rssfeed.item(ep.getRSSData(originalHostPrefix)) }) this.xml = rssfeed.xml() return this.xml diff --git a/server/objects/FeedEpisode.js b/server/objects/FeedEpisode.js index 6d9f36a0..13d590ff 100644 --- a/server/objects/FeedEpisode.js +++ b/server/objects/FeedEpisode.js @@ -79,7 +79,7 @@ class FeedEpisode { this.title = episode.title this.description = episode.description || '' this.enclosure = { - url: `${serverAddress}${contentUrl}`, + url: `${contentUrl}`, type: episode.audioTrack.mimeType, size: episode.size } @@ -136,7 +136,7 @@ class FeedEpisode { this.title = title this.description = mediaMetadata.description || '' this.enclosure = { - url: `${serverAddress}${contentUrl}`, + url: `${contentUrl}`, type: audioTrack.mimeType, size: audioTrack.metadata.size } @@ -151,15 +151,19 @@ class FeedEpisode { this.fullPath = audioTrack.metadata.path } - getRSSData() { + getRSSData(hostPrefix) { return { title: this.title, description: this.description || '', - url: this.link, - guid: this.enclosure.url, + url: `${hostPrefix}${this.link}`, + guid: `${hostPrefix}${this.enclosure.url}`, author: this.author, date: this.pubDate, - enclosure: this.enclosure, + enclosure: { + url: `${hostPrefix}${this.enclosure.url}`, + type: this.enclosure.type, + size: this.enclosure.size + }, custom_elements: [ { 'itunes:author': this.author }, { 'itunes:duration': secondsToTimestamp(this.duration) }, diff --git a/server/objects/FeedMeta.js b/server/objects/FeedMeta.js index 307e12bc..e439fe8f 100644 --- a/server/objects/FeedMeta.js +++ b/server/objects/FeedMeta.js @@ -60,42 +60,36 @@ class FeedMeta { } } - getRSSData() { - const blockTags = [ - { 'itunes:block': 'yes' }, - { 'googleplay:block': 'yes' } - ] + getRSSData(hostPrefix) { + const blockTags = [{ 'itunes:block': 'yes' }, { 'googleplay:block': 'yes' }] return { title: this.title, description: this.description || '', generator: 'Audiobookshelf', - feed_url: this.feedUrl, - site_url: this.link, - image_url: this.imageUrl, + feed_url: `${hostPrefix}${this.feedUrl}`, + site_url: `${hostPrefix}${this.link}`, + image_url: `${hostPrefix}${this.imageUrl}`, custom_namespaces: { - 'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd', - 'psc': 'http://podlove.org/simple-chapters', - 'podcast': 'https://podcastindex.org/namespace/1.0', - 'googleplay': 'http://www.google.com/schemas/play-podcasts/1.0' + itunes: 'http://www.itunes.com/dtds/podcast-1.0.dtd', + psc: 'http://podlove.org/simple-chapters', + podcast: 'https://podcastindex.org/namespace/1.0', + googleplay: 'http://www.google.com/schemas/play-podcasts/1.0' }, custom_elements: [ - { 'language': this.language || 'en' }, - { 'author': this.author || 'advplyr' }, + { language: this.language || 'en' }, + { author: this.author || 'advplyr' }, { 'itunes:author': this.author || 'advplyr' }, { 'itunes:summary': this.description || '' }, { 'itunes:type': this.type }, { 'itunes:image': { _attr: { - href: this.imageUrl + href: `${hostPrefix}${this.imageUrl}` } } }, { - 'itunes:owner': [ - { 'itunes:name': this.ownerName || this.author || '' }, - { 'itunes:email': this.ownerEmail || '' } - ] + 'itunes:owner': [{ 'itunes:name': this.ownerName || this.author || '' }, { 'itunes:email': this.ownerEmail || '' }] }, { 'itunes:explicit': !!this.explicit }, ...(this.preventIndexing ? blockTags : []) diff --git a/test/server/migrations/v2.17.5-remove-host-from-feed-urls.test.js b/test/server/migrations/v2.17.5-remove-host-from-feed-urls.test.js new file mode 100644 index 00000000..786ed6ae --- /dev/null +++ b/test/server/migrations/v2.17.5-remove-host-from-feed-urls.test.js @@ -0,0 +1,202 @@ +const { expect } = require('chai') +const sinon = require('sinon') +const { up, down } = require('../../../server/migrations/v2.17.5-remove-host-from-feed-urls') +const { Sequelize, DataTypes } = require('sequelize') +const Logger = require('../../../server/Logger') + +const defineModels = (sequelize) => { + const Feeds = sequelize.define('Feeds', { + id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 }, + feedUrl: { type: DataTypes.STRING }, + imageUrl: { type: DataTypes.STRING }, + siteUrl: { type: DataTypes.STRING }, + serverAddress: { type: DataTypes.STRING } + }) + + const FeedEpisodes = sequelize.define('FeedEpisodes', { + id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 }, + feedId: { type: DataTypes.UUID }, + siteUrl: { type: DataTypes.STRING }, + enclosureUrl: { type: DataTypes.STRING } + }) + + return { Feeds, FeedEpisodes } +} + +describe('Migration v2.17.4-use-subfolder-for-oidc-redirect-uris', () => { + let queryInterface, logger, context + let sequelize + let Feeds, FeedEpisodes + const feed1Id = '00000000-0000-4000-a000-000000000001' + const feed2Id = '00000000-0000-4000-a000-000000000002' + const feedEpisode1Id = '00000000-4000-a000-0000-000000000011' + const feedEpisode2Id = '00000000-4000-a000-0000-000000000012' + const feedEpisode3Id = '00000000-4000-a000-0000-000000000021' + + before(async () => { + sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) + queryInterface = sequelize.getQueryInterface() + ;({ Feeds, FeedEpisodes } = defineModels(sequelize)) + await sequelize.sync() + }) + + after(async () => { + await sequelize.close() + }) + + beforeEach(async () => { + // Reset tables before each test + await Feeds.destroy({ where: {}, truncate: true }) + await FeedEpisodes.destroy({ where: {}, truncate: true }) + + logger = { + info: sinon.stub(), + error: sinon.stub() + } + context = { queryInterface, logger } + }) + + describe('up', () => { + it('should remove serverAddress from URLs in Feeds and FeedEpisodes tables', async () => { + await Feeds.bulkCreate([ + { id: feed1Id, feedUrl: 'http://server1.com/feed1', imageUrl: 'http://server1.com/img1', siteUrl: 'http://server1.com/site1', serverAddress: 'http://server1.com' }, + { id: feed2Id, feedUrl: 'http://server2.com/feed2', imageUrl: 'http://server2.com/img2', siteUrl: 'http://server2.com/site2', serverAddress: 'http://server2.com' } + ]) + + await FeedEpisodes.bulkCreate([ + { id: feedEpisode1Id, feedId: feed1Id, siteUrl: 'http://server1.com/episode11', enclosureUrl: 'http://server1.com/enclosure11' }, + { id: feedEpisode2Id, feedId: feed1Id, siteUrl: 'http://server1.com/episode12', enclosureUrl: 'http://server1.com/enclosure12' }, + { id: feedEpisode3Id, feedId: feed2Id, siteUrl: 'http://server2.com/episode21', enclosureUrl: 'http://server2.com/enclosure21' } + ]) + + await up({ context }) + const feeds = await Feeds.findAll({ raw: true }) + const feedEpisodes = await FeedEpisodes.findAll({ raw: true }) + + expect(logger.info.calledWith('[2.17.5 migration] UPGRADE BEGIN: 2.17.5-remove-host-from-feed-urls')).to.be.true + expect(logger.info.calledWith('[2.17.5 migration] Removing serverAddress from Feeds table URLs')).to.be.true + + expect(feeds[0].feedUrl).to.equal('/feed1') + expect(feeds[0].imageUrl).to.equal('/img1') + expect(feeds[0].siteUrl).to.equal('/site1') + expect(feeds[1].feedUrl).to.equal('/feed2') + expect(feeds[1].imageUrl).to.equal('/img2') + expect(feeds[1].siteUrl).to.equal('/site2') + + expect(logger.info.calledWith('[2.17.5 migration] Removed serverAddress from Feeds table URLs')).to.be.true + expect(logger.info.calledWith('[2.17.5 migration] Removing serverAddress from FeedEpisodes table URLs')).to.be.true + + expect(feedEpisodes[0].siteUrl).to.equal('/episode11') + expect(feedEpisodes[0].enclosureUrl).to.equal('/enclosure11') + expect(feedEpisodes[1].siteUrl).to.equal('/episode12') + expect(feedEpisodes[1].enclosureUrl).to.equal('/enclosure12') + expect(feedEpisodes[2].siteUrl).to.equal('/episode21') + expect(feedEpisodes[2].enclosureUrl).to.equal('/enclosure21') + + expect(logger.info.calledWith('[2.17.5 migration] Removed serverAddress from FeedEpisodes table URLs')).to.be.true + expect(logger.info.calledWith('[2.17.5 migration] UPGRADE END: 2.17.5-remove-host-from-feed-urls')).to.be.true + }) + + it('should handle null URLs in Feeds and FeedEpisodes tables', async () => { + await Feeds.bulkCreate([{ id: feed1Id, feedUrl: 'http://server1.com/feed1', imageUrl: null, siteUrl: 'http://server1.com/site1', serverAddress: 'http://server1.com' }]) + + await FeedEpisodes.bulkCreate([{ id: feedEpisode1Id, feedId: feed1Id, siteUrl: null, enclosureUrl: 'http://server1.com/enclosure11' }]) + + await up({ context }) + const feeds = await Feeds.findAll({ raw: true }) + const feedEpisodes = await FeedEpisodes.findAll({ raw: true }) + + expect(feeds[0].feedUrl).to.equal('/feed1') + expect(feeds[0].imageUrl).to.be.null + expect(feeds[0].siteUrl).to.equal('/site1') + expect(feedEpisodes[0].siteUrl).to.be.null + expect(feedEpisodes[0].enclosureUrl).to.equal('/enclosure11') + }) + + it('should handle null serverAddress in Feeds table', async () => { + await Feeds.bulkCreate([{ id: feed1Id, feedUrl: 'http://server1.com/feed1', imageUrl: 'http://server1.com/img1', siteUrl: 'http://server1.com/site1', serverAddress: null }]) + await FeedEpisodes.bulkCreate([{ id: feedEpisode1Id, feedId: feed1Id, siteUrl: 'http://server1.com/episode11', enclosureUrl: 'http://server1.com/enclosure11' }]) + + await up({ context }) + const feeds = await Feeds.findAll({ raw: true }) + const feedEpisodes = await FeedEpisodes.findAll({ raw: true }) + + expect(feeds[0].feedUrl).to.equal('http://server1.com/feed1') + expect(feeds[0].imageUrl).to.equal('http://server1.com/img1') + expect(feeds[0].siteUrl).to.equal('http://server1.com/site1') + expect(feedEpisodes[0].siteUrl).to.equal('http://server1.com/episode11') + expect(feedEpisodes[0].enclosureUrl).to.equal('http://server1.com/enclosure11') + }) + }) + + describe('down', () => { + it('should add serverAddress back to URLs in Feeds and FeedEpisodes tables', async () => { + await Feeds.bulkCreate([ + { id: feed1Id, feedUrl: '/feed1', imageUrl: '/img1', siteUrl: '/site1', serverAddress: 'http://server1.com' }, + { id: feed2Id, feedUrl: '/feed2', imageUrl: '/img2', siteUrl: '/site2', serverAddress: 'http://server2.com' } + ]) + + await FeedEpisodes.bulkCreate([ + { id: feedEpisode1Id, feedId: feed1Id, siteUrl: '/episode11', enclosureUrl: '/enclosure11' }, + { id: feedEpisode2Id, feedId: feed1Id, siteUrl: '/episode12', enclosureUrl: '/enclosure12' }, + { id: feedEpisode3Id, feedId: feed2Id, siteUrl: '/episode21', enclosureUrl: '/enclosure21' } + ]) + + await down({ context }) + const feeds = await Feeds.findAll({ raw: true }) + const feedEpisodes = await FeedEpisodes.findAll({ raw: true }) + + expect(logger.info.calledWith('[2.17.5 migration] DOWNGRADE BEGIN: 2.17.5-remove-host-from-feed-urls')).to.be.true + expect(logger.info.calledWith('[2.17.5 migration] Adding serverAddress back to Feeds table URLs')).to.be.true + + expect(feeds[0].feedUrl).to.equal('http://server1.com/feed1') + expect(feeds[0].imageUrl).to.equal('http://server1.com/img1') + expect(feeds[0].siteUrl).to.equal('http://server1.com/site1') + expect(feeds[1].feedUrl).to.equal('http://server2.com/feed2') + expect(feeds[1].imageUrl).to.equal('http://server2.com/img2') + expect(feeds[1].siteUrl).to.equal('http://server2.com/site2') + + expect(logger.info.calledWith('[2.17.5 migration] Added serverAddress back to Feeds table URLs')).to.be.true + expect(logger.info.calledWith('[2.17.5 migration] Adding serverAddress back to FeedEpisodes table URLs')).to.be.true + + expect(feedEpisodes[0].siteUrl).to.equal('http://server1.com/episode11') + expect(feedEpisodes[0].enclosureUrl).to.equal('http://server1.com/enclosure11') + expect(feedEpisodes[1].siteUrl).to.equal('http://server1.com/episode12') + expect(feedEpisodes[1].enclosureUrl).to.equal('http://server1.com/enclosure12') + expect(feedEpisodes[2].siteUrl).to.equal('http://server2.com/episode21') + expect(feedEpisodes[2].enclosureUrl).to.equal('http://server2.com/enclosure21') + + expect(logger.info.calledWith('[2.17.5 migration] DOWNGRADE END: 2.17.5-remove-host-from-feed-urls')).to.be.true + }) + + it('should handle null URLs in Feeds and FeedEpisodes tables', async () => { + await Feeds.bulkCreate([{ id: feed1Id, feedUrl: '/feed1', imageUrl: null, siteUrl: '/site1', serverAddress: 'http://server1.com' }]) + await FeedEpisodes.bulkCreate([{ id: feedEpisode1Id, feedId: feed1Id, siteUrl: null, enclosureUrl: '/enclosure11' }]) + + await down({ context }) + const feeds = await Feeds.findAll({ raw: true }) + const feedEpisodes = await FeedEpisodes.findAll({ raw: true }) + + expect(feeds[0].feedUrl).to.equal('http://server1.com/feed1') + expect(feeds[0].imageUrl).to.be.null + expect(feeds[0].siteUrl).to.equal('http://server1.com/site1') + expect(feedEpisodes[0].siteUrl).to.be.null + expect(feedEpisodes[0].enclosureUrl).to.equal('http://server1.com/enclosure11') + }) + + it('should handle null serverAddress in Feeds table', async () => { + await Feeds.bulkCreate([{ id: feed1Id, feedUrl: '/feed1', imageUrl: '/img1', siteUrl: '/site1', serverAddress: null }]) + await FeedEpisodes.bulkCreate([{ id: feedEpisode1Id, feedId: feed1Id, siteUrl: '/episode11', enclosureUrl: '/enclosure11' }]) + + await down({ context }) + const feeds = await Feeds.findAll({ raw: true }) + const feedEpisodes = await FeedEpisodes.findAll({ raw: true }) + + expect(feeds[0].feedUrl).to.equal('/feed1') + expect(feeds[0].imageUrl).to.equal('/img1') + expect(feeds[0].siteUrl).to.equal('/site1') + expect(feedEpisodes[0].siteUrl).to.equal('/episode11') + expect(feedEpisodes[0].enclosureUrl).to.equal('/enclosure11') + }) + }) +}) From a8ab8badd5c42e1794715a370b6a8ae60c6b8652 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 8 Dec 2024 09:23:39 +0200 Subject: [PATCH 2/5] always set req.originalHostPrefix --- server/Server.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/server/Server.js b/server/Server.js index dfcb474a..79598275 100644 --- a/server/Server.js +++ b/server/Server.js @@ -251,18 +251,17 @@ class Server { const router = express.Router() // if RouterBasePath is set, modify all requests to include the base path - if (global.RouterBasePath) { - app.use((req, res, next) => { - const host = req.get('host') - const protocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http' - const prefix = req.url.startsWith(global.RouterBasePath) ? global.RouterBasePath : '' - req.originalHostPrefix = `${protocol}://${host}${prefix}` - if (!req.url.startsWith(global.RouterBasePath)) { - req.url = `${global.RouterBasePath}${req.url}` - } - next() - }) - } + app.use((req, res, next) => { + const urlStartsWithRouterBasePath = req.url.startsWith(global.RouterBasePath) + const host = req.get('host') + const protocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http' + const prefix = urlStartsWithRouterBasePath ? global.RouterBasePath : '' + req.originalHostPrefix = `${protocol}://${host}${prefix}` + if (!urlStartsWithRouterBasePath) { + req.url = `${global.RouterBasePath}${req.url}` + } + next() + }) app.use(global.RouterBasePath, router) app.disable('x-powered-by') From b38ce4173144a9d33330ac3b59fbf7faf8320292 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 8 Dec 2024 09:48:58 +0200 Subject: [PATCH 3/5] Remove xml cache from Feed object --- server/objects/Feed.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/server/objects/Feed.js b/server/objects/Feed.js index da76067d..ac50b899 100644 --- a/server/objects/Feed.js +++ b/server/objects/Feed.js @@ -29,9 +29,6 @@ class Feed { this.createdAt = null this.updatedAt = null - // Cached xml - this.xml = null - if (feed) { this.construct(feed) } @@ -202,7 +199,6 @@ class Feed { } this.updatedAt = Date.now() - this.xml = null } setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) { @@ -297,7 +293,6 @@ class Feed { }) this.updatedAt = Date.now() - this.xml = null } setFromSeries(userId, slug, seriesExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) { @@ -399,18 +394,14 @@ class Feed { }) this.updatedAt = Date.now() - this.xml = null } buildXml(originalHostPrefix) { - if (this.xml) return this.xml - var rssfeed = new RSS(this.meta.getRSSData(originalHostPrefix)) this.episodes.forEach((ep) => { rssfeed.item(ep.getRSSData(originalHostPrefix)) }) - this.xml = rssfeed.xml() - return this.xml + return rssfeed.xml() } getAuthorsStringFromLibraryItems(libraryItems) { From 5646466aa371cc03f12496cd0a1d28de34839734 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 8 Dec 2024 08:05:33 -0600 Subject: [PATCH 4/5] Update JSDocs for feeds endpoints --- server/managers/RssFeedManager.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index 8984a39b..583f0bb6 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -1,3 +1,4 @@ +const { Request, Response } = require('express') const Path = require('path') const Logger = require('../Logger') @@ -77,6 +78,12 @@ class RssFeedManager { return Database.feedModel.findByPkOld(id) } + /** + * GET: /feed/:slug + * + * @param {Request} req + * @param {Response} res + */ async getFeed(req, res) { const feed = await this.findFeedBySlug(req.params.slug) if (!feed) { @@ -167,6 +174,12 @@ class RssFeedManager { res.send(xml) } + /** + * GET: /feed/:slug/item/:episodeId/* + * + * @param {Request} req + * @param {Response} res + */ async getFeedItem(req, res) { const feed = await this.findFeedBySlug(req.params.slug) if (!feed) { @@ -183,6 +196,12 @@ class RssFeedManager { res.sendFile(episodePath) } + /** + * GET: /feed/:slug/cover* + * + * @param {Request} req + * @param {Response} res + */ async getFeedCover(req, res) { const feed = await this.findFeedBySlug(req.params.slug) if (!feed) { From f7b7b85673fb8a5ac1a9b9c09e1bb686aa7d2f90 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 8 Dec 2024 08:19:23 -0600 Subject: [PATCH 5/5] Add v2.17.5 migration to changelog --- server/migrations/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/server/migrations/changelog.md b/server/migrations/changelog.md index f46cd4ae..f4992432 100644 --- a/server/migrations/changelog.md +++ b/server/migrations/changelog.md @@ -10,3 +10,4 @@ Please add a record of every database migration that you create to this file. Th | v2.17.0 | v2.17.0-uuid-replacement | Changes the data type of columns with UUIDv4 to UUID matching the associated model | | v2.17.3 | v2.17.3-fk-constraints | Changes the foreign key constraints for tables due to sequelize bug dropping constraints in v2.17.0 migration | | v2.17.4 | v2.17.4-use-subfolder-for-oidc-redirect-uris | Save subfolder to OIDC redirect URIs to support existing installations | +| v2.17.5 | v2.17.5-remove-host-from-feed-urls | removes the host (serverAddress) from URL columns in the feeds and feedEpisodes tables |