domma-cms 1.8.8 → 1.8.10
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/CHANGELOG.md +10 -0
- package/CLAUDE.md +22 -0
- package/admin/js/lib/blocks-arrange.js +1 -1
- package/admin/js/lib/site-settings-arrange.js +1 -1
- package/admin/js/lib/wordpress-import-arrange.js +1 -1
- package/admin/js/templates/settings.html +5 -0
- package/admin/js/templates/wordpress-import.html +20 -1
- package/admin/js/views/blocks.js +4 -4
- package/admin/js/views/wordpress-import.js +23 -9
- package/docs/cache.md +2 -0
- package/docs/configuration.md +1 -0
- package/package.json +1 -1
- package/public/css/site.css +1 -1
- package/public/js/search.js +1 -1
- package/public/js/site.js +1 -1
- package/server/routes/api/blocks.js +8 -3
- package/server/routes/api/settings.js +8 -0
- package/server/routes/api/wordpress-import.js +38 -2
- package/server/routes/docs-public.js +9 -3
- package/server/routes/public.js +6 -6
- package/server/services/docs.js +11 -5
- package/server/services/renderer.js +56 -3
- package/server/services/userRoles.js +15 -0
- package/server/services/wpImport/convert.js +2 -1
- package/server/services/wpImport/index.js +167 -7
- package/server/services/wpImport/plan.js +35 -3
- package/server/services/wpImport/widgets.js +355 -0
- package/server/templates/page.html +3 -3
|
@@ -35,6 +35,13 @@
|
|
|
35
35
|
* left out; what they wrote is credited to whoever they became. A user here is
|
|
36
36
|
* only ever credited - never changed.
|
|
37
37
|
*
|
|
38
|
+
* Widgets (./widgets.js) come from a .wie file - the JSON the "Widget Importer
|
|
39
|
+
* & Exporter" WordPress plugin writes - uploaded beside the export (or inside
|
|
40
|
+
* its zip). Each widget area becomes a block (wp-<area>); a footer area's block
|
|
41
|
+
* is added to the site's footer (site.json footer.blocks, drawn on every page by
|
|
42
|
+
* services/renderer.js) - added once, never taken out, and not put back once
|
|
43
|
+
* someone removed it here.
|
|
44
|
+
*
|
|
38
45
|
* The one request hook here answers WordPress's query addresses (/?p=12,
|
|
39
46
|
* /?page_id=7, /?attachment_id=3) from the mapping; path redirects go into
|
|
40
47
|
* SEO's redirect store.
|
|
@@ -67,12 +74,19 @@ import {adminRightsGiven} from '../notification-sources.js';
|
|
|
67
74
|
import {getRoleLevel} from '../roles.js';
|
|
68
75
|
import {creditMap} from './people.js';
|
|
69
76
|
import {seoEngine} from '../seo/index.js';
|
|
70
|
-
import {hooks as coreHooks} from '../hooks.js';
|
|
77
|
+
import {getShortcodeProcessors, hooks as coreHooks} from '../hooks.js';
|
|
78
|
+
import {getBlock, listBlocks, saveBlock} from '../blocks.js';
|
|
79
|
+
import {getConfig, saveConfig} from '../../config.js';
|
|
80
|
+
import * as cache from '../cache/index.js';
|
|
81
|
+
import {getSearchSettings} from '../search.js';
|
|
82
|
+
import {readWie, widgetAreaHtml, WIE_MAX_BYTES} from './widgets.js';
|
|
83
|
+
import {FOOTER_BLOCKS_MAX} from '../renderer.js';
|
|
71
84
|
import {allowInGitignore} from '../siteGitignore.js';
|
|
72
85
|
import fsSync from 'node:fs';
|
|
73
86
|
|
|
74
87
|
export const WXR_MAX_BYTES = 50 * 1024 * 1024;
|
|
75
88
|
export const UPLOADS_ZIP_MAX_BYTES = 1024 * 1024 * 1024;
|
|
89
|
+
export {WIE_MAX_BYTES};
|
|
76
90
|
const DOWNLOAD_CONCURRENCY = 4;
|
|
77
91
|
|
|
78
92
|
const BLOG = {posts: 'blog-posts', categories: 'blog-categories', comments: 'blog-comments'};
|
|
@@ -202,6 +216,16 @@ const postFingerprint = (d) => fingerprint(d?.title, d?.content, d?.excerpt);
|
|
|
202
216
|
/** A custom type's entry: its non-empty values (a re-save that adds blank fields is not an edit). */
|
|
203
217
|
export const customFingerprint = (d) => fingerprint(entryFingerprintText(d));
|
|
204
218
|
const menuFingerprint = (items) => fingerprint(JSON.stringify(items || []));
|
|
219
|
+
/** A widget area's block: its template as written. */
|
|
220
|
+
const blockFingerprint = (content) => fingerprint(String(content ?? ''));
|
|
221
|
+
|
|
222
|
+
/** The blocks in the footer now (site.json footer.blocks), names only. */
|
|
223
|
+
export function footerBlockList(site = null) {
|
|
224
|
+
let s = site;
|
|
225
|
+
if (!s) { try { s = getConfig('site'); } catch { s = {}; } }
|
|
226
|
+
const list = s?.footer?.blocks;
|
|
227
|
+
return Array.isArray(list) ? list.filter(n => typeof n === 'string' && /^[a-z0-9][a-z0-9-]*$/.test(n)) : [];
|
|
228
|
+
}
|
|
205
229
|
|
|
206
230
|
/** The site's state as the planner needs it. */
|
|
207
231
|
export async function siteState(mapping = {}) {
|
|
@@ -252,11 +276,23 @@ export async function siteState(mapping = {}) {
|
|
|
252
276
|
for (const e of entriesOf(await listEntries(m.slug, {limit: 100000}).catch(() => []))) hashes.set(e.id, customFingerprint(e.data || {}));
|
|
253
277
|
customEntries.set(m.slug, hashes);
|
|
254
278
|
}
|
|
279
|
+
// Blocks: their names (a new one must not take one), and the templates of those an earlier run made.
|
|
280
|
+
const blockNames = new Set((await listBlocks().catch(() => [])).map(b => b.name));
|
|
281
|
+
const blockHashes = {};
|
|
282
|
+
for (const m of Object.values(mapping.items || {})) {
|
|
283
|
+
if (m?.kind !== 'widgets' || !m.block || !blockNames.has(m.block)) continue;
|
|
284
|
+
const b = await getBlock(m.block).catch(() => null);
|
|
285
|
+
if (b) blockHashes[m.block] = blockFingerprint(b.content);
|
|
286
|
+
}
|
|
287
|
+
let searchOn = false;
|
|
288
|
+
try { searchOn = getSearchSettings().enabled !== false; } catch { /* no search settings */ }
|
|
255
289
|
const seo = seoEngine();
|
|
256
290
|
// The users here - only what matching an author needs (never a password or a token).
|
|
257
291
|
const users = (await listUsers().catch(() => [])).map(u => ({id: u.id, email: String(u.email || '').toLowerCase(), name: u.name || ''}));
|
|
258
292
|
return {
|
|
259
293
|
pagePaths, pageHashes, shop, collections, customEntries, users,
|
|
294
|
+
blockNames, blockHashes, footerBlocks: footerBlockList(), searchOn,
|
|
295
|
+
shortcodes: new Set(getShortcodeProcessors().map(p => p.name)),
|
|
260
296
|
mediaNames: new Set(media.map(m => String(m.name).toLowerCase())),
|
|
261
297
|
menuSlugs: new Set(menus.map(m => m.slug)),
|
|
262
298
|
menuHashes,
|
|
@@ -284,6 +320,16 @@ export async function inspectStaged(id) {
|
|
|
284
320
|
if (!xml) throw new ZipError('There is no .xml file in that zip - export from WordPress (Tools › Export) and upload the file it gives you.');
|
|
285
321
|
if (xml.size > WXR_MAX_BYTES) throw new ZipError(`The export is ${Math.round(xml.size / 1048576)} MB - over the ${WXR_MAX_BYTES / 1048576} MB limit.`);
|
|
286
322
|
await fs.writeFile(stagingPath(id, 'export.xml'), await zip.read(xml));
|
|
323
|
+
// A widgets file packed with the export comes along, if it reads as one.
|
|
324
|
+
const wie = zip.entries.filter(e => /\.wie$/i.test(e.name) && !/(^|\/)__MACOSX\//.test(e.name) && e.size <= WIE_MAX_BYTES).sort((a, b) => b.size - a.size)[0];
|
|
325
|
+
if (wie) {
|
|
326
|
+
try {
|
|
327
|
+
const buf = await zip.read(wie);
|
|
328
|
+
const read = readWie(buf);
|
|
329
|
+
await fs.writeFile(stagingPath(id, 'widgets.wie'), buf, {mode: 0o600});
|
|
330
|
+
await updateStagingMeta(id, {widgets: widgetsMeta(String(wie.name).split('/').pop(), buf.length, read)});
|
|
331
|
+
} catch { /* not a widgets file - the export imports without it */ }
|
|
332
|
+
}
|
|
287
333
|
} finally {
|
|
288
334
|
await zip.close();
|
|
289
335
|
}
|
|
@@ -306,6 +352,22 @@ export async function loadExport(id) {
|
|
|
306
352
|
return wxr;
|
|
307
353
|
}
|
|
308
354
|
|
|
355
|
+
/** What the staging meta says about a widgets file. */
|
|
356
|
+
export function widgetsMeta(fileName, size, read) {
|
|
357
|
+
return {
|
|
358
|
+
fileName: String(fileName || 'widgets.wie').slice(0, 200).replace(/[^\w.() -]/g, '_'), size,
|
|
359
|
+
areas: read.areas.filter(a => a.widgets.length).length,
|
|
360
|
+
widgets: read.areas.reduce((n, a) => n + a.widgets.length, 0)
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** The staged widgets file, read, or null when there is none. */
|
|
365
|
+
export async function loadWidgets(id) {
|
|
366
|
+
const meta = await stagingMeta(id);
|
|
367
|
+
if (!meta?.widgets) return null;
|
|
368
|
+
return readWie(await fs.readFile(stagingPath(id, 'widgets.wie')));
|
|
369
|
+
}
|
|
370
|
+
|
|
309
371
|
export async function discardStaged(id) {
|
|
310
372
|
parsed.delete(id);
|
|
311
373
|
await dropStaging(id);
|
|
@@ -347,9 +409,10 @@ export async function previewImport(id, options = {}) {
|
|
|
347
409
|
const mapping = await mappingFor(sourceKey(wxr.site));
|
|
348
410
|
const state = await siteState(mapping);
|
|
349
411
|
const zipPaths = options.media === 'zip' ? await uploadsIndex(id) : null;
|
|
412
|
+
const wie = await loadWidgets(id);
|
|
350
413
|
// `people` (what the person may do with users) comes from the route, never from the request body.
|
|
351
|
-
const plan = planImport(wxr, state, mapping, {...cleanOptions(options), people: options.people || null, zipPaths, anyPort: testHostsAllowed()});
|
|
352
|
-
return {plan, state, wxr, mapping, meta};
|
|
414
|
+
const plan = planImport(wxr, state, mapping, {...cleanOptions(options), people: options.people || null, zipPaths, wie, anyPort: testHostsAllowed()});
|
|
415
|
+
return {plan, state, wxr, mapping, meta, wie};
|
|
353
416
|
}
|
|
354
417
|
|
|
355
418
|
/** Options from a request - whitelisted. */
|
|
@@ -368,6 +431,7 @@ export function cleanOptions(o = {}) {
|
|
|
368
431
|
products: o.products !== false,
|
|
369
432
|
types: cleanTypeChoices(o.types, ['collection', 'pages', 'skip']),
|
|
370
433
|
listPages: cleanTypeChoices(o.listPages, [false]),
|
|
434
|
+
footer: cleanAreaChoices(o.footer),
|
|
371
435
|
choices,
|
|
372
436
|
users: cleanUserChoices(o.users),
|
|
373
437
|
userRole: typeof o.userRole === 'string' && /^[a-z0-9][a-z0-9_-]{0,59}$/i.test(o.userRole) ? o.userRole : '',
|
|
@@ -383,6 +447,14 @@ function cleanUserChoices(map) {
|
|
|
383
447
|
return out;
|
|
384
448
|
}
|
|
385
449
|
|
|
450
|
+
/** {widgetAreaId: false} - a footer widget area NOT placed in the footer (placing is the default). */
|
|
451
|
+
function cleanAreaChoices(map) {
|
|
452
|
+
const out = {};
|
|
453
|
+
if (!map || typeof map !== 'object') return out;
|
|
454
|
+
for (const [k, v] of Object.entries(map).slice(0, 100)) if (/^[A-Za-z0-9_-]{1,100}$/.test(k) && v === false) out[k] = false;
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
|
|
386
458
|
/** {postType: choice} - only real post type names, only the allowed values. */
|
|
387
459
|
function cleanTypeChoices(map, allowed) {
|
|
388
460
|
const out = {};
|
|
@@ -438,7 +510,7 @@ export async function startRun(id, options, user, log = console) {
|
|
|
438
510
|
return job;
|
|
439
511
|
}
|
|
440
512
|
|
|
441
|
-
async function runJob(job, {plan, wxr, mapping: oldMapping, meta, inviteOrigin}, user, log) {
|
|
513
|
+
async function runJob(job, {plan, wxr, mapping: oldMapping, meta, inviteOrigin, wie}, user, log) {
|
|
442
514
|
const started = Date.now();
|
|
443
515
|
const mapping = {
|
|
444
516
|
items: {...(oldMapping.items || {})},
|
|
@@ -446,7 +518,7 @@ async function runJob(job, {plan, wxr, mapping: oldMapping, meta, inviteOrigin},
|
|
|
446
518
|
query: {p: {...oldMapping.query?.p}, page_id: {...oldMapping.query?.page_id}, attachment_id: {...oldMapping.query?.attachment_id}}
|
|
447
519
|
};
|
|
448
520
|
try {
|
|
449
|
-
await runPhases(job, {plan, wxr, oldMapping, meta, inviteOrigin}, user, log, mapping, started);
|
|
521
|
+
await runPhases(job, {plan, wxr, oldMapping, meta, inviteOrigin, wie}, user, log, mapping, started);
|
|
450
522
|
} catch (err) {
|
|
451
523
|
// What was written before the failure is recorded, so a retry updates it rather than duplicating it.
|
|
452
524
|
await saveMapping(plan.source, mapping).catch(() => {});
|
|
@@ -455,7 +527,7 @@ async function runJob(job, {plan, wxr, mapping: oldMapping, meta, inviteOrigin},
|
|
|
455
527
|
}
|
|
456
528
|
}
|
|
457
529
|
|
|
458
|
-
async function runPhases(job, {plan, wxr, oldMapping, meta, inviteOrigin}, user, log, mapping, started) {
|
|
530
|
+
async function runPhases(job, {plan, wxr, oldMapping, meta, inviteOrigin, wie}, user, log, mapping, started) {
|
|
459
531
|
const byId = new Map(wxr.items.map(i => [i.id, i]));
|
|
460
532
|
const rows = new Map(plan.items.map(r => [r.key, r]));
|
|
461
533
|
const results = new Map(); // key -> {result, error?, target?, notes?}
|
|
@@ -833,6 +905,93 @@ async function runPhases(job, {plan, wxr, oldMapping, meta, inviteOrigin}, user,
|
|
|
833
905
|
await step(r.title);
|
|
834
906
|
}
|
|
835
907
|
|
|
908
|
+
await checkpoint(true);
|
|
909
|
+
// ---- 7b. Widgets: one block per widget area; footer areas join the footer ----
|
|
910
|
+
const widgetRows = plan.items.filter(x => x.kind === 'widgets');
|
|
911
|
+
if (widgetRows.length && wie) {
|
|
912
|
+
job.phase = 'Widgets';
|
|
913
|
+
const areas = new Map(wie.areas.map(a => [a.id, a]));
|
|
914
|
+
// The menus as this run left them: a WordPress menu's term id -> the menu made from it.
|
|
915
|
+
const menuSlugs = new Map();
|
|
916
|
+
for (const m of wxr.menus) {
|
|
917
|
+
const made = mapping.items[`menu:${m.slug}`];
|
|
918
|
+
const ok = ['created', 'updated', 'kept'].includes(results.get(`menu:${m.slug}`)?.result);
|
|
919
|
+
if (m.id && made?.slug && ok) menuSlugs.set(m.id, made.slug);
|
|
920
|
+
}
|
|
921
|
+
const mediaUrl = (aid, url) => (aid && mediaById.get(aid)?.url) || (url && mediaFor(url)) || '';
|
|
922
|
+
const blog = await blogTarget();
|
|
923
|
+
const shortcodes = new Set(getShortcodeProcessors().map(p => p.name));
|
|
924
|
+
let searchOn = false;
|
|
925
|
+
try { searchOn = getSearchSettings().enabled !== false; } catch { /* no search settings */ }
|
|
926
|
+
const wctx = {
|
|
927
|
+
convert: {mediaFor, mediaById: (aid) => mediaById.get(aid) || null, linkFor, attachmentsOf},
|
|
928
|
+
mediaUrl,
|
|
929
|
+
hasMedia: (aid, url) => Boolean(mediaUrl(aid, url)),
|
|
930
|
+
menuSlug: (termId) => menuSlugs.get(termId) || '',
|
|
931
|
+
blog: {posts: blogMode && blog.available, list: shortcodes.has('blog-list'), categories: shortcodes.has('blog-categories'), basePath: plan.blogBase},
|
|
932
|
+
search: searchOn
|
|
933
|
+
};
|
|
934
|
+
const place = [];
|
|
935
|
+
for (const r of widgetRows) {
|
|
936
|
+
const was = oldMapping.items[r.key];
|
|
937
|
+
if (!works(r)) {
|
|
938
|
+
if (r.action === 'keep') {
|
|
939
|
+
mapping.items[r.key] = {...was};
|
|
940
|
+
if (r.place === 'add') place.push(r);
|
|
941
|
+
else if (r.place === 'there') mapping.items[r.key].placed = true;
|
|
942
|
+
}
|
|
943
|
+
set(r.key, {result: r.action === 'keep' ? 'kept' : 'skipped', target: r.target, place: r.action === 'keep' ? r.place : ''});
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
const area = areas.get(r.area);
|
|
948
|
+
if (!area) throw new Error('It is not in the widgets file any more.');
|
|
949
|
+
const {html, notes, rebuild} = widgetAreaHtml(area, wctx);
|
|
950
|
+
let existing = null;
|
|
951
|
+
try { existing = await getBlock(r.target); } catch { /* not there */ }
|
|
952
|
+
if (existing && r.action !== 'update') throw new Error(`A block called ${r.target} was made while the import ran.`);
|
|
953
|
+
await saveBlock(r.target, html, {meta: {...(existing?.meta || {}), importedFrom: {source: 'wordpress', site: plan.source, widgetArea: r.area}}});
|
|
954
|
+
mapping.items[r.key] = {kind: 'widgets', block: r.target, area: r.area, hash: blockFingerprint(html), ...(was?.placed && {placed: true}), at: new Date().toISOString()};
|
|
955
|
+
if (r.place === 'add') place.push(r);
|
|
956
|
+
else if (r.place === 'there') mapping.items[r.key].placed = true;
|
|
957
|
+
set(r.key, {result: r.action === 'update' ? 'updated' : 'created', target: r.target, notes, place: r.place,
|
|
958
|
+
needs: rebuild.length ? `Rebuild: ${rebuild.join('; ')}` : ''});
|
|
959
|
+
} catch (err) {
|
|
960
|
+
set(r.key, {result: 'failed', error: err.message || String(err)});
|
|
961
|
+
}
|
|
962
|
+
await step(r.title);
|
|
963
|
+
}
|
|
964
|
+
// The footer: added after whatever is there, in the widgets file's order - nothing is ever taken out.
|
|
965
|
+
if (place.length) {
|
|
966
|
+
try {
|
|
967
|
+
const site = getConfig('site');
|
|
968
|
+
const now = footerBlockList(site);
|
|
969
|
+
const fresh = place.filter(r => !now.includes(r.target));
|
|
970
|
+
// The footer draws at most FOOTER_BLOCKS_MAX (and Site Settings refuses more): the rest stay blocks only.
|
|
971
|
+
const room = Math.max(0, FOOTER_BLOCKS_MAX - now.length);
|
|
972
|
+
const added = fresh.slice(0, room);
|
|
973
|
+
for (const r of fresh.slice(room)) {
|
|
974
|
+
const res = results.get(r.key);
|
|
975
|
+
if (res) { res.place = 'full'; (res.notes ||= []).push(`The footer already has ${FOOTER_BLOCKS_MAX} blocks, so this one was not added - make room in Site Settings › Footer and add ${r.target} there.`); }
|
|
976
|
+
}
|
|
977
|
+
if (added.length) {
|
|
978
|
+
saveConfig('site', {...site, footer: {...(site.footer || {}), blocks: [...now, ...added.map(r => r.target)]}});
|
|
979
|
+
await cache.invalidateTags(['site']);
|
|
980
|
+
}
|
|
981
|
+
for (const r of place.filter(x => !fresh.slice(room).includes(x))) {
|
|
982
|
+
mapping.items[r.key] = {...mapping.items[r.key], placed: true};
|
|
983
|
+
const res = results.get(r.key);
|
|
984
|
+
if (res) res.placed = true;
|
|
985
|
+
}
|
|
986
|
+
} catch (err) {
|
|
987
|
+
for (const r of place) {
|
|
988
|
+
const res = results.get(r.key);
|
|
989
|
+
if (res) (res.notes ||= []).push(`It was not added to the footer: ${err.message || err}`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
836
995
|
await checkpoint(true);
|
|
837
996
|
// ---- 8. Redirects ----
|
|
838
997
|
job.phase = 'Redirects';
|
|
@@ -1195,7 +1354,8 @@ function buildReport({job, plan, results, redirectResults, meta, user, started,
|
|
|
1195
1354
|
return {
|
|
1196
1355
|
key: r.key, kind: r.kind, title: r.title, wpId: r.wpId, oldUrl: r.oldUrl || '', target: res.target || r.target || '',
|
|
1197
1356
|
planned: r.action, result: res.result, reason: res.error || r.reason || '',
|
|
1198
|
-
shortcodes: r.shortcodes || [], builder: r.builder || '', needs: r.needs
|
|
1357
|
+
shortcodes: r.shortcodes || [], builder: r.builder || '', needs: res.needs ?? r.needs ?? '', notes: res.notes || [], format: res.format || '',
|
|
1358
|
+
...(r.kind === 'widgets' && {area: r.area, footer: Boolean(r.footer), place: res.place || r.place || '', placed: Boolean(res.placed)}),
|
|
1199
1359
|
...(res.entryId && {entryId: res.entryId}),
|
|
1200
1360
|
...((res.collection || r.collection) && {collection: res.collection || r.collection}),
|
|
1201
1361
|
...(r.postType && {postType: r.postType})
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
* at their old address (default), pages, or nothing; WordPress's plumbing is
|
|
22
22
|
* listed with the reason it is not imported (summary.customTypes / internalTypes).
|
|
23
23
|
* The authors (./people.js) are matched to users here by email, or made into
|
|
24
|
-
* new accounts, or left out - plan.people.
|
|
24
|
+
* new accounts, or left out - plan.people. Widgets, from a .wie file uploaded
|
|
25
|
+
* beside the export (./widgets.js), become one block per widget area, and a
|
|
26
|
+
* footer area's block joins the footer (summary.widgets).
|
|
25
27
|
*
|
|
26
28
|
* @module services/wpImport/plan
|
|
27
29
|
*/
|
|
@@ -31,6 +33,7 @@ import {FETCH_PORTS, portOf} from './fetch.js';
|
|
|
31
33
|
import {readWooProducts, shopProductInput, wooPageRole} from './woo.js';
|
|
32
34
|
import {customFields, customOldPath, customTypesOf, mediaContext, rewriteSlugOf, typeLabel} from './custom.js';
|
|
33
35
|
import {planPeople} from './people.js';
|
|
36
|
+
import {planWidgets} from './widgets.js';
|
|
34
37
|
|
|
35
38
|
/** Addresses under the Shop's that are its own pages, never a product's. */
|
|
36
39
|
const SHOP_RESERVED = ['cart', 'checkout', 'basket', 'account', 'order'];
|
|
@@ -191,6 +194,11 @@ export function isoDate(gmt, local) {
|
|
|
191
194
|
* @param {{id: string, email: string, name: string}[]} [site.users] - the users here (authors are matched by email)
|
|
192
195
|
* @param {boolean} site.redirectsAvailable - SEO is running (its redirects take the old addresses)
|
|
193
196
|
* @param {Set<string>} site.redirectFroms - addresses that already redirect
|
|
197
|
+
* @param {Set<string>} [site.blockNames] - the blocks here
|
|
198
|
+
* @param {Object<string,string>} [site.blockHashes] - block -> fingerprint now (blocks an earlier run made)
|
|
199
|
+
* @param {string[]} [site.footerBlocks] - the blocks in the footer now (site.json footer.blocks)
|
|
200
|
+
* @param {Set<string>} [site.shortcodes] - shortcodes registered here (the Blog's [blog-list]…)
|
|
201
|
+
* @param {boolean} [site.searchOn] - site search is switched on
|
|
194
202
|
* @param {object} [mapping] - what earlier runs of this import made ({items: {key: {...}}})
|
|
195
203
|
* @param {object} [options]
|
|
196
204
|
* @param {'blog'|'pages'|'skip'} [options.posts]
|
|
@@ -209,13 +217,15 @@ export function isoDate(gmt, local) {
|
|
|
209
217
|
* @param {string} [options.userRole] - the role for new accounts
|
|
210
218
|
* @param {boolean} [options.invite] - email new accounts a link to set their password
|
|
211
219
|
* @param {object} [options.people] - what the person running it may do with users (never from a request)
|
|
220
|
+
* @param {object|null} [options.wie] - the widgets file, from readWie() (./widgets.js)
|
|
221
|
+
* @param {Object<string,false>} [options.footer] - per widget area: false = not placed in the footer
|
|
212
222
|
* @returns {object}
|
|
213
223
|
*/
|
|
214
224
|
export function planImport(wxr, site, mapping = {}, options = {}) {
|
|
215
225
|
const opt = {
|
|
216
226
|
posts: site.blog?.available ? 'blog' : 'pages',
|
|
217
227
|
media: 'download', comments: true, menus: true, redirects: true, drafts: true, products: true, choices: {}, zipPaths: null,
|
|
218
|
-
types: {}, listPages: {}, users: {}, userRole: '', invite: false,
|
|
228
|
+
types: {}, listPages: {}, users: {}, userRole: '', invite: false, footer: {}, wie: null,
|
|
219
229
|
...options
|
|
220
230
|
};
|
|
221
231
|
opt.types ||= {};
|
|
@@ -505,6 +515,27 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
|
|
|
505
515
|
}
|
|
506
516
|
}
|
|
507
517
|
|
|
518
|
+
// ---- Widgets: one block per widget area (./widgets.js) ----
|
|
519
|
+
const menuSlugOf = new Map(menuRows.filter(r => r.action !== 'skip' && r.wpId).map(r => [r.wpId, r.target]));
|
|
520
|
+
const mediaIds = new Set(items.filter(r => r.kind === 'media' && r.action !== 'skip').map(r => r.wpId));
|
|
521
|
+
const widgetPlan = planWidgets(opt.wie || null, {
|
|
522
|
+
mapped, choices, footer: opt.footer || {},
|
|
523
|
+
blockNames: site.blockNames || new Set(), blockHashes: site.blockHashes || {}, footerBlocks: site.footerBlocks || [],
|
|
524
|
+
menuSlug: (termId) => menuSlugOf.get(termId) || '',
|
|
525
|
+
hasMedia: (id, url) => {
|
|
526
|
+
if (id && mediaIds.has(id)) return true;
|
|
527
|
+
const rel = uploadsPath(url).toLowerCase();
|
|
528
|
+
return Boolean(rel && (mediaByRel[rel] || mediaByRel[originalUploadsPath(rel)]));
|
|
529
|
+
},
|
|
530
|
+
blog: {posts: opt.posts === 'blog', list: Boolean(site.shortcodes?.has('blog-list')), categories: Boolean(site.shortcodes?.has('blog-categories')), basePath: blogBase},
|
|
531
|
+
search: site.searchOn === true
|
|
532
|
+
});
|
|
533
|
+
for (const r of widgetPlan.rows) push(r);
|
|
534
|
+
const sidebars = widgetPlan.rows.filter(r => !r.footer && r.action !== 'skip' && r.target);
|
|
535
|
+
if (sidebars.length) {
|
|
536
|
+
notes.push(`Widget areas that are not a footer (${sidebars.map(r => r.target).join(', ')}) become blocks only - nothing here shows them by itself. Put one on a page with [block template="${sidebars[0].target}" /]; Content › Blocks lists them.`);
|
|
537
|
+
}
|
|
538
|
+
|
|
508
539
|
// ---- Comments (with the Blog) ----
|
|
509
540
|
let comments = 0;
|
|
510
541
|
if (opt.posts === 'blog' && opt.comments && site.blog?.comments !== false) {
|
|
@@ -641,6 +672,7 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
|
|
|
641
672
|
customTypes,
|
|
642
673
|
internalTypes,
|
|
643
674
|
people: people.counts,
|
|
675
|
+
widgets: widgetPlan.summary,
|
|
644
676
|
products: {
|
|
645
677
|
total: wooProducts.length,
|
|
646
678
|
types: productTypes,
|
|
@@ -656,7 +688,7 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
|
|
|
656
688
|
return {
|
|
657
689
|
source: sourceKey(wxr.site),
|
|
658
690
|
site: {title: wxr.site.title, url: wxr.site.baseSiteUrl || wxr.site.link, wxrVersion: wxr.site.wxrVersion, hosts: wxr.site.hosts},
|
|
659
|
-
options: {...opt, zipPaths: undefined, anyPort: undefined, people: undefined, inviteOrigin: undefined, invite: people.invite, choices},
|
|
691
|
+
options: {...opt, zipPaths: undefined, anyPort: undefined, people: undefined, inviteOrigin: undefined, wie: undefined, invite: people.invite, choices},
|
|
660
692
|
blogBase,
|
|
661
693
|
shopBase,
|
|
662
694
|
items,
|