From 16469ab64e4856682b85b023de26f46f76e0cfb5 Mon Sep 17 00:00:00 2001 From: David Russ Date: Wed, 23 Mar 2022 16:34:34 -0400 Subject: [PATCH 01/14] Adding support for relation fields --- server/services/core.js | 17 ++++++--- server/services/pattern.js | 72 +++++++++++++++++++++++++++++++++----- 2 files changed, 76 insertions(+), 13 deletions(-) diff --git a/server/services/core.js b/server/services/core.js index f63bb05..1c6fbf5 100644 --- a/server/services/core.js +++ b/server/services/core.js @@ -90,7 +90,6 @@ const getLanguageLinks = async (page, contentType, defaultURL, excludeDrafts) => const getSitemapPageData = async (page, contentType, excludeDrafts) => { let locale = page.locale || 'und'; const config = await getService('settings').getConfig(); - // Return when there is no pattern for the page. if ( !config.contentTypes[contentType]['languages'][locale] @@ -130,6 +129,16 @@ const createSitemapEntries = async () => { // Collection entries. await Promise.all(Object.keys(config.contentTypes).map(async (contentType) => { const excludeDrafts = config.excludeDrafts && strapi.contentTypes[contentType].options.draftAndPublish; + + const populate = ['localizations'].concat(Object.keys(strapi.contentTypes[contentType].attributes).reduce((prev, current) => { + + + if(strapi.contentTypes[contentType].attributes[current].type == 'relation'){ + prev.push(current) + } + return prev + }, [])) + const pages = await noLimit(strapi.query(contentType), { where: { $or: [ @@ -148,16 +157,15 @@ const createSitemapEntries = async () => { $notNull: excludeDrafts, }, }, - populate: ['localizations'], + populate, }); - // Add formatted sitemap page data to the array. await Promise.all(pages.map(async (page) => { + const pageData = await getSitemapPageData(page, contentType, excludeDrafts); if (pageData) sitemapEntries.push(pageData); })); })); - // Custom entries. await Promise.all(Object.keys(config.customEntries).map(async (customEntry) => { sitemapEntries.push({ @@ -224,6 +232,7 @@ const createSitemap = async () => { }); const sitemapEntries = await createSitemapEntries(); + if (isEmpty(sitemapEntries)) { strapi.log.info(logMessage(`No sitemap XML was generated because there were 0 URLs configured.`)); return; diff --git a/server/services/pattern.js b/server/services/pattern.js index f3692e8..7dc6f0c 100644 --- a/server/services/pattern.js +++ b/server/services/pattern.js @@ -18,6 +18,15 @@ const getAllowedFields = async (contentType) => { if (field.type === fieldType) { fields.push(fieldName); } + if (field.type === 'relation'){ + const relation = strapi.contentTypes[field.target] + Object.entries(relation.attributes).map(([fieldName, field]) => { + if (field.type === fieldType) { + fields.push(fieldName); + } + }) + + } }); }); @@ -29,16 +38,32 @@ const getAllowedFields = async (contentType) => { return fields; }; +const recursiveMatch = (fields) => { + let result = {} + for(let o of fields){ + + let field = RegExp(/\[([\w\d\[\]]+)\]/g).exec(o)[1]; + if(RegExp(/\[.*\]/g).test(field)){ + let fieldName = RegExp(/[\w\d]+/g).exec(field)[0] + result[fieldName] = recursiveMatch(field.match(/\[([\w\d\[\]]+)\]/g)); + + } else { + result[field] = {} + } + } + return result +} + /** * Get all fields from a pattern. * * @param {string} pattern - The pattern. * - * @returns {array} The fields. + * @returns {array} The fields.\[([\w\d\[\]]+)\] */ const getFieldsFromPattern = (pattern) => { - let fields = pattern.match(/[[\w\d]+]/g); // Get all substrings between [] as array. - fields = fields.map((field) => RegExp(/(?<=\[)(.*?)(?=\])/).exec(field)[0]); // Strip [] from string. + let fields = pattern.match(/\[([\w\d\[\]]+)\]/g); // Get all substrings between [] as array. + fields = recursiveMatch(fields); // Strip [] from string. return fields; }; @@ -50,11 +75,25 @@ const getFieldsFromPattern = (pattern) => { * * @returns {string} The path. */ + + + const resolvePattern = async (pattern, entity) => { const fields = getFieldsFromPattern(pattern); - fields.map((field) => { - pattern = pattern.replace(`[${field}]`, entity[field] || ''); + Object.keys(fields).map((field) => { + + if(!Object.keys(fields[field]).length){ + pattern = pattern.replace(`[${field}]`, entity[field] || ''); + } else { + + const subField = Object.keys(fields[field])[0] + if(Array.isArray(entity[field]) && entity[field][0]){ + pattern = pattern.replace(`[${field}[${subField}]]`, entity[field][0][subField] || ''); + } else { + pattern = pattern.replace(`[${field}[${subField}]]`, entity[field][subField] || ''); + } + } }); pattern = pattern.replace(/([^:]\/)\/+/g, "$1"); // Remove duplicate forward slashes. @@ -98,9 +137,24 @@ const validatePattern = async (pattern, allowedFieldNames) => { } let fieldsAreAllowed = true; - getFieldsFromPattern(pattern).map((field) => { - if (!allowedFieldNames.includes(field)) fieldsAreAllowed = false; - }); + const allowedFieldsRecursive = (fields) => { + Object.keys(fields).map((field) => { + try{ + if(Object.keys(fields[field]) && Object.keys(fields[field]).length > 0){ + allowedFieldsRecursive(fields[field]) + } + } catch(e) { + console.log("Failed!") + console.log(e) + } + + if (!allowedFieldNames.includes(field)) fieldsAreAllowed = false; + return true + }); + + } + allowedFieldsRecursive(getFieldsFromPattern(pattern)) + if (!fieldsAreAllowed) { return { @@ -108,7 +162,7 @@ const validatePattern = async (pattern, allowedFieldNames) => { message: "Pattern contains forbidden fields", }; } - + return { valid: true, message: "Valid pattern", From b8db8b88e8f37e11482ec2daa9547df96b2bc624 Mon Sep 17 00:00:00 2001 From: David Russ Date: Wed, 23 Mar 2022 16:56:48 -0400 Subject: [PATCH 02/14] get allowed fields bug fix --- server/services/pattern.js | 138 ++++++++++++++++++------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/server/services/pattern.js b/server/services/pattern.js index 7dc6f0c..0131cd4 100644 --- a/server/services/pattern.js +++ b/server/services/pattern.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict' /** * Pattern service. @@ -11,42 +11,39 @@ * * @returns {string} The fields. */ -const getAllowedFields = async (contentType) => { - const fields = []; - strapi.config.get('plugin.sitemap.allowedFields').map((fieldType) => { +const getAllowedFields = async contentType => { + const fields = [] + strapi.config.get('plugin.sitemap.allowedFields').map(fieldType => { Object.entries(contentType.attributes).map(([fieldName, field]) => { if (field.type === fieldType) { - fields.push(fieldName); + fields.push(fieldName) } - if (field.type === 'relation'){ + if (field.type === 'relation' && field.target) { const relation = strapi.contentTypes[field.target] Object.entries(relation.attributes).map(([fieldName, field]) => { if (field.type === fieldType) { - fields.push(fieldName); + fields.push(fieldName) } }) - } - }); - }); + }) + }) // Add id field manually because it is not on the attributes object of a content type. if (strapi.config.get('plugin.sitemap.allowedFields').includes('id')) { - fields.push('id'); + fields.push('id') } - return fields; -}; + return fields +} -const recursiveMatch = (fields) => { +const recursiveMatch = fields => { let result = {} - for(let o of fields){ - - let field = RegExp(/\[([\w\d\[\]]+)\]/g).exec(o)[1]; - if(RegExp(/\[.*\]/g).test(field)){ + for (let o of fields) { + let field = RegExp(/\[([\w\d\[\]]+)\]/g).exec(o)[1] + if (RegExp(/\[.*\]/g).test(field)) { let fieldName = RegExp(/[\w\d]+/g).exec(field)[0] - result[fieldName] = recursiveMatch(field.match(/\[([\w\d\[\]]+)\]/g)); - + result[fieldName] = recursiveMatch(field.match(/\[([\w\d\[\]]+)\]/g)) } else { result[field] = {} } @@ -61,11 +58,11 @@ const recursiveMatch = (fields) => { * * @returns {array} The fields.\[([\w\d\[\]]+)\] */ -const getFieldsFromPattern = (pattern) => { - let fields = pattern.match(/\[([\w\d\[\]]+)\]/g); // Get all substrings between [] as array. - fields = recursiveMatch(fields); // Strip [] from string. - return fields; -}; +const getFieldsFromPattern = pattern => { + let fields = pattern.match(/\[([\w\d\[\]]+)\]/g) // Get all substrings between [] as array. + fields = recursiveMatch(fields) // Strip [] from string. + return fields +} /** * Resolve a pattern string from pattern to path for a single entity. @@ -76,30 +73,32 @@ const getFieldsFromPattern = (pattern) => { * @returns {string} The path. */ - - const resolvePattern = async (pattern, entity) => { - const fields = getFieldsFromPattern(pattern); - - Object.keys(fields).map((field) => { + const fields = getFieldsFromPattern(pattern) - if(!Object.keys(fields[field]).length){ - pattern = pattern.replace(`[${field}]`, entity[field] || ''); + Object.keys(fields).map(field => { + if (!Object.keys(fields[field]).length) { + pattern = pattern.replace(`[${field}]`, entity[field] || '') } else { - const subField = Object.keys(fields[field])[0] - if(Array.isArray(entity[field]) && entity[field][0]){ - pattern = pattern.replace(`[${field}[${subField}]]`, entity[field][0][subField] || ''); + if (Array.isArray(entity[field]) && entity[field][0]) { + pattern = pattern.replace( + `[${field}[${subField}]]`, + entity[field][0][subField] || '' + ) } else { - pattern = pattern.replace(`[${field}[${subField}]]`, entity[field][subField] || ''); + pattern = pattern.replace( + `[${field}[${subField}]]`, + entity[field][subField] || '' + ) } } - }); + }) - pattern = pattern.replace(/([^:]\/)\/+/g, "$1"); // Remove duplicate forward slashes. - pattern = pattern.startsWith('/') ? pattern : `/${pattern}`; // Add a starting slash. - return pattern; -}; + pattern = pattern.replace(/([^:]\/)\/+/g, '$1') // Remove duplicate forward slashes. + pattern = pattern.startsWith('/') ? pattern : `/${pattern}` // Add a starting slash. + return pattern +} /** * Validate if a pattern is correctly structured. @@ -115,63 +114,64 @@ const validatePattern = async (pattern, allowedFieldNames) => { if (!pattern) { return { valid: false, - message: "Pattern can not be empty", - }; + message: 'Pattern can not be empty' + } } - const preCharCount = pattern.split("[").length - 1; - const postCharount = pattern.split("]").length - 1; + const preCharCount = pattern.split('[').length - 1 + const postCharount = pattern.split(']').length - 1 if (preCharCount < 1 || postCharount < 1) { return { valid: false, - message: "Pattern should contain at least one field", - }; + message: 'Pattern should contain at least one field' + } } if (preCharCount !== postCharount) { return { valid: false, - message: "Fields in the pattern are not escaped correctly", - }; + message: 'Fields in the pattern are not escaped correctly' + } } - let fieldsAreAllowed = true; - const allowedFieldsRecursive = (fields) => { - Object.keys(fields).map((field) => { - try{ - if(Object.keys(fields[field]) && Object.keys(fields[field]).length > 0){ + let fieldsAreAllowed = true + const allowedFieldsRecursive = fields => { + Object.keys(fields).map(field => { + try { + if ( + Object.keys(fields[field]) && + Object.keys(fields[field]).length > 0 + ) { allowedFieldsRecursive(fields[field]) } - } catch(e) { - console.log("Failed!") + } catch (e) { + console.log('Failed!') console.log(e) } - - if (!allowedFieldNames.includes(field)) fieldsAreAllowed = false; - return true - }); + if (!allowedFieldNames.includes(field)) fieldsAreAllowed = false + return true + }) } allowedFieldsRecursive(getFieldsFromPattern(pattern)) - if (!fieldsAreAllowed) { return { valid: false, - message: "Pattern contains forbidden fields", - }; + message: 'Pattern contains forbidden fields' + } } - + return { valid: true, - message: "Valid pattern", - }; -}; + message: 'Valid pattern' + } +} module.exports = () => ({ getAllowedFields, getFieldsFromPattern, resolvePattern, - validatePattern, -}); + validatePattern +}) From d496f408ee44ed0a948f6232cf1d16996684a170 Mon Sep 17 00:00:00 2001 From: David Russ Date: Thu, 24 Mar 2022 09:17:06 -0400 Subject: [PATCH 03/14] Adding ES Lint fixes --- server/services/core.js | 18 ++--- server/services/pattern.js | 141 ++++++++++++++++++------------------- 2 files changed, 79 insertions(+), 80 deletions(-) diff --git a/server/services/core.js b/server/services/core.js index 268a480..9d4fc38 100644 --- a/server/services/core.js +++ b/server/services/core.js @@ -130,15 +130,15 @@ const createSitemapEntries = async () => { // Collection entries. await Promise.all(Object.keys(config.contentTypes).map(async (contentType) => { const excludeDrafts = config.excludeDrafts && strapi.contentTypes[contentType].options.draftAndPublish; - + const populate = ['localizations'].concat(Object.keys(strapi.contentTypes[contentType].attributes).reduce((prev, current) => { - - - if(strapi.contentTypes[contentType].attributes[current].type == 'relation'){ - prev.push(current) + + + if (strapi.contentTypes[contentType].attributes[current].type === 'relation') { + prev.push(current); } - return prev - }, [])) + return prev; + }, [])); const pages = await noLimit(strapi.query(contentType), { where: { @@ -159,11 +159,11 @@ const createSitemapEntries = async () => { }, }, populate, - orderBy: 'id' + orderBy: 'id', }); // Add formatted sitemap page data to the array. await Promise.all(pages.map(async (page) => { - + const pageData = await getSitemapPageData(page, contentType, excludeDrafts); if (pageData) sitemapEntries.push(pageData); })); diff --git a/server/services/pattern.js b/server/services/pattern.js index 0131cd4..d681200 100644 --- a/server/services/pattern.js +++ b/server/services/pattern.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; /** * Pattern service. @@ -11,45 +11,44 @@ * * @returns {string} The fields. */ -const getAllowedFields = async contentType => { - const fields = [] - strapi.config.get('plugin.sitemap.allowedFields').map(fieldType => { +const getAllowedFields = async (contentType) => { + const fields = []; + strapi.config.get('plugin.sitemap.allowedFields').map((fieldType) => { Object.entries(contentType.attributes).map(([fieldName, field]) => { if (field.type === fieldType) { - fields.push(fieldName) + fields.push(fieldName); } if (field.type === 'relation' && field.target) { - const relation = strapi.contentTypes[field.target] - Object.entries(relation.attributes).map(([fieldName, field]) => { - if (field.type === fieldType) { - fields.push(fieldName) + const relation = strapi.contentTypes[field.target]; + Object.entries(relation.attributes).map(([subFieldName, subField]) => { + if (subField.type === fieldType) { + fields.push(subFieldName); } - }) + }); } - }) - }) + }); + }); // Add id field manually because it is not on the attributes object of a content type. if (strapi.config.get('plugin.sitemap.allowedFields').includes('id')) { - fields.push('id') + fields.push('id'); } - return fields -} + return fields; +}; -const recursiveMatch = fields => { - let result = {} - for (let o of fields) { - let field = RegExp(/\[([\w\d\[\]]+)\]/g).exec(o)[1] +const recursiveMatch = (fields) => { + return fields.reduce((result, o) => { + const field = RegExp(/\[([\w\d[\]]+)\]/g).exec(o)[1]; if (RegExp(/\[.*\]/g).test(field)) { - let fieldName = RegExp(/[\w\d]+/g).exec(field)[0] - result[fieldName] = recursiveMatch(field.match(/\[([\w\d\[\]]+)\]/g)) + const fieldName = RegExp(/[\w\d]+/g).exec(field)[0]; + result[fieldName] = recursiveMatch(field.match(/\[([\w\d[\]]+)\]/g)); } else { - result[field] = {} + result[field] = {}; } - } - return result -} + return result; + }, {}); +}; /** * Get all fields from a pattern. @@ -58,11 +57,11 @@ const recursiveMatch = fields => { * * @returns {array} The fields.\[([\w\d\[\]]+)\] */ -const getFieldsFromPattern = pattern => { - let fields = pattern.match(/\[([\w\d\[\]]+)\]/g) // Get all substrings between [] as array. - fields = recursiveMatch(fields) // Strip [] from string. - return fields -} +const getFieldsFromPattern = (pattern) => { + let fields = pattern.match(/\[([\w\d[\]]+)\]/g); // Get all substrings between [] as array. + fields = recursiveMatch(fields); // Strip [] from string. + return fields; +}; /** * Resolve a pattern string from pattern to path for a single entity. @@ -74,31 +73,31 @@ const getFieldsFromPattern = pattern => { */ const resolvePattern = async (pattern, entity) => { - const fields = getFieldsFromPattern(pattern) + const fields = getFieldsFromPattern(pattern); - Object.keys(fields).map(field => { + Object.keys(fields).map((field) => { if (!Object.keys(fields[field]).length) { - pattern = pattern.replace(`[${field}]`, entity[field] || '') + pattern = pattern.replace(`[${field}]`, entity[field] || ''); } else { - const subField = Object.keys(fields[field])[0] + const subField = Object.keys(fields[field])[0]; if (Array.isArray(entity[field]) && entity[field][0]) { pattern = pattern.replace( `[${field}[${subField}]]`, - entity[field][0][subField] || '' - ) + entity[field][0][subField] || '', + ); } else { pattern = pattern.replace( `[${field}[${subField}]]`, - entity[field][subField] || '' - ) + entity[field][subField] || '', + ); } } - }) + }); - pattern = pattern.replace(/([^:]\/)\/+/g, '$1') // Remove duplicate forward slashes. - pattern = pattern.startsWith('/') ? pattern : `/${pattern}` // Add a starting slash. - return pattern -} + pattern = pattern.replace(/([^:]\/)\/+/g, '$1'); // Remove duplicate forward slashes. + pattern = pattern.startsWith('/') ? pattern : `/${pattern}`; // Add a starting slash. + return pattern; +}; /** * Validate if a pattern is correctly structured. @@ -114,64 +113,64 @@ const validatePattern = async (pattern, allowedFieldNames) => { if (!pattern) { return { valid: false, - message: 'Pattern can not be empty' - } + message: 'Pattern can not be empty', + }; } - const preCharCount = pattern.split('[').length - 1 - const postCharount = pattern.split(']').length - 1 + const preCharCount = pattern.split('[').length - 1; + const postCharount = pattern.split(']').length - 1; if (preCharCount < 1 || postCharount < 1) { return { valid: false, - message: 'Pattern should contain at least one field' - } + message: 'Pattern should contain at least one field', + }; } if (preCharCount !== postCharount) { return { valid: false, - message: 'Fields in the pattern are not escaped correctly' - } + message: 'Fields in the pattern are not escaped correctly', + }; } - let fieldsAreAllowed = true - const allowedFieldsRecursive = fields => { - Object.keys(fields).map(field => { + let fieldsAreAllowed = true; + const allowedFieldsRecursive = (fields) => { + Object.keys(fields).map((field) => { try { if ( - Object.keys(fields[field]) && - Object.keys(fields[field]).length > 0 + Object.keys(fields[field]) + && Object.keys(fields[field]).length > 0 ) { - allowedFieldsRecursive(fields[field]) + allowedFieldsRecursive(fields[field]); } } catch (e) { - console.log('Failed!') - console.log(e) + console.log('Failed!'); + console.log(e); } - if (!allowedFieldNames.includes(field)) fieldsAreAllowed = false - return true - }) - } - allowedFieldsRecursive(getFieldsFromPattern(pattern)) + if (!allowedFieldNames.includes(field)) fieldsAreAllowed = false; + return true; + }); + }; + allowedFieldsRecursive(getFieldsFromPattern(pattern)); if (!fieldsAreAllowed) { return { valid: false, - message: 'Pattern contains forbidden fields' - } + message: 'Pattern contains forbidden fields', + }; } return { valid: true, - message: 'Valid pattern' - } -} + message: 'Valid pattern', + }; +}; module.exports = () => ({ getAllowedFields, getFieldsFromPattern, resolvePattern, - validatePattern -}) + validatePattern, +}); From fed1491f58f39605e57947e32c389becf77121fb Mon Sep 17 00:00:00 2001 From: David Russ Date: Thu, 14 Apr 2022 11:44:07 -0400 Subject: [PATCH 04/14] Changing relational fields to resolve with . instead of nested [] --- server/services/core.js | 27 +++++------- server/services/pattern.js | 89 +++++++++++++------------------------- 2 files changed, 40 insertions(+), 76 deletions(-) diff --git a/server/services/core.js b/server/services/core.js index a19d074..27dcb70 100644 --- a/server/services/core.js +++ b/server/services/core.js @@ -42,9 +42,9 @@ const getLanguageLinks = async (page, contentType, defaultURL, excludeDrafts) => }, ], id: translation.id, - published_at: excludeDrafts ? { - $notNull: true, - } : {}, + publishedAt: { + $notNull: excludeDrafts, + }, }, orderBy: 'id', populate: ['localizations'], @@ -69,8 +69,7 @@ const getLanguageLinks = async (page, contentType, defaultURL, excludeDrafts) => const { pattern } = config.contentTypes[contentType]['languages'][locale]; const translationUrl = await strapi.plugins.sitemap.services.pattern.resolvePattern(pattern, translationEntity); - let hostnameOverride = config.hostname_overrides[translationEntity.locale] || ''; - hostnameOverride = hostnameOverride.replace(/\/+$/, ""); + const hostnameOverride = config.hostname_overrides[translationEntity.locale]?.replace(/\/+$/, "") || ''; links.push({ lang: translationEntity.locale, url: `${hostnameOverride}${translationUrl}`, @@ -107,23 +106,17 @@ const getSitemapPageData = async (page, contentType, excludeDrafts) => { const { pattern } = config.contentTypes[contentType]['languages'][locale]; const path = await strapi.plugins.sitemap.services.pattern.resolvePattern(pattern, page); - let hostnameOverride = config.hostname_overrides[page.locale] || ''; - hostnameOverride = hostnameOverride.replace(/\/+$/, ""); + + const hostnameOverride = config.hostname_overrides[page.locale]?.replace(/\/+$/, "") || ''; const url = `${hostnameOverride}${path}`; - const pageData = { + return { lastmod: page.updatedAt, url: url, links: await getLanguageLinks(page, contentType, url, excludeDrafts), changefreq: config.contentTypes[contentType]['languages'][locale].changefreq || 'monthly', priority: parseFloat(config.contentTypes[contentType]['languages'][locale].priority) || 0.5, }; - - if (config.contentTypes[contentType]['languages'][locale].includeLastmod === false) { - delete pageData.lastmod; - } - - return pageData; }; /** @@ -162,9 +155,9 @@ const createSitemapEntries = async () => { }, }, ], - published_at: excludeDrafts ? { - $notNull: true, - } : {}, + published_at: { + $notNull: excludeDrafts, + }, }, populate, orderBy: 'id', diff --git a/server/services/pattern.js b/server/services/pattern.js index d681200..2ae8f8e 100644 --- a/server/services/pattern.js +++ b/server/services/pattern.js @@ -15,16 +15,19 @@ const getAllowedFields = async (contentType) => { const fields = []; strapi.config.get('plugin.sitemap.allowedFields').map((fieldType) => { Object.entries(contentType.attributes).map(([fieldName, field]) => { - if (field.type === fieldType) { + if (field.type === fieldType && field.type !== 'relation') { fields.push(fieldName); - } - if (field.type === 'relation' && field.target) { + } else if (field.type === 'relation' && field.target && !field.private) { const relation = strapi.contentTypes[field.target]; - Object.entries(relation.attributes).map(([subFieldName, subField]) => { - if (subField.type === fieldType) { - fields.push(subFieldName); - } - }); + if (!fields.includes(`${fieldName}.id`)) { + fields.push(`${fieldName}.id`); + } + Object.entries(relation.attributes).map(([subFieldName, subField]) => { + if (subField.type === fieldType) { + fields.push(`${fieldName}.${subFieldName}`); + } + }); + } }); }); @@ -37,18 +40,6 @@ const getAllowedFields = async (contentType) => { return fields; }; -const recursiveMatch = (fields) => { - return fields.reduce((result, o) => { - const field = RegExp(/\[([\w\d[\]]+)\]/g).exec(o)[1]; - if (RegExp(/\[.*\]/g).test(field)) { - const fieldName = RegExp(/[\w\d]+/g).exec(field)[0]; - result[fieldName] = recursiveMatch(field.match(/\[([\w\d[\]]+)\]/g)); - } else { - result[field] = {}; - } - return result; - }, {}); -}; /** * Get all fields from a pattern. @@ -58,8 +49,8 @@ const recursiveMatch = (fields) => { * @returns {array} The fields.\[([\w\d\[\]]+)\] */ const getFieldsFromPattern = (pattern) => { - let fields = pattern.match(/\[([\w\d[\]]+)\]/g); // Get all substrings between [] as array. - fields = recursiveMatch(fields); // Strip [] from string. + let fields = pattern.match(/[[\w\d.]+]/g); // Get all substrings between [] as array. + fields = fields.map((field) => RegExp(/(?<=\[)(.*?)(?=\])/).exec(field)[0]); // Strip [] from string. return fields; }; @@ -72,29 +63,24 @@ const getFieldsFromPattern = (pattern) => { * @returns {string} The path. */ -const resolvePattern = async (pattern, entity) => { + const resolvePattern = async (pattern, entity) => { const fields = getFieldsFromPattern(pattern); - Object.keys(fields).map((field) => { - if (!Object.keys(fields[field]).length) { - pattern = pattern.replace(`[${field}]`, entity[field] || ''); - } else { - const subField = Object.keys(fields[field])[0]; - if (Array.isArray(entity[field]) && entity[field][0]) { - pattern = pattern.replace( - `[${field}[${subField}]]`, - entity[field][0][subField] || '', - ); - } else { - pattern = pattern.replace( - `[${field}[${subField}]]`, - entity[field][subField] || '', - ); - } - } + fields.map((field) => { + const relationalField = field.split('.').length > 1 ? field.split('.') : null; + if (!relationalField) { + pattern = pattern.replace(`[${field}]`, entity[field] || ''); + } else if (Array.isArray(entity[relationalField[0]])) { + // If the relational attribute is an array, use the first result. + pattern = pattern.replace(`[${field}]`, entity[relationalField[0]][0] && entity[relationalField[0]][0][relationalField[1]] ? entity[relationalField[0]][0][relationalField[1]] : ''); + } else if (typeof entity[relationalField[0]] === 'object') { + pattern = pattern.replace(`[${field}]`, entity[relationalField[0]] && entity[relationalField[0]][relationalField[1]] ? entity[relationalField[0]][relationalField[1]] : ''); + } + + }); - pattern = pattern.replace(/([^:]\/)\/+/g, '$1'); // Remove duplicate forward slashes. + pattern = pattern.replace(/([^:]\/)\/+/g, "$1"); // Remove duplicate forward slashes. pattern = pattern.startsWith('/') ? pattern : `/${pattern}`; // Add a starting slash. return pattern; }; @@ -135,25 +121,10 @@ const validatePattern = async (pattern, allowedFieldNames) => { } let fieldsAreAllowed = true; - const allowedFieldsRecursive = (fields) => { - Object.keys(fields).map((field) => { - try { - if ( - Object.keys(fields[field]) - && Object.keys(fields[field]).length > 0 - ) { - allowedFieldsRecursive(fields[field]); - } - } catch (e) { - console.log('Failed!'); - console.log(e); - } - if (!allowedFieldNames.includes(field)) fieldsAreAllowed = false; - return true; - }); - }; - allowedFieldsRecursive(getFieldsFromPattern(pattern)); + getFieldsFromPattern(pattern).map((field) => { + if (!allowedFieldNames.includes(field)) fieldsAreAllowed = false; + }); if (!fieldsAreAllowed) { return { From 455a633f6c2121d72762c0d24784c79bbd6b5c97 Mon Sep 17 00:00:00 2001 From: Boaz Poolman Date: Sat, 16 Apr 2022 08:59:02 +0200 Subject: [PATCH 05/14] fix: Put back overridden code --- server/services/core.js | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/server/services/core.js b/server/services/core.js index 27dcb70..38adc44 100644 --- a/server/services/core.js +++ b/server/services/core.js @@ -42,9 +42,9 @@ const getLanguageLinks = async (page, contentType, defaultURL, excludeDrafts) => }, ], id: translation.id, - publishedAt: { - $notNull: excludeDrafts, - }, + published_at: excludeDrafts ? { + $notNull: true, + } : {}, }, orderBy: 'id', populate: ['localizations'], @@ -69,7 +69,8 @@ const getLanguageLinks = async (page, contentType, defaultURL, excludeDrafts) => const { pattern } = config.contentTypes[contentType]['languages'][locale]; const translationUrl = await strapi.plugins.sitemap.services.pattern.resolvePattern(pattern, translationEntity); - const hostnameOverride = config.hostname_overrides[translationEntity.locale]?.replace(/\/+$/, "") || ''; + let hostnameOverride = config.hostname_overrides[translationEntity.locale] || ''; + hostnameOverride = hostnameOverride.replace(/\/+$/, ""); links.push({ lang: translationEntity.locale, url: `${hostnameOverride}${translationUrl}`, @@ -91,6 +92,7 @@ const getLanguageLinks = async (page, contentType, defaultURL, excludeDrafts) => const getSitemapPageData = async (page, contentType, excludeDrafts) => { let locale = page.locale || 'und'; const config = await getService('settings').getConfig(); + // Return when there is no pattern for the page. if ( !config.contentTypes[contentType]['languages'][locale] @@ -106,17 +108,23 @@ const getSitemapPageData = async (page, contentType, excludeDrafts) => { const { pattern } = config.contentTypes[contentType]['languages'][locale]; const path = await strapi.plugins.sitemap.services.pattern.resolvePattern(pattern, page); - - const hostnameOverride = config.hostname_overrides[page.locale]?.replace(/\/+$/, "") || ''; + let hostnameOverride = config.hostname_overrides[page.locale] || ''; + hostnameOverride = hostnameOverride.replace(/\/+$/, ""); const url = `${hostnameOverride}${path}`; - return { + const pageData = { lastmod: page.updatedAt, url: url, links: await getLanguageLinks(page, contentType, url, excludeDrafts), changefreq: config.contentTypes[contentType]['languages'][locale].changefreq || 'monthly', priority: parseFloat(config.contentTypes[contentType]['languages'][locale].priority) || 0.5, }; + + if (config.contentTypes[contentType]['languages'][locale].includeLastmod === false) { + delete pageData.lastmod; + } + + return pageData; }; /** @@ -155,9 +163,9 @@ const createSitemapEntries = async () => { }, }, ], - published_at: { - $notNull: excludeDrafts, - }, + published_at: excludeDrafts ? { + $notNull: true, + } : {}, }, populate, orderBy: 'id', From 78ed6a2d007107e44d0173b5b4ceb28b109f15b9 Mon Sep 17 00:00:00 2001 From: Boaz Poolman Date: Mon, 18 Apr 2022 09:47:32 +0200 Subject: [PATCH 06/14] fix: Relational pattern for a bilingual site --- server/services/pattern.js | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/server/services/pattern.js b/server/services/pattern.js index 2ae8f8e..b0bfd40 100644 --- a/server/services/pattern.js +++ b/server/services/pattern.js @@ -18,16 +18,24 @@ const getAllowedFields = async (contentType) => { if (field.type === fieldType && field.type !== 'relation') { fields.push(fieldName); } else if (field.type === 'relation' && field.target && !field.private) { - const relation = strapi.contentTypes[field.target]; - if (!fields.includes(`${fieldName}.id`)) { - fields.push(`${fieldName}.id`); - } + if (fieldName === 'localizations') { + return null; + } else { + const relation = strapi.contentTypes[field.target]; + + if ( + strapi.config.get('plugin.sitemap.allowedFields').includes('id') + && !fields.includes(`${fieldName}.id`) + ) { + fields.push(`${fieldName}.id`); + } + Object.entries(relation.attributes).map(([subFieldName, subField]) => { if (subField.type === fieldType) { fields.push(`${fieldName}.${subFieldName}`); } }); - + } } }); }); @@ -68,16 +76,15 @@ const getFieldsFromPattern = (pattern) => { fields.map((field) => { const relationalField = field.split('.').length > 1 ? field.split('.') : null; + if (!relationalField) { pattern = pattern.replace(`[${field}]`, entity[field] || ''); } else if (Array.isArray(entity[relationalField[0]])) { - // If the relational attribute is an array, use the first result. - pattern = pattern.replace(`[${field}]`, entity[relationalField[0]][0] && entity[relationalField[0]][0][relationalField[1]] ? entity[relationalField[0]][0][relationalField[1]] : ''); - } else if (typeof entity[relationalField[0]] === 'object') { - pattern = pattern.replace(`[${field}]`, entity[relationalField[0]] && entity[relationalField[0]][relationalField[1]] ? entity[relationalField[0]][relationalField[1]] : ''); - } - - + // If the relational attribute is an array, use the first result. + pattern = pattern.replace(`[${field}]`, entity[relationalField[0]][0] && entity[relationalField[0]][0][relationalField[1]] ? entity[relationalField[0]][0][relationalField[1]] : ''); + } else if (typeof entity[relationalField[0]] === 'object') { + pattern = pattern.replace(`[${field}]`, entity[relationalField[0]] && entity[relationalField[0]][relationalField[1]] ? entity[relationalField[0]][relationalField[1]] : ''); + } }); pattern = pattern.replace(/([^:]\/)\/+/g, "$1"); // Remove duplicate forward slashes. From 252aebe10483bb45a97f6a385de8730692d754ad Mon Sep 17 00:00:00 2001 From: Boaz Poolman Date: Mon, 18 Apr 2022 09:57:33 +0200 Subject: [PATCH 07/14] chore: Write tests for the new relational pattern logic --- server/services/__tests__/pattern.test.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/services/__tests__/pattern.test.js b/server/services/__tests__/pattern.test.js index a824fbe..74e020c 100644 --- a/server/services/__tests__/pattern.test.js +++ b/server/services/__tests__/pattern.test.js @@ -6,24 +6,27 @@ const patternService = require('../pattern'); describe('Pattern service', () => { describe('Get fields from pattern', () => { test('Should return an array of fieldnames extracted from a pattern', () => { - const pattern = '/en/[category]/[slug]'; + const pattern = '/en/[category]/[slug]/[relation.id]'; const result = patternService().getFieldsFromPattern(pattern); - expect(result).toEqual(['category', 'slug']); + expect(result).toEqual(['category', 'slug', 'relation.id']); }); }); describe('Resolve pattern', () => { test('Resolve valid pattern', async () => { - const pattern = '/en/[category]/[slug]'; + const pattern = '/en/[category]/[slug]/[relation.url]'; const entity = { category: 'category-a', slug: 'my-page-slug', + relation: { + url: 'relation-url', + }, }; const result = await patternService().resolvePattern(pattern, entity); - expect(result).toMatch('/en/category-a/my-page-slug'); + expect(result).toMatch('/en/category-a/my-page-slug/relation-url'); }); test('Resolve pattern with missing field', async () => { From a4175fb457b7225ce9c0fb7492bca9432c91ca40 Mon Sep 17 00:00:00 2001 From: Boaz Poolman Date: Thu, 21 Apr 2022 19:15:11 +0200 Subject: [PATCH 08/14] refactor: Disallow array relations in the pattern --- server/services/pattern.js | 42 ++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/server/services/pattern.js b/server/services/pattern.js index b0bfd40..fc16113 100644 --- a/server/services/pattern.js +++ b/server/services/pattern.js @@ -1,5 +1,7 @@ 'use strict'; +const { logMessage } = require("../utils"); + /** * Pattern service. */ @@ -17,25 +19,26 @@ const getAllowedFields = async (contentType) => { Object.entries(contentType.attributes).map(([fieldName, field]) => { if (field.type === fieldType && field.type !== 'relation') { fields.push(fieldName); - } else if (field.type === 'relation' && field.target && !field.private) { - if (fieldName === 'localizations') { - return null; - } else { - const relation = strapi.contentTypes[field.target]; - - if ( - strapi.config.get('plugin.sitemap.allowedFields').includes('id') - && !fields.includes(`${fieldName}.id`) - ) { - fields.push(`${fieldName}.id`); - } - - Object.entries(relation.attributes).map(([subFieldName, subField]) => { - if (subField.type === fieldType) { - fields.push(`${fieldName}.${subFieldName}`); - } - }); + } else if ( + field.type === 'relation' + && field.target + && field.relation.endsWith('ToOne') // TODO: implement `ToMany` relations (#78). + && fieldName !== 'localizations' + ) { + const relation = strapi.contentTypes[field.target]; + + if ( + strapi.config.get('plugin.sitemap.allowedFields').includes('id') + && !fields.includes(`${fieldName}.id`) + ) { + fields.push(`${fieldName}.id`); } + + Object.entries(relation.attributes).map(([subFieldName, subField]) => { + if (subField.type === fieldType) { + fields.push(`${fieldName}.${subFieldName}`); + } + }); } }); }); @@ -80,8 +83,7 @@ const getFieldsFromPattern = (pattern) => { if (!relationalField) { pattern = pattern.replace(`[${field}]`, entity[field] || ''); } else if (Array.isArray(entity[relationalField[0]])) { - // If the relational attribute is an array, use the first result. - pattern = pattern.replace(`[${field}]`, entity[relationalField[0]][0] && entity[relationalField[0]][0][relationalField[1]] ? entity[relationalField[0]][0][relationalField[1]] : ''); + strapi.log.error(logMessage('Something went wrong whilst resolving the pattern.')); } else if (typeof entity[relationalField[0]] === 'object') { pattern = pattern.replace(`[${field}]`, entity[relationalField[0]] && entity[relationalField[0]][relationalField[1]] ? entity[relationalField[0]][relationalField[1]] : ''); } From 457e57cb8c18485cf920d4ff36139e6ff3f0aefa Mon Sep 17 00:00:00 2001 From: Boaz Poolman Date: Thu, 21 Apr 2022 19:33:02 +0200 Subject: [PATCH 09/14] chore: Write more pattern tests --- server/services/__tests__/pattern.test.js | 63 +++++++++++++++++++++++ server/services/pattern.js | 12 +++-- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/server/services/__tests__/pattern.test.js b/server/services/__tests__/pattern.test.js index 74e020c..f04b06e 100644 --- a/server/services/__tests__/pattern.test.js +++ b/server/services/__tests__/pattern.test.js @@ -3,7 +3,70 @@ const patternService = require('../pattern'); +global.strapi = { + contentTypes: { + 'another-test-relation:target:api': { + attributes: { + slugField: { + type: 'uid', + }, + textField: { + type: 'text', + }, + }, + }, + }, +}; + describe('Pattern service', () => { + describe('Get allowed fields for a content type', () => { + test('Should return the right fields', () => { + const allowedFields = ['id', 'uid']; + const contentType = { + attributes: { + urlField: { + type: 'uid', + }, + textField: { + type: 'text', + }, + localizations: { + type: 'relation', + target: 'test:target:api', + relation: 'oneToOne', + }, + relation: { + type: 'relation', + target: 'another-test:target:api', + relation: 'oneToMany', + }, + anotherRelation: { + type: 'relation', + target: 'another-test-relation:target:api', + relation: 'oneToOne', + }, + }, + }; + + const result = patternService().getAllowedFields(contentType, allowedFields); + + expect(result).toContain('id'); + expect(result).toContain('urlField'); + expect(result).toContain('anotherRelation.id'); + expect(result).toContain('anotherRelation.slugField'); + }); + + test('Should return only the id as allowed', () => { + const allowedFields = ['id', 'uid']; + const contentType = { + attributes: {}, + }; + + const result = patternService().getAllowedFields(contentType, allowedFields); + + expect(result).toEqual(['id']); + }); + }); describe('Get fields from pattern', () => { test('Should return an array of fieldnames extracted from a pattern', () => { const pattern = '/en/[category]/[slug]/[relation.id]'; diff --git a/server/services/pattern.js b/server/services/pattern.js index fc16113..8d4ee58 100644 --- a/server/services/pattern.js +++ b/server/services/pattern.js @@ -10,12 +10,14 @@ const { logMessage } = require("../utils"); * Get all field names allowed in the URL of a given content type. * * @param {string} contentType - The content type. + * @param {array} allowedFields - Override the allowed fields. * - * @returns {string} The fields. + * @returns {string[]} The fields. */ -const getAllowedFields = async (contentType) => { +const getAllowedFields = (contentType, allowedFields = []) => { const fields = []; - strapi.config.get('plugin.sitemap.allowedFields').map((fieldType) => { + const fieldTypes = allowedFields.length > 0 ? allowedFields : strapi.config.get('plugin.sitemap.allowedFields'); + fieldTypes.map((fieldType) => { Object.entries(contentType.attributes).map(([fieldName, field]) => { if (field.type === fieldType && field.type !== 'relation') { fields.push(fieldName); @@ -28,7 +30,7 @@ const getAllowedFields = async (contentType) => { const relation = strapi.contentTypes[field.target]; if ( - strapi.config.get('plugin.sitemap.allowedFields').includes('id') + fieldTypes.includes('id') && !fields.includes(`${fieldName}.id`) ) { fields.push(`${fieldName}.id`); @@ -44,7 +46,7 @@ const getAllowedFields = async (contentType) => { }); // Add id field manually because it is not on the attributes object of a content type. - if (strapi.config.get('plugin.sitemap.allowedFields').includes('id')) { + if (fieldTypes.includes('id')) { fields.push('id'); } From ce758f3a4e17d07f709487577a3ad206461f6c18 Mon Sep 17 00:00:00 2001 From: Boaz Poolman Date: Thu, 21 Apr 2022 19:35:14 +0200 Subject: [PATCH 10/14] fix: Remove duplicate test --- server/services/__tests__/pattern.test.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/server/services/__tests__/pattern.test.js b/server/services/__tests__/pattern.test.js index f04b06e..3971d48 100644 --- a/server/services/__tests__/pattern.test.js +++ b/server/services/__tests__/pattern.test.js @@ -55,17 +55,6 @@ describe('Pattern service', () => { expect(result).toContain('anotherRelation.id'); expect(result).toContain('anotherRelation.slugField'); }); - - test('Should return only the id as allowed', () => { - const allowedFields = ['id', 'uid']; - const contentType = { - attributes: {}, - }; - - const result = patternService().getAllowedFields(contentType, allowedFields); - - expect(result).toEqual(['id']); - }); }); describe('Get fields from pattern', () => { test('Should return an array of fieldnames extracted from a pattern', () => { From 4b4dfaed03e9f93f06b6ee9f8e51a44b73a24295 Mon Sep 17 00:00:00 2001 From: Boaz Poolman Date: Thu, 21 Apr 2022 19:37:35 +0200 Subject: [PATCH 11/14] chore: Update tests --- server/services/__tests__/pattern.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/services/__tests__/pattern.test.js b/server/services/__tests__/pattern.test.js index 3971d48..4647690 100644 --- a/server/services/__tests__/pattern.test.js +++ b/server/services/__tests__/pattern.test.js @@ -52,8 +52,10 @@ describe('Pattern service', () => { expect(result).toContain('id'); expect(result).toContain('urlField'); + expect(result).not.toContain('textField'); expect(result).toContain('anotherRelation.id'); expect(result).toContain('anotherRelation.slugField'); + expect(result).not.toContain('anotherRelation.textField'); }); }); describe('Get fields from pattern', () => { From 7892edbd2ddde9736fe83efd8de120d4a22aeb4a Mon Sep 17 00:00:00 2001 From: Boaz Poolman Date: Thu, 21 Apr 2022 19:43:51 +0200 Subject: [PATCH 12/14] docs: Update readme --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 82c5551..332e8ed 100644 --- a/README.md +++ b/README.md @@ -97,15 +97,12 @@ Custom URLs will get the following XML attributes: To create dynamic URLs this plugin uses **URL patterns**. A URL pattern is used when adding URL bundles to the sitemap and has the following format: ``` -/pages/[my-uid-field] +/pages/[category.slug]/[my-uid-field] ``` Fields can be injected in the pattern by escaping them with `[]`. -The following field types are by default allowed in a pattern: - -- id -- uid +Also relations can be queried in the pattern like so: `[relation.fieldname]`. *Allowed field types can be altered with the `allowedFields` config. Read more about it below.* From 9c56e64a27f4c6de8effd20627a99367274dbc14 Mon Sep 17 00:00:00 2001 From: Boaz Poolman Date: Thu, 21 Apr 2022 19:45:55 +0200 Subject: [PATCH 13/14] docs: Update readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 332e8ed..b13708b 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,11 @@ Fields can be injected in the pattern by escaping them with `[]`. Also relations can be queried in the pattern like so: `[relation.fieldname]`. +The following field types are by default allowed in a pattern: + +- `id` +- `uid` + *Allowed field types can be altered with the `allowedFields` config. Read more about it below.* ## 🌍 Multilingual From ecedd297125e3cb906081bda33c3d85828719e2b Mon Sep 17 00:00:00 2001 From: Boaz Poolman Date: Fri, 22 Apr 2022 16:06:30 +0200 Subject: [PATCH 14/14] fix: Bilingual relational field --- server/services/core.js | 12 ++++++++---- server/services/pattern.js | 2 ++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/server/services/core.js b/server/services/core.js index 38adc44..7bed7f7 100644 --- a/server/services/core.js +++ b/server/services/core.js @@ -26,6 +26,13 @@ const getLanguageLinks = async (page, contentType, defaultURL, excludeDrafts) => const links = []; links.push({ lang: page.locale, url: defaultURL }); + const populate = ['localizations'].concat(Object.keys(strapi.contentTypes[contentType].attributes).reduce((prev, current) => { + if (strapi.contentTypes[contentType].attributes[current].type === 'relation') { + prev.push(current); + } + return prev; + }, [])); + await Promise.all(page.localizations.map(async (translation) => { const translationEntity = await strapi.query(contentType).findOne({ where: { @@ -46,8 +53,7 @@ const getLanguageLinks = async (page, contentType, defaultURL, excludeDrafts) => $notNull: true, } : {}, }, - orderBy: 'id', - populate: ['localizations'], + populate, }); if (!translationEntity) return null; @@ -141,8 +147,6 @@ const createSitemapEntries = async () => { const excludeDrafts = config.excludeDrafts && strapi.contentTypes[contentType].options.draftAndPublish; const populate = ['localizations'].concat(Object.keys(strapi.contentTypes[contentType].attributes).reduce((prev, current) => { - - if (strapi.contentTypes[contentType].attributes[current].type === 'relation') { prev.push(current); } diff --git a/server/services/pattern.js b/server/services/pattern.js index 8d4ee58..fb96bbd 100644 --- a/server/services/pattern.js +++ b/server/services/pattern.js @@ -26,6 +26,8 @@ const getAllowedFields = (contentType, allowedFields = []) => { && field.target && field.relation.endsWith('ToOne') // TODO: implement `ToMany` relations (#78). && fieldName !== 'localizations' + && fieldName !== 'createdBy' + && fieldName !== 'updatedBy' ) { const relation = strapi.contentTypes[field.target];