diff --git a/ghost/core/core/server/services/mentions/ResourceService.js b/ghost/core/core/server/services/mentions/ResourceService.js new file mode 100644 index 0000000000..2d2a5a467e --- /dev/null +++ b/ghost/core/core/server/services/mentions/ResourceService.js @@ -0,0 +1,45 @@ +const ObjectID = require('bson-objectid').default; + +/** + * @typedef {import('@tryghost/webmentions/lib/MentionsAPI').IResourceService} IResourceService + */ + +/** + * @implements {IResourceService} + */ +module.exports = class ResourceService { + /** @type {import('@tryghost/url-utils/lib/url-utils')} */ + #urlUtils; + + /** @type {import('../url')} */ + #urlService; + + /** + * @param {object} deps + * @param {import('@tryghost/url-utils/lib/url-utils')} deps.urlUtils + * @param {import('../url')} deps.urlService + */ + constructor(deps) { + this.#urlUtils = deps.urlUtils; + this.#urlService = deps.urlService; + } + + /** + * @param {URL} url + * @returns {Promise} + */ + async getByURL(url) { + const path = this.#urlUtils.absoluteToRelative(url.href, {withoutSubdirectory: true}); + const resource = this.#urlService.getResource(path); + if (resource?.config?.type === 'posts') { + return { + type: 'post', + id: ObjectID.createFromHexString(resource.data.id) + }; + } + return { + type: null, + id: null + }; + } +}; diff --git a/ghost/core/core/server/services/mentions/service.js b/ghost/core/core/server/services/mentions/service.js index 58e2d2d2ee..cd6e4af202 100644 --- a/ghost/core/core/server/services/mentions/service.js +++ b/ghost/core/core/server/services/mentions/service.js @@ -1,4 +1,3 @@ -const ObjectID = require('bson-objectid').default; const MentionController = require('./MentionController'); const WebmentionMetadata = require('./WebmentionMetadata'); const { @@ -7,6 +6,7 @@ const { MentionDiscoveryService } = require('@tryghost/webmentions'); const BookshelfMentionRepository = require('./BookshelfMentionRepository'); +const ResourceService = require('./ResourceService'); const RoutingService = require('./RoutingService'); const models = require('../../models'); @@ -30,6 +30,10 @@ module.exports = { }); const webmentionMetadata = new WebmentionMetadata(); const discoveryService = new MentionDiscoveryService({externalRequest}); + const resourceService = new ResourceService({ + urlUtils, + urlService + }); const routingService = new RoutingService({ siteUrl: new URL(urlUtils.getSiteUrl()) }); @@ -37,22 +41,7 @@ module.exports = { const api = new MentionsAPI({ repository, webmentionMetadata, - resourceService: { - async getByURL(url) { - const path = urlUtils.absoluteToRelative(url.href, {withoutSubdirectory: true}); - const resource = urlService.getResource(path); - if (resource?.config?.type === 'posts') { - return { - type: 'post', - id: ObjectID.createFromHexString(resource.data.id) - }; - } - return { - type: null, - id: null - }; - } - }, + resourceService, routingService }); diff --git a/ghost/core/test/unit/server/services/mentions/ResourceService.test.js b/ghost/core/test/unit/server/services/mentions/ResourceService.test.js new file mode 100644 index 0000000000..9c8ce64f4d --- /dev/null +++ b/ghost/core/test/unit/server/services/mentions/ResourceService.test.js @@ -0,0 +1,128 @@ +const assert = require('assert'); +const sinon = require('sinon'); +const ResourceService = require('../../../../../core/server/services/mentions/ResourceService'); +const UrlUtils = require('@tryghost/url-utils'); +const UrlService = require('../../../../../core/server/services/url/UrlService'); + +function stubGetResource(urlService) { + const getResource = sinon.stub(urlService, 'getResource'); + + getResource.withArgs('/post-resource').returns({ + config: { + type: 'posts' + }, + data: { + id: '63ce473f992390b739b00b01' + } + }); + + getResource.withArgs('/tag-resource').returns({ + config: { + type: 'tags' + }, + data: { + id: '63ce473f992390b739b00b02' + } + }); + + getResource.withArgs('/no-resource').returns(null); + + return getResource; +} + +describe('ResourceService', function () { + describe('getByURL', function () { + it('Correctly converts post resources', async function () { + const urlUtils = new UrlUtils({ + getSiteUrl() { + return 'https://site.com/blah/'; + }, + getSubdir() { + return '/blah'; + }, + getAdminUrl() { + return 'https://admin.com'; + } + }); + + const urlService = new UrlService(); + const resourceService = new ResourceService({ + urlUtils, + urlService + }); + + const getResource = stubGetResource(urlService); + + const result = await resourceService.getByURL( + new URL('https://site.com/blah/post-resource') + ); + + assert(getResource.calledWithExactly('/post-resource')); + + assert.equal(result.type, 'post'); + assert.equal(result.id.toHexString(), '63ce473f992390b739b00b01'); + }); + + it('Does not convert tag resources', async function () { + const urlUtils = new UrlUtils({ + getSiteUrl() { + return 'https://site.com/blah/'; + }, + getSubdir() { + return '/blah'; + }, + getAdminUrl() { + return 'https://admin.com'; + } + }); + + const urlService = new UrlService(); + const resourceService = new ResourceService({ + urlUtils, + urlService + }); + + const getResource = stubGetResource(urlService); + + const result = await resourceService.getByURL( + new URL('https://site.com/blah/tag-resource') + ); + + assert(getResource.calledWithExactly('/tag-resource')); + + assert.equal(result.type, null); + assert.equal(result.id, null); + }); + + it('Handles non-resources', async function () { + const urlUtils = new UrlUtils({ + getSiteUrl() { + return 'https://site.com/blah/'; + }, + getSubdir() { + return '/blah'; + }, + getAdminUrl() { + return 'https://admin.com'; + } + }); + + const urlService = new UrlService(); + const resourceService = new ResourceService({ + urlUtils, + urlService + }); + + const getResource = stubGetResource(urlService); + + const result = await resourceService.getByURL( + new URL('https://site.com/blah/no-resource') + ); + + assert(getResource.calledWithExactly('/no-resource')); + + assert.equal(result.type, null); + assert.equal(result.id, null); + }); + }); +});