@webbycrown/strapi-advanced-sitemap 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +194 -0
- package/dist/_chunks/Settings-CDmtmOAh.js +26522 -0
- package/dist/_chunks/Settings-O4TWg_hE.mjs +26503 -0
- package/dist/_chunks/en-B2cxOPRT.mjs +104 -0
- package/dist/_chunks/en-DXDFX_Yk.js +104 -0
- package/dist/_chunks/index-D0n235aN.js +83 -0
- package/dist/_chunks/index-lDBXYK9v.mjs +84 -0
- package/dist/admin/index.js +5 -0
- package/dist/admin/index.mjs +6 -0
- package/dist/server/index.js +3 -0
- package/dist/server/index.mjs +6 -0
- package/dist/server/src/bootstrap.js +130 -0
- package/dist/server/src/config/index.js +8 -0
- package/dist/server/src/content-types/content-type/schema.json +28 -0
- package/dist/server/src/content-types/index.js +9 -0
- package/dist/server/src/content-types/option/schema.json +22 -0
- package/dist/server/src/content-types/single-url/schema.json +23 -0
- package/dist/server/src/controllers/controller.js +209 -0
- package/dist/server/src/controllers/index.js +7 -0
- package/dist/server/src/destroy.js +7 -0
- package/dist/server/src/index.js +27 -0
- package/dist/server/src/middlewares/index.js +7 -0
- package/dist/server/src/middlewares/permission-check.js +20 -0
- package/dist/server/src/policies/index.js +5 -0
- package/dist/server/src/register.js +8 -0
- package/dist/server/src/routes/index.js +74 -0
- package/dist/server/src/services/index.js +7 -0
- package/dist/server/src/services/service.js +371 -0
- package/dist/server/src/utils/check-sitemap-permission.js +139 -0
- package/package.json +82 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
4
|
+
const SITEMAP_NAMESPACE = 'http://www.sitemaps.org/schemas/sitemap/0.9';
|
|
5
|
+
const XHTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
|
6
|
+
const IMAGE_NAMESPACE = 'http://www.google.com/schemas/sitemap-image/1.1';
|
|
7
|
+
|
|
8
|
+
const escapeXml = (value = '') =>
|
|
9
|
+
value
|
|
10
|
+
.replace(/&/g, '&')
|
|
11
|
+
.replace(/"/g, '"')
|
|
12
|
+
.replace(/'/g, ''')
|
|
13
|
+
.replace(/</g, '<')
|
|
14
|
+
.replace(/>/g, '>');
|
|
15
|
+
|
|
16
|
+
const toNumberOrNull = (value) => {
|
|
17
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
22
|
+
const parsed = Number(value);
|
|
23
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const clampPriority = (value) => {
|
|
30
|
+
if (value === null || value === undefined || Number.isNaN(value)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const numeric = Math.min(Math.max(Number(value), 0), 1);
|
|
35
|
+
return Number(numeric.toFixed(3));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const formatPriority = (priority) => {
|
|
39
|
+
if (priority === null || priority === undefined) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return priority % 1 === 0 ? priority.toString() : priority.toFixed(1).replace(/0+$/, '').replace(/\.$/, '');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const sanitizeManualSitemap = (input = {}) => {
|
|
47
|
+
const kind = input.kind === 'index' ? 'index' : 'single';
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
name: typeof input.name === 'string' ? input.name : '',
|
|
51
|
+
kind,
|
|
52
|
+
basePath: typeof input.basePath === 'string' ? trimSlashes(input.basePath) : '',
|
|
53
|
+
filename: typeof input.filename === 'string' ? input.filename : '',
|
|
54
|
+
urls: Array.isArray(input.urls)
|
|
55
|
+
? input.urls.map((url = {}) => ({
|
|
56
|
+
loc: typeof url.loc === 'string' ? url.loc : '',
|
|
57
|
+
priority: kind === 'single' ? clampPriority(toNumberOrNull(url.priority)) : null,
|
|
58
|
+
}))
|
|
59
|
+
: [],
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const sanitizeCollectionConfig = (strapi, input = {}) => {
|
|
64
|
+
let type = typeof input.type === 'string' ? input.type : '';
|
|
65
|
+
|
|
66
|
+
if (type && !type.includes('::')) {
|
|
67
|
+
const match = Object.values(strapi.contentTypes).find((ct) => ct.info?.singularName === type || ct.uid === type);
|
|
68
|
+
if (match) {
|
|
69
|
+
type = match.uid;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const fallbackFrequency = 'weekly';
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
id: input.id,
|
|
77
|
+
type,
|
|
78
|
+
subPath: typeof input.subPath === 'string' ? trimSlashes(input.subPath) : '',
|
|
79
|
+
pattern: typeof input.pattern === 'string' ? input.pattern : '',
|
|
80
|
+
priority: clampPriority(toNumberOrNull(input.priority)),
|
|
81
|
+
frequency: typeof input.frequency === 'string' && input.frequency.trim() !== '' ? input.frequency : fallbackFrequency,
|
|
82
|
+
lastModified: typeof input.lastModified === 'string' ? input.lastModified : String(Boolean(input.lastModified)),
|
|
83
|
+
basePath: typeof input.basePath === 'string' ? trimSlashes(input.basePath) : '',
|
|
84
|
+
filename: typeof input.filename === 'string' ? trimSlashes(input.filename) : '',
|
|
85
|
+
createdAt: input.createdAt || null,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const ensureLeadingSlash = (value = '') => (value.startsWith('/') ? value : `/${value}`);
|
|
90
|
+
const trimSlashes = (value = '') => value.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
91
|
+
const joinUrlSegments = (segments = []) => segments.filter(Boolean).join('/');
|
|
92
|
+
|
|
93
|
+
const resolveManualSitemapPublicUrl = (baseUrl = '', sitemap = {}) => {
|
|
94
|
+
const filename = trimSlashes(sitemap?.filename || '');
|
|
95
|
+
if (!filename) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (/^https?:\/\//i.test(filename)) {
|
|
100
|
+
return filename;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const trimmedBaseUrl = baseUrl.replace(/\/+$/, '');
|
|
104
|
+
if (!trimmedBaseUrl) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const basePath = trimSlashes(sitemap?.basePath || '');
|
|
109
|
+
const path = joinUrlSegments([basePath, filename]);
|
|
110
|
+
return `${trimmedBaseUrl}${ensureLeadingSlash(encodeURI(path))}`;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const resolveCollectionSitemapPublicUrl = (baseUrl = '', config = {}) => {
|
|
114
|
+
const filename = trimSlashes(config?.filename || '');
|
|
115
|
+
if (!filename) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (/^https?:\/\//i.test(filename)) {
|
|
120
|
+
return filename;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const trimmedBaseUrl = baseUrl.replace(/\/+$/, '');
|
|
124
|
+
if (!trimmedBaseUrl) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const basePath = trimSlashes(config?.basePath || '');
|
|
129
|
+
const path = joinUrlSegments([basePath, filename]);
|
|
130
|
+
return `${trimmedBaseUrl}${ensureLeadingSlash(encodeURI(path))}`;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const extractTokens = (pattern = '') => {
|
|
134
|
+
const matches = pattern.match(/\[[^\]]+]/g) || [];
|
|
135
|
+
return matches.map((token) => token.slice(1, -1)).filter(Boolean);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const getDeepValue = (entry, path) => {
|
|
139
|
+
if (!path) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return path.split('.').reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), entry);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
module.exports = ({ strapi }) => {
|
|
147
|
+
const manualSitemapsStore = () =>
|
|
148
|
+
strapi.store({
|
|
149
|
+
type: 'plugin',
|
|
150
|
+
name: 'strapi-advanced-sitemap',
|
|
151
|
+
key: 'manual-sitemaps',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const collectionConfigUid = 'plugin::strapi-advanced-sitemap.strapi-advanced-sitemap-content-type';
|
|
155
|
+
const optionUid = 'plugin::strapi-advanced-sitemap.strapi-advanced-sitemap-option';
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {string} uid
|
|
159
|
+
*/
|
|
160
|
+
const fetchAllEntries = async (uid) => {
|
|
161
|
+
if (!uid) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const pageSize = 500;
|
|
166
|
+
let page = 1;
|
|
167
|
+
const all = [];
|
|
168
|
+
|
|
169
|
+
while (true) {
|
|
170
|
+
const schema = strapi.getModel(uid);
|
|
171
|
+
const hasDraft = schema?.options?.draftAndPublish;
|
|
172
|
+
const query = {
|
|
173
|
+
publicationState: hasDraft ? 'live' : undefined,
|
|
174
|
+
pagination: { page, pageSize },
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Always include key meta fields to compute last modified
|
|
178
|
+
query.fields = undefined;
|
|
179
|
+
|
|
180
|
+
const batch = await strapi.entityService.findMany(uid, query);
|
|
181
|
+
|
|
182
|
+
if (!Array.isArray(batch) || batch.length === 0) {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
all.push(...batch);
|
|
187
|
+
|
|
188
|
+
if (batch.length < pageSize) {
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
page += 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return all;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const resolveBaseUrl = (configured, fallback) => {
|
|
199
|
+
if (configured && /^https?:\/\//i.test(configured)) {
|
|
200
|
+
return configured.replace(/\/+$/, '');
|
|
201
|
+
}
|
|
202
|
+
return fallback.replace(/\/+$/, '');
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
async getManualSitemaps() {
|
|
207
|
+
const stored = (await manualSitemapsStore().get()) || [];
|
|
208
|
+
return stored.map(sanitizeManualSitemap);
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
async setManualSitemaps(sitemaps = []) {
|
|
212
|
+
const sanitized = Array.isArray(sitemaps) ? sitemaps.map(sanitizeManualSitemap) : [];
|
|
213
|
+
await manualSitemapsStore().set({ value: sanitized });
|
|
214
|
+
return sanitized;
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
async getCollectionConfigs() {
|
|
218
|
+
const results = await strapi.entityService.findMany(collectionConfigUid, {
|
|
219
|
+
sort: [{ createdAt: 'asc' }, { id: 'asc' }],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return Array.isArray(results) ? results.map((item) => sanitizeCollectionConfig(strapi, item)) : [];
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
async getConfiguredBaseUrl() {
|
|
226
|
+
const data = await strapi.entityService.findMany(optionUid);
|
|
227
|
+
const single = Array.isArray(data) ? data[0] : data;
|
|
228
|
+
const baseUrl = single?.baseUrl;
|
|
229
|
+
return typeof baseUrl === 'string' && baseUrl.trim() !== '' ? baseUrl.trim() : null;
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
buildManualSitemapIndex(sitemaps = [], publicBaseUrl = '') {
|
|
233
|
+
const entries = sitemaps
|
|
234
|
+
.map((sitemap) => resolveManualSitemapPublicUrl(publicBaseUrl, sitemap))
|
|
235
|
+
.filter(Boolean)
|
|
236
|
+
.map((loc) => `<sitemap><loc>${escapeXml(loc)}</loc></sitemap>`) // prettier-ignore
|
|
237
|
+
.join('');
|
|
238
|
+
|
|
239
|
+
return `${XML_HEADER}<sitemapindex xmlns="${SITEMAP_NAMESPACE}">${entries}</sitemapindex>`;
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
buildManualSitemapFile(sitemap = {}, baseUrl = '') {
|
|
243
|
+
const sanitized = sanitizeManualSitemap(sitemap);
|
|
244
|
+
|
|
245
|
+
if (sanitized.kind === 'index') {
|
|
246
|
+
const entries = (sanitized.urls || [])
|
|
247
|
+
.filter((url) => typeof url.loc === 'string' && url.loc.trim() !== '')
|
|
248
|
+
.map((url) => {
|
|
249
|
+
if (/^https?:\/\//i.test(url.loc)) {
|
|
250
|
+
return `<sitemap><loc>${escapeXml(url.loc)}</loc></sitemap>`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const trimmedBase = baseUrl.replace(/\/+$/, '');
|
|
254
|
+
const slug = trimSlashes(url.loc);
|
|
255
|
+
const absoluteUrl = slug ? `${trimmedBase}${ensureLeadingSlash(slug)}` : trimmedBase;
|
|
256
|
+
return `<sitemap><loc>${escapeXml(absoluteUrl)}</loc></sitemap>`;
|
|
257
|
+
})
|
|
258
|
+
.join('');
|
|
259
|
+
|
|
260
|
+
return `${XML_HEADER}<sitemapindex xmlns="${SITEMAP_NAMESPACE}">${entries}</sitemapindex>`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const urls = (sanitized.urls || [])
|
|
264
|
+
.filter((url) => typeof url.loc === 'string' && url.loc.trim() !== '')
|
|
265
|
+
.map((url) => {
|
|
266
|
+
if (/^https?:\/\//i.test(url.loc)) {
|
|
267
|
+
const priority = formatPriority(url.priority);
|
|
268
|
+
const priorityTag = priority ? `<priority>${escapeXml(priority)}</priority>` : '';
|
|
269
|
+
return `<url><loc>${escapeXml(url.loc)}</loc>${priorityTag}</url>`; // prettier-ignore
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const trimmedBase = baseUrl.replace(/\/+$/, '');
|
|
273
|
+
const slug = trimSlashes(url.loc);
|
|
274
|
+
|
|
275
|
+
const absoluteUrl = slug ? `${trimmedBase}${ensureLeadingSlash(slug)}` : trimmedBase;
|
|
276
|
+
const priority = formatPriority(url.priority);
|
|
277
|
+
const priorityTag = priority ? `<priority>${escapeXml(priority)}</priority>` : '';
|
|
278
|
+
return `<url><loc>${escapeXml(absoluteUrl)}</loc>${priorityTag}</url>`; // prettier-ignore
|
|
279
|
+
})
|
|
280
|
+
.join('');
|
|
281
|
+
|
|
282
|
+
return `${XML_HEADER}<urlset xmlns="${SITEMAP_NAMESPACE}" xmlns:xhtml="${XHTML_NAMESPACE}" xmlns:image="${IMAGE_NAMESPACE}">${urls}</urlset>`;
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
buildRootSitemap(manualSitemaps = [], collectionConfigs = [], { apiBaseUrl = '', publicBaseUrl = '' } = {}) {
|
|
286
|
+
const trimmedApiBase = apiBaseUrl.replace(/\/+$/, '');
|
|
287
|
+
const manualEntries = manualSitemaps
|
|
288
|
+
.map((sitemap) => resolveManualSitemapPublicUrl(publicBaseUrl, sitemap))
|
|
289
|
+
.filter(Boolean)
|
|
290
|
+
.map((loc) => `<sitemap><loc>${escapeXml(loc)}</loc></sitemap>`);
|
|
291
|
+
|
|
292
|
+
const collectionEntries = collectionConfigs
|
|
293
|
+
.map((config) => {
|
|
294
|
+
if (config.id === undefined) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const publicUrl = resolveCollectionSitemapPublicUrl(publicBaseUrl, config);
|
|
299
|
+
if (publicUrl) {
|
|
300
|
+
return `<sitemap><loc>${escapeXml(publicUrl)}</loc></sitemap>`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return `<sitemap><loc>${escapeXml(`${trimmedApiBase}/collection-sitemaps/${encodeURIComponent(String(config.id))}.xml`)}</loc></sitemap>`;
|
|
304
|
+
})
|
|
305
|
+
.filter(Boolean);
|
|
306
|
+
|
|
307
|
+
const allEntries = [...manualEntries, ...collectionEntries].join('');
|
|
308
|
+
|
|
309
|
+
return `${XML_HEADER}<sitemapindex xmlns="${SITEMAP_NAMESPACE}">${allEntries}</sitemapindex>`;
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
async buildCollectionSitemapFile(config = {}, baseUrl = '') {
|
|
313
|
+
const sanitizedConfig = sanitizeCollectionConfig(strapi, config);
|
|
314
|
+
|
|
315
|
+
if (!sanitizedConfig.type || !sanitizedConfig.pattern) {
|
|
316
|
+
return `${XML_HEADER}<urlset xmlns="${SITEMAP_NAMESPACE}" xmlns:xhtml="${XHTML_NAMESPACE}" xmlns:image="${IMAGE_NAMESPACE}"></urlset>`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const tokens = extractTokens(sanitizedConfig.pattern);
|
|
320
|
+
const entries = await fetchAllEntries(sanitizedConfig.type);
|
|
321
|
+
const trimmedBase = baseUrl.replace(/\/+$/, '');
|
|
322
|
+
|
|
323
|
+
const urls = entries
|
|
324
|
+
.map((entry) => {
|
|
325
|
+
let resolvedPath = sanitizedConfig.pattern;
|
|
326
|
+
|
|
327
|
+
for (const token of tokens) {
|
|
328
|
+
const rawValue = getDeepValue(entry, token);
|
|
329
|
+
if (rawValue === null || rawValue === undefined) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
resolvedPath = resolvedPath.replace(`[${token}]`, String(rawValue));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (resolvedPath.includes('[')) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!/^https?:\/\//i.test(resolvedPath)) {
|
|
340
|
+
const joined = joinUrlSegments([sanitizedConfig.subPath, trimSlashes(resolvedPath)]);
|
|
341
|
+
resolvedPath = `${trimmedBase}${ensureLeadingSlash(joined)}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const absoluteUrl = resolvedPath;
|
|
345
|
+
|
|
346
|
+
const priorityValue = formatPriority(sanitizedConfig.priority);
|
|
347
|
+
const priorityTag = priorityValue ? `<priority>${escapeXml(priorityValue)}</priority>` : '';
|
|
348
|
+
const changeFreqTag = sanitizedConfig.frequency ? `<changefreq>${escapeXml(sanitizedConfig.frequency)}</changefreq>` : '';
|
|
349
|
+
|
|
350
|
+
let lastModTag = '';
|
|
351
|
+
if (String(sanitizedConfig.lastModified).toLowerCase() === 'true') {
|
|
352
|
+
const lastModSource = entry.updatedAt || entry.publishedAt || entry.createdAt;
|
|
353
|
+
if (lastModSource) {
|
|
354
|
+
const iso = new Date(lastModSource).toISOString();
|
|
355
|
+
lastModTag = `<lastmod>${escapeXml(iso)}</lastmod>`;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return `<url><loc>${escapeXml(absoluteUrl)}</loc>${lastModTag}${changeFreqTag}${priorityTag}</url>`; // prettier-ignore
|
|
360
|
+
})
|
|
361
|
+
.filter(Boolean)
|
|
362
|
+
.join('');
|
|
363
|
+
|
|
364
|
+
return `${XML_HEADER}<urlset xmlns="${SITEMAP_NAMESPACE}" xmlns:xhtml="${XHTML_NAMESPACE}" xmlns:image="${IMAGE_NAMESPACE}">${urls}</urlset>`;
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
resolveBaseUrl,
|
|
368
|
+
resolveManualSitemapPublicUrl,
|
|
369
|
+
resolveCollectionSitemapPublicUrl,
|
|
370
|
+
};
|
|
371
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ACTIONS = {
|
|
4
|
+
root: 'plugin::strapi-advanced-sitemap.controller.serveRootSitemap',
|
|
5
|
+
manualIndex: 'plugin::strapi-advanced-sitemap.controller.serveManualSitemapIndex',
|
|
6
|
+
manualFile: 'plugin::strapi-advanced-sitemap.controller.serveManualSitemapFile',
|
|
7
|
+
collectionFile: 'plugin::strapi-advanced-sitemap.controller.serveCollectionSitemapFile',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const SITEMAP_PERMISSION_DEFINITIONS = [
|
|
11
|
+
{ controller: 'controller', action: 'serveRootSitemap', uid: ACTIONS.root },
|
|
12
|
+
{ controller: 'controller', action: 'serveManualSitemapIndex', uid: ACTIONS.manualIndex },
|
|
13
|
+
{ controller: 'controller', action: 'serveManualSitemapFile', uid: ACTIONS.manualFile },
|
|
14
|
+
{ controller: 'controller', action: 'serveCollectionSitemapFile', uid: ACTIONS.collectionFile },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const PATCHED_FLAG = Symbol('strapi-advanced-sitemap-permissions-patched');
|
|
18
|
+
|
|
19
|
+
const patchUsersPermissionsGetActions = (usersPermissionsService) => {
|
|
20
|
+
if (!usersPermissionsService || usersPermissionsService[PATCHED_FLAG]) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const originalGetActions = usersPermissionsService.getActions.bind(usersPermissionsService);
|
|
25
|
+
|
|
26
|
+
usersPermissionsService.getActions = (options = {}) => {
|
|
27
|
+
const result = originalGetActions(options) || {};
|
|
28
|
+
const defaultEnable = options?.defaultEnable ?? false;
|
|
29
|
+
const namespace = 'plugin::strapi-advanced-sitemap';
|
|
30
|
+
|
|
31
|
+
const currentControllers = result[namespace]?.controllers || {};
|
|
32
|
+
const patchedControllers = { ...currentControllers };
|
|
33
|
+
|
|
34
|
+
SITEMAP_PERMISSION_DEFINITIONS.forEach(({ controller, action }) => {
|
|
35
|
+
const controllerActions = patchedControllers[controller] ? { ...patchedControllers[controller] } : {};
|
|
36
|
+
if (!controllerActions[action]) {
|
|
37
|
+
controllerActions[action] = { enabled: defaultEnable, policy: '' };
|
|
38
|
+
}
|
|
39
|
+
patchedControllers[controller] = controllerActions;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
result[namespace] = {
|
|
43
|
+
controllers: patchedControllers,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
usersPermissionsService[PATCHED_FLAG] = true;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const registerSitemapActions = async () => {
|
|
53
|
+
const permissionsService = strapi.plugin('users-permissions')?.service('users-permissions');
|
|
54
|
+
|
|
55
|
+
if (!permissionsService || typeof permissionsService.syncPermissions !== 'function') {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
patchUsersPermissionsGetActions(permissionsService);
|
|
60
|
+
|
|
61
|
+
await permissionsService.syncPermissions();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const getRoleIdFromCtx = async (ctx) => {
|
|
65
|
+
if (ctx.state?.user?.role?.id) {
|
|
66
|
+
return ctx.state.user.role.id;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (ctx.state?.auth?.credentials?.role?.id) {
|
|
70
|
+
return ctx.state.auth.credentials.role.id;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (ctx.state?.auth?.strategy?.name === 'api-token') {
|
|
74
|
+
// API tokens are handled separately via the permissions array on the token
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const publicRole = await strapi.db.query('plugin::users-permissions.role').findOne({
|
|
79
|
+
where: { type: 'public' },
|
|
80
|
+
select: ['id'],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return publicRole?.id || null;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const hasApiTokenPermission = (ctx, action) => {
|
|
87
|
+
if (ctx.state?.auth?.strategy?.name !== 'api-token') {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const permissions = ctx.state?.auth?.credentials?.permissions;
|
|
92
|
+
if (!Array.isArray(permissions)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return permissions.includes(action);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const isActionEnabledForRole = (rolePermissions, action) => {
|
|
100
|
+
if (!rolePermissions) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const [namespace, controller, actionName] = action.split('.').slice(-3);
|
|
105
|
+
const controllerSet = rolePermissions?.[namespace]?.controllers?.[controller];
|
|
106
|
+
return controllerSet?.[actionName]?.enabled === true;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const ensureSitemapPermission = async (ctx, actionKey) => {
|
|
110
|
+
const action = ACTIONS[actionKey] || actionKey;
|
|
111
|
+
|
|
112
|
+
if (hasApiTokenPermission(ctx, action)) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const roleId = await getRoleIdFromCtx(ctx);
|
|
117
|
+
|
|
118
|
+
if (!roleId) {
|
|
119
|
+
ctx.unauthorized();
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const roleService = strapi.plugin('users-permissions').service('role');
|
|
124
|
+
const role = await roleService.findOne(roleId);
|
|
125
|
+
|
|
126
|
+
if (isActionEnabledForRole(role.permissions, action)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
ctx.unauthorized();
|
|
131
|
+
return false;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
ACTIONS,
|
|
136
|
+
registerSitemapActions,
|
|
137
|
+
ensureSitemapPermission,
|
|
138
|
+
};
|
|
139
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webbycrown/strapi-advanced-sitemap",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"strapi",
|
|
6
|
+
"plugin",
|
|
7
|
+
"sitemap",
|
|
8
|
+
"xml",
|
|
9
|
+
"seo",
|
|
10
|
+
"sitemap-generator"
|
|
11
|
+
],
|
|
12
|
+
"type": "commonjs",
|
|
13
|
+
"exports": {
|
|
14
|
+
"./package.json": "./package.json",
|
|
15
|
+
"./strapi-admin": {
|
|
16
|
+
"source": "./admin/src/index.js",
|
|
17
|
+
"import": "./dist/admin/index.mjs",
|
|
18
|
+
"require": "./dist/admin/index.js",
|
|
19
|
+
"default": "./dist/admin/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./strapi-server": {
|
|
22
|
+
"source": "./server/src/index.js",
|
|
23
|
+
"import": "./dist/server/index.mjs",
|
|
24
|
+
"require": "./dist/server/index.js",
|
|
25
|
+
"default": "./dist/server/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "npm run build:admin && npm run build:server",
|
|
35
|
+
"build:admin": "strapi-plugin build",
|
|
36
|
+
"build:server": "node scripts/build-server.js",
|
|
37
|
+
"watch": "strapi-plugin watch",
|
|
38
|
+
"watch:link": "strapi-plugin watch:link",
|
|
39
|
+
"verify": "strapi-plugin verify"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@strapi/design-system": "^2.0.0-rc.21",
|
|
43
|
+
"@strapi/icons": "^2.0.0-rc.23",
|
|
44
|
+
"react-intl": "^7.1.11"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@strapi/sdk-plugin": "^5.3.2",
|
|
48
|
+
"@strapi/strapi": "^5.12.5",
|
|
49
|
+
"prettier": "^3.5.3",
|
|
50
|
+
"react": "^18.3.1",
|
|
51
|
+
"react-dom": "^18.3.1",
|
|
52
|
+
"styled-components": "^6.1.17"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@strapi/sdk-plugin": "^5.3.2",
|
|
56
|
+
"@strapi/strapi": "^5.12.5",
|
|
57
|
+
"react": "^18.3.1",
|
|
58
|
+
"react-dom": "^18.3.1",
|
|
59
|
+
"styled-components": "^6.1.17"
|
|
60
|
+
},
|
|
61
|
+
"strapi": {
|
|
62
|
+
"kind": "plugin",
|
|
63
|
+
"name": "strapi-advanced-sitemap",
|
|
64
|
+
"displayName": "Strapi Advanced Sitemap",
|
|
65
|
+
"description": "Flexible sitemap tooling for Strapi: build manual or dynamic XML files, publish sitemap indexes, and control access per role."
|
|
66
|
+
},
|
|
67
|
+
"description": "Flexible sitemap tooling for Strapi: build manual or dynamic XML files, publish sitemap indexes, and control access per role.",
|
|
68
|
+
"license": "MIT",
|
|
69
|
+
"author": {
|
|
70
|
+
"name": "WebbyCrown",
|
|
71
|
+
"email": "info@webbycrown.com",
|
|
72
|
+
"url": "https://webbycrown.com"
|
|
73
|
+
},
|
|
74
|
+
"repository": {
|
|
75
|
+
"type": "git",
|
|
76
|
+
"url": "https://github.com/webbycrown/strapi-advanced-sitemap.git"
|
|
77
|
+
},
|
|
78
|
+
"engines": {
|
|
79
|
+
"node": ">=18.0.0",
|
|
80
|
+
"npm": ">=8.0.0"
|
|
81
|
+
}
|
|
82
|
+
}
|