@jwilleke/ngdpbase-types 5.43.0 → 5.44.0
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/lib/src/context/ApiContext.d.ts +10 -3
- package/lib/src/context/ApiContext.js +36 -3
- package/lib/src/ice/runExport.d.ts +8 -3
- package/lib/src/ice/searchExportSource.d.ts +44 -0
- package/lib/src/ice/types.d.ts +6 -0
- package/lib/src/managers/AttachmentManager.d.ts +11 -0
- package/lib/src/managers/AuditManager.d.ts +0 -1
- package/lib/src/managers/AuthManager.d.ts +38 -0
- package/lib/src/managers/AuthManager.js +84 -12
- package/lib/src/managers/BackupManager.d.ts +9 -2
- package/lib/src/managers/BackupManager.js +42 -19
- package/lib/src/managers/BaseManager.d.ts +17 -3
- package/lib/src/managers/BaseManager.js +12 -8
- package/lib/src/managers/ConfigurationManager.d.ts +13 -0
- package/lib/src/managers/ConsentManager.d.ts +1 -1
- package/lib/src/managers/DatabaseManager.js +17 -8
- package/lib/src/managers/EmailManager.d.ts +1 -1
- package/lib/src/managers/ExportManager.d.ts +1 -44
- package/lib/src/managers/ExportManager.js +3 -115
- package/lib/src/managers/ImportManager.d.ts +7 -0
- package/lib/src/managers/PageManager.d.ts +9 -0
- package/lib/src/managers/PolicyEvaluator.d.ts +113 -30
- package/lib/src/managers/ThemeManager.d.ts +20 -1
- package/lib/src/providers/FileLoggingProvider.d.ts +12 -1
- package/lib/src/providers/FileLoggingProvider.js +37 -4
- package/lib/src/security/PolicyDecisionPoint.d.ts +50 -3
- package/lib/src/security/PolicyInformationPoint.d.ts +27 -1
- package/lib/src/security/ownedRecords.d.ts +69 -0
- package/lib/src/security/signedOutWrites.d.ts +69 -0
- package/lib/src/security/signedOutWrites.js +146 -0
- package/lib/src/types/Policy.d.ts +26 -30
- package/lib/src/utils/addonConfigLayer.d.ts +6 -3
- package/lib/src/utils/addonConfigLayer.js +61 -3
- package/lib/src/utils/addonHost.d.ts +25 -0
- package/lib/src/utils/addonHost.js +29 -1
- package/lib/src/utils/availability.d.ts +15 -0
- package/lib/src/utils/availability.js +88 -0
- package/lib/src/utils/pageTitleConflict.d.ts +32 -0
- package/lib/src/utils/pageWritePolicies.d.ts +48 -29
- package/lib/src/utils/plainAttention.d.ts +19 -0
- package/lib/src/utils/plainAttention.js +18 -0
- package/lib/src/utils/policyAttributes.d.ts +113 -22
- package/lib/src/utils/policyAttributes.js +228 -43
- package/lib/src/utils/roleGrants.d.ts +1 -1
- package/package.json +7 -7
|
@@ -282,7 +282,21 @@ export interface ManagerStatus {
|
|
|
282
282
|
configKey?: string;
|
|
283
283
|
/** Whether the feature is down or works but needs an admin (#1946). Present when degraded or failed. */
|
|
284
284
|
kind?: DegradedKind;
|
|
285
|
+
/** Where the fix is, when it is not the setting `configKey` names (a record the app writes, not a value to edit). */
|
|
286
|
+
fixUrl?: string;
|
|
287
|
+
/** What the link to `fixUrl` says, e.g. "Open Admin → Backup". */
|
|
288
|
+
fixLabel?: string;
|
|
289
|
+
/** One line per problem, drawn as a list under `reason` (#2348). `reason` stays the headline and the audit text. */
|
|
290
|
+
details?: string[];
|
|
291
|
+
/** Buttons under the item (#2355): each posts a form to `action` with `fields`; the page adds the CSRF token. Labels and values are text. */
|
|
292
|
+
buttons?: Array<{
|
|
293
|
+
label: string;
|
|
294
|
+
action: string;
|
|
295
|
+
fields: Record<string, string>;
|
|
296
|
+
}>;
|
|
285
297
|
}
|
|
298
|
+
/** What a caller may add to a needs-attention status (#2348). */
|
|
299
|
+
export type AttentionOptions = Pick<ManagerStatus, 'fixUrl' | 'fixLabel' | 'details' | 'buttons'>;
|
|
286
300
|
declare abstract class BaseManager {
|
|
287
301
|
/** Reference to the wiki engine */
|
|
288
302
|
protected engine: WikiEngine;
|
|
@@ -425,18 +439,18 @@ declare abstract class BaseManager {
|
|
|
425
439
|
* starts degraded and stays degraded must not re-emit on every boot, or
|
|
426
440
|
* the signal is buried in its own noise.
|
|
427
441
|
*/
|
|
428
|
-
protected markDegraded(reason: string, configKey?: string): boolean;
|
|
442
|
+
protected markDegraded(reason: string, configKey?: string, opts?: AttentionOptions): boolean;
|
|
429
443
|
/**
|
|
430
444
|
* Record that this manager works, but an administrator has something to do
|
|
431
445
|
* (#1946). Still `degraded` — it stays on the dashboard, is audited, and
|
|
432
446
|
* clears with `markReadyIfDegradedBy` — but shown as needing attention, not
|
|
433
447
|
* as down.
|
|
434
448
|
*/
|
|
435
|
-
protected markNeedsAttention(reason: string, configKey?: string): boolean;
|
|
449
|
+
protected markNeedsAttention(reason: string, configKey?: string, opts?: AttentionOptions): boolean;
|
|
436
450
|
/** Record that this manager is deliberately off. Not a problem (#1155). */
|
|
437
451
|
protected markDisabled(reason: string): boolean;
|
|
438
452
|
/** Record that this manager could not initialise at all (#1155). */
|
|
439
|
-
protected markFailed(reason: string, configKey?: string): boolean;
|
|
453
|
+
protected markFailed(reason: string, configKey?: string, opts?: AttentionOptions): boolean;
|
|
440
454
|
/**
|
|
441
455
|
* Record that this manager is working.
|
|
442
456
|
*
|
|
@@ -18,6 +18,7 @@ import logger from '../utils/logger.js';
|
|
|
18
18
|
import { checkConfiguredPath } from '../utils/PathPreflight.js';
|
|
19
19
|
import { recordAuditEvent } from '../utils/auditEvents.js';
|
|
20
20
|
import { AUDIT_EVENT } from '../utils/auditEventNames.js';
|
|
21
|
+
import { STAGED_RESTORE_FIX, stagedRestoreNotApplied, UNUSABLE_FOLDER_REASON } from '../utils/plainAttention.js';
|
|
21
22
|
class BaseManager {
|
|
22
23
|
/** Reference to the wiki engine */
|
|
23
24
|
engine;
|
|
@@ -121,7 +122,9 @@ class BaseManager {
|
|
|
121
122
|
const { applyStagedPartsAtStart } = await import('./BackupManager.js');
|
|
122
123
|
this.stagedRestore = applyStagedPartsAtStart(this.engine, this.registeredName(), declared);
|
|
123
124
|
if (this.stagedRestore?.refused) {
|
|
124
|
-
|
|
125
|
+
// #2354: the dashboard says it in plain words; the parts and the cause stay in the log.
|
|
126
|
+
logger.warn(`[${this.constructor.name}] A staged restore of ${declared.map((d) => d.part).join(', ')} was not applied: ${this.stagedRestore.refused}`);
|
|
127
|
+
this.markNeedsAttention(stagedRestoreNotApplied('some of the site\'s data'), undefined, STAGED_RESTORE_FIX);
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
130
|
/**
|
|
@@ -193,7 +196,8 @@ class BaseManager {
|
|
|
193
196
|
// degrading manager already goes through, so setting the state here is
|
|
194
197
|
// what makes twelve invisible degradations visible without each caller
|
|
195
198
|
// remembering to say so.
|
|
196
|
-
|
|
199
|
+
// #2354: the dashboard says it in plain words; the path and key are in the line above.
|
|
200
|
+
this.markDegraded(UNUSABLE_FOLDER_REASON, configKey);
|
|
197
201
|
}
|
|
198
202
|
return result;
|
|
199
203
|
}
|
|
@@ -216,8 +220,8 @@ class BaseManager {
|
|
|
216
220
|
* starts degraded and stays degraded must not re-emit on every boot, or
|
|
217
221
|
* the signal is buried in its own noise.
|
|
218
222
|
*/
|
|
219
|
-
markDegraded(reason, configKey) {
|
|
220
|
-
return this.transition({ state: 'degraded', reason, configKey, kind: 'outage' });
|
|
223
|
+
markDegraded(reason, configKey, opts = {}) {
|
|
224
|
+
return this.transition({ state: 'degraded', reason, configKey, kind: 'outage', ...opts });
|
|
221
225
|
}
|
|
222
226
|
/**
|
|
223
227
|
* Record that this manager works, but an administrator has something to do
|
|
@@ -225,16 +229,16 @@ class BaseManager {
|
|
|
225
229
|
* clears with `markReadyIfDegradedBy` — but shown as needing attention, not
|
|
226
230
|
* as down.
|
|
227
231
|
*/
|
|
228
|
-
markNeedsAttention(reason, configKey) {
|
|
229
|
-
return this.transition({ state: 'degraded', reason, configKey, kind: 'attention' });
|
|
232
|
+
markNeedsAttention(reason, configKey, opts = {}) {
|
|
233
|
+
return this.transition({ state: 'degraded', reason, configKey, kind: 'attention', ...opts });
|
|
230
234
|
}
|
|
231
235
|
/** Record that this manager is deliberately off. Not a problem (#1155). */
|
|
232
236
|
markDisabled(reason) {
|
|
233
237
|
return this.transition({ state: 'disabled', reason });
|
|
234
238
|
}
|
|
235
239
|
/** Record that this manager could not initialise at all (#1155). */
|
|
236
|
-
markFailed(reason, configKey) {
|
|
237
|
-
return this.transition({ state: 'failed', reason, configKey, kind: 'outage' });
|
|
240
|
+
markFailed(reason, configKey, opts = {}) {
|
|
241
|
+
return this.transition({ state: 'failed', reason, configKey, kind: 'outage', ...opts });
|
|
238
242
|
}
|
|
239
243
|
/**
|
|
240
244
|
* Record that this manager is working.
|
|
@@ -54,6 +54,8 @@ declare class ConfigurationManager extends BaseManager {
|
|
|
54
54
|
private defaultConfig;
|
|
55
55
|
private customConfig;
|
|
56
56
|
private mergedConfig;
|
|
57
|
+
/** #2279: counts every change to `mergedConfig`, so a reader may keep what it derived from a value until the next one. */
|
|
58
|
+
private revision;
|
|
57
59
|
/** #1638: shipped ⊕ enabled addons, before the operator's file — what the build declares. */
|
|
58
60
|
private declaredConfig;
|
|
59
61
|
/** #1813: each enabled addon's own default-config.json, as folded into the merge. */
|
|
@@ -200,8 +202,19 @@ declare class ConfigurationManager extends BaseManager {
|
|
|
200
202
|
* @throws {Error} If default configuration file cannot be loaded
|
|
201
203
|
*/
|
|
202
204
|
private loadConfigurations;
|
|
205
|
+
/**
|
|
206
|
+
* A number that changes whenever the configuration in force changes (#2279):
|
|
207
|
+
* a load or reload, a save, a reset, a restore, a runtime value an add-on
|
|
208
|
+
* sets. Equal numbers mean nothing was changed through this manager, so
|
|
209
|
+
* what a reader derived from `getProperty` then still holds. A value
|
|
210
|
+
* returned by `getProperty` must not be changed in place; that is the one
|
|
211
|
+
* change this cannot see.
|
|
212
|
+
*/
|
|
213
|
+
getRevision(): number;
|
|
203
214
|
/** What the #2255 page-write migration rewrote in memory, one sentence each; PolicyDecisionPoint reports it. */
|
|
204
215
|
pageWritePolicyMigration: string[];
|
|
216
|
+
/** Each `pageWritePolicyMigration` line in plain words, for the needs-attention entry. */
|
|
217
|
+
pageWritePolicyMigrationShown: Record<string, string>;
|
|
205
218
|
/**
|
|
206
219
|
* #2255: before the merge, rewrite the custom layer's stored copies of the
|
|
207
220
|
* shipped role policies whose page writes moved to the category policies
|
|
@@ -56,7 +56,7 @@ declare class ConsentManager extends BaseManager {
|
|
|
56
56
|
private readonly addons;
|
|
57
57
|
/** Username → their records, read once and dropped on every write. */
|
|
58
58
|
private readonly cache;
|
|
59
|
-
/** Document id → why it cannot be asked for
|
|
59
|
+
/** Document id → why it cannot be asked for (the needs-attention reason is made from these), and whether it comes with an add-on (core's Required Pages Sync does not restore those). */
|
|
60
60
|
private readonly problems;
|
|
61
61
|
private attentionReason;
|
|
62
62
|
constructor(engine: WikiEngine);
|
|
@@ -50,6 +50,7 @@ import { applyStagedRestore, manifestPath, runOffThread, setAsideStagedRestore,
|
|
|
50
50
|
import { BACKUP_KEY_ENV, backupKeyFingerprint, currentBackupKey, LEFT_OUT_FOR_NO_KEY } from '../utils/backupKey.js';
|
|
51
51
|
// The environment variable holding the SQLCipher key: one declaration, `src/utils/databaseKey.ts`.
|
|
52
52
|
import { DATABASE_KEY_ENV } from '../utils/databaseKey.js';
|
|
53
|
+
import { STAGED_RESTORE_FIX } from '../utils/plainAttention.js';
|
|
53
54
|
export { DATABASE_KEY_ENV };
|
|
54
55
|
// #1723, #1876: the backup key has one declaration (`src/utils/backupKey.ts`); re-exported for the callers that import it from here.
|
|
55
56
|
export { BACKUP_KEY_ENV, backupKeyFingerprint } from '../utils/backupKey.js';
|
|
@@ -138,7 +139,7 @@ class DatabaseManager extends BaseManager {
|
|
|
138
139
|
catch (err) {
|
|
139
140
|
this.markUnavailable(`A staged database restore could not be put in place: ${err.message}. ` +
|
|
140
141
|
'No database was opened. Some databases may already be restored: each one moved is kept beside it as *.pre-restore, and the rest are still *.staged beside ' +
|
|
141
|
-
`${path.basename(manifestPath(file))}. Stop the server, put the files back as they should be, and restart (#1892)
|
|
142
|
+
`${path.basename(manifestPath(file))}. Stop the server, put the files back as they should be, and restart (#1892)`, 'A saved copy of the databases was being put back and it did not finish, so the site is in maintenance mode and no database is open. Some may already have been replaced. Ask whoever runs the server to check the database files and restart the site; this is done on the server, outside this page.');
|
|
142
143
|
return;
|
|
143
144
|
}
|
|
144
145
|
// #1892: a database that cannot be opened safely (no key or the wrong key
|
|
@@ -151,7 +152,7 @@ class DatabaseManager extends BaseManager {
|
|
|
151
152
|
provider = new SqliteDatabaseProvider(file, key, DATABASE_MIGRATIONS, 'schema_migrations', { busyTimeoutMs: this.busyTimeoutMs });
|
|
152
153
|
}
|
|
153
154
|
catch (err) {
|
|
154
|
-
this.markUnavailable(DatabaseManager.openFailure(file, err.message, key));
|
|
155
|
+
this.markUnavailable(DatabaseManager.openFailure(file, err.message, key), 'The database could not be opened safely, so the site is in maintenance mode. If its key is missing or wrong, fix it in the server\'s settings (outside this page) and restart the site. If the file is damaged, restore the databases from a backup.', STAGED_RESTORE_FIX);
|
|
155
156
|
return;
|
|
156
157
|
}
|
|
157
158
|
this.provider = provider;
|
|
@@ -195,11 +196,12 @@ class DatabaseManager extends BaseManager {
|
|
|
195
196
|
* administrators can sign in and reach /admin, and `reportAtBoot()` sends
|
|
196
197
|
* them the reason as a critical notification once notifications exist.
|
|
197
198
|
*/
|
|
198
|
-
markUnavailable(reason) {
|
|
199
|
+
markUnavailable(reason, plain, fix) {
|
|
199
200
|
this.unavailable = reason;
|
|
200
201
|
this.provider = null;
|
|
201
202
|
logger.error(`[DatabaseManager] ${reason}`);
|
|
202
|
-
|
|
203
|
+
// #2354: the dashboard says it in plain words; the developer text is the log line above.
|
|
204
|
+
this.markFailed(plain, 'ngdpbase.database.file', fix);
|
|
203
205
|
// #1906: no message of its own, so visitors see the site's maintenance message.
|
|
204
206
|
this.engine.maintenance.enter(reason);
|
|
205
207
|
}
|
|
@@ -690,7 +692,13 @@ class DatabaseManager extends BaseManager {
|
|
|
690
692
|
}
|
|
691
693
|
/** Degrade for one cause; the dashboard shows every cause still standing. */
|
|
692
694
|
fault(cause, reason, kind) {
|
|
693
|
-
|
|
695
|
+
// #2354: the dashboard shows the plain words; `reason` is the developer text, already logged by the caller.
|
|
696
|
+
const plain = cause === 'restore'
|
|
697
|
+
? 'A saved copy of the databases was not put back when the site started, so the databases that were there before are still in use. Open the Backup page to see what to do next.'
|
|
698
|
+
: this.backupKey()
|
|
699
|
+
? "The backup key is the same as the database key, so one stolen secret would open both. Give the backups a key of their own, in the server's settings, outside this page."
|
|
700
|
+
: "Databases are not being backed up, because the backup key is not set. This is set in the server's settings, outside this page.";
|
|
701
|
+
this.faults.set(cause, { reason, plain, kind });
|
|
694
702
|
this.markFaults();
|
|
695
703
|
}
|
|
696
704
|
clearFault(cause) {
|
|
@@ -704,11 +712,12 @@ class DatabaseManager extends BaseManager {
|
|
|
704
712
|
/** Every cause still standing; down when any one of them is (#1946). */
|
|
705
713
|
markFaults() {
|
|
706
714
|
const faults = [...this.faults.values()];
|
|
707
|
-
const reason = faults.map((f) => f.
|
|
715
|
+
const reason = faults.map((f) => f.plain).join(' ');
|
|
716
|
+
const fix = this.faults.has('restore') ? STAGED_RESTORE_FIX : {};
|
|
708
717
|
if (faults.some((f) => f.kind === 'outage'))
|
|
709
|
-
this.markDegraded(reason);
|
|
718
|
+
this.markDegraded(reason, undefined, fix);
|
|
710
719
|
else
|
|
711
|
-
this.markNeedsAttention(reason);
|
|
720
|
+
this.markNeedsAttention(reason, undefined, fix);
|
|
712
721
|
}
|
|
713
722
|
async notify(level, title, message) {
|
|
714
723
|
try {
|
|
@@ -35,7 +35,7 @@ import type { MailAttachment, MailMessage, MailProvider } from '../mail/MailProv
|
|
|
35
35
|
import type { ActorContext } from '../context/ActorContext.js';
|
|
36
36
|
export type { MailAttachment } from '../mail/MailProvider.js';
|
|
37
37
|
/** The degraded reason while sends fail; cleared by the next success. */
|
|
38
|
-
export declare const SEND_FAILING_REASON = "
|
|
38
|
+
export declare const SEND_FAILING_REASON = "The site can't send email right now. Check the email settings.";
|
|
39
39
|
/** Title of the administrator notice. */
|
|
40
40
|
export declare const SEND_FAILING_TITLE = "Email could not be sent";
|
|
41
41
|
/** An attachment the message cannot carry (#1974): refused before sending, so it is not a delivery failure. */
|
|
@@ -10,17 +10,7 @@ export type { PdfOptions, PdfMargin, PdfPageSize } from '../export/pdfEngine.js'
|
|
|
10
10
|
* when something asked for a PDF and could not have one; cleared by the next
|
|
11
11
|
* render that works.
|
|
12
12
|
*/
|
|
13
|
-
export declare const PDF_UNAVAILABLE_REASON = "
|
|
14
|
-
/**
|
|
15
|
-
* Export file metadata interface
|
|
16
|
-
*/
|
|
17
|
-
export interface ExportFileInfo {
|
|
18
|
-
filename: string;
|
|
19
|
-
path: string;
|
|
20
|
-
size: number;
|
|
21
|
-
created: Date;
|
|
22
|
-
modified: Date;
|
|
23
|
-
}
|
|
13
|
+
export declare const PDF_UNAVAILABLE_REASON = "PDFs can't be made right now. Choose how PDFs are made in the settings, or fix the one already chosen.";
|
|
24
14
|
/**
|
|
25
15
|
* Export configuration interface
|
|
26
16
|
*/
|
|
@@ -146,13 +136,6 @@ declare class ExportManager extends BaseManager {
|
|
|
146
136
|
* @returns {Promise<void>}
|
|
147
137
|
*/
|
|
148
138
|
initialize(config?: ExportConfig): Promise<void>;
|
|
149
|
-
/**
|
|
150
|
-
* Export a single page to HTML
|
|
151
|
-
* @param {string} pageName - Page name to export
|
|
152
|
-
* @param {ExportUser|null} user - User object for locale-aware formatting
|
|
153
|
-
* @returns {Promise<string>} HTML content
|
|
154
|
-
*/
|
|
155
|
-
exportPageToHtml(pageName: string, ctx: ActorContext, user?: ExportUser | null): Promise<string>;
|
|
156
139
|
/**
|
|
157
140
|
* One page as a complete HTML document: its content rendered, with the
|
|
158
141
|
* export's styling and metadata footer. It reads nothing: the caller has
|
|
@@ -171,32 +154,6 @@ declare class ExportManager extends BaseManager {
|
|
|
171
154
|
* @returns {Promise<string>} Combined HTML content
|
|
172
155
|
*/
|
|
173
156
|
exportPagesToHtml(pageNames: string[], ctx: ActorContext, user?: ExportUser | null): Promise<string>;
|
|
174
|
-
/**
|
|
175
|
-
* Export page(s) to markdown
|
|
176
|
-
* @param {string|string[]} pageNames - Single page name or array of page names
|
|
177
|
-
* @param {ExportUser|null} user - User object for locale-aware formatting
|
|
178
|
-
* @returns {Promise<string>} Markdown content
|
|
179
|
-
*/
|
|
180
|
-
exportToMarkdown(pageNames: string | string[], ctx: ActorContext, user?: ExportUser | null): Promise<string>;
|
|
181
|
-
/**
|
|
182
|
-
* Save export to file
|
|
183
|
-
* @param {string} content - Content to save
|
|
184
|
-
* @param {string} filename - Filename
|
|
185
|
-
* @param {string} format - File format
|
|
186
|
-
* @returns {Promise<string>} File path
|
|
187
|
-
*/
|
|
188
|
-
saveExport(content: string, filename: string, format: string): Promise<string>;
|
|
189
|
-
/**
|
|
190
|
-
* Get list of available exports
|
|
191
|
-
* @returns {Promise<ExportFileInfo[]>} List of export files
|
|
192
|
-
*/
|
|
193
|
-
getExports(): Promise<ExportFileInfo[]>;
|
|
194
|
-
/**
|
|
195
|
-
* Delete an export file
|
|
196
|
-
* @param {string} filename - Export filename
|
|
197
|
-
* @returns {Promise<void>}
|
|
198
|
-
*/
|
|
199
|
-
deleteExport(filename: string): Promise<void>;
|
|
200
157
|
/**
|
|
201
158
|
* Get formatted timestamp using user's locale
|
|
202
159
|
* @param {ExportUser|null} user - User object (optional)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import BaseManager from './BaseManager.js';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
|
-
import path from 'path';
|
|
4
3
|
import LocaleUtils from '../utils/LocaleUtils.js';
|
|
5
4
|
import logger from '../utils/logger.js';
|
|
6
5
|
import { actorOf } from '../context/ActorContext.js';
|
|
@@ -15,7 +14,7 @@ export { PdfUnavailableError };
|
|
|
15
14
|
* when something asked for a PDF and could not have one; cleared by the next
|
|
16
15
|
* render that works.
|
|
17
16
|
*/
|
|
18
|
-
export const PDF_UNAVAILABLE_REASON = '
|
|
17
|
+
export const PDF_UNAVAILABLE_REASON = "PDFs can't be made right now. Choose how PDFs are made in the settings, or fix the one already chosen.";
|
|
19
18
|
/** Shipped defaults of the `ngdpbase.export.pdf.*` keys. */
|
|
20
19
|
const PDF_DEFAULTS = { provider: 'none', pageSize: 'Letter', timeoutMs: 30_000, maxConcurrent: 2, serviceMaxBytes: 52_428_800 };
|
|
21
20
|
/**
|
|
@@ -34,7 +33,7 @@ const PDF_DEFAULTS = { provider: 'none', pageSize: 'Letter', timeoutMs: 30_000,
|
|
|
34
33
|
*
|
|
35
34
|
* @example
|
|
36
35
|
* const exportManager = engine.getManager('ExportManager');
|
|
37
|
-
* const
|
|
36
|
+
* const csv = exportManager.toCsv(['name'], [['Main']]);
|
|
38
37
|
*/
|
|
39
38
|
/** A text cell starting with one of these runs as a spreadsheet formula. */
|
|
40
39
|
const CSV_FORMULA_START = /^[=+\-@\t\r]/;
|
|
@@ -229,7 +228,7 @@ class ExportManager extends BaseManager {
|
|
|
229
228
|
this.exportDirectory = config.exportDirectory
|
|
230
229
|
|| this.engine.getManager('ConfigurationManager')?.getResolvedDataPath('ngdpbase.directories.exports', '${FAST_STORAGE}/exports')
|
|
231
230
|
|| './exports';
|
|
232
|
-
const preflight = this.preflightConfiguredPath('
|
|
231
|
+
const preflight = this.preflightConfiguredPath('ngdpbase.directories.exports', this.exportDirectory);
|
|
233
232
|
if (!preflight.ok) {
|
|
234
233
|
this.exportDirectory = '';
|
|
235
234
|
logger.info('ExportManager initialized (degraded — exports disabled)');
|
|
@@ -240,24 +239,6 @@ class ExportManager extends BaseManager {
|
|
|
240
239
|
const pdfProviderName = this.engine.getManager('ConfigurationManager')?.getProperty('ngdpbase.export.pdf.provider', PDF_DEFAULTS.provider);
|
|
241
240
|
logger.info(`ExportManager initialized (PDF provider: ${pdfProviderName ?? PDF_DEFAULTS.provider})`);
|
|
242
241
|
}
|
|
243
|
-
/**
|
|
244
|
-
* Export a single page to HTML
|
|
245
|
-
* @param {string} pageName - Page name to export
|
|
246
|
-
* @param {ExportUser|null} user - User object for locale-aware formatting
|
|
247
|
-
* @returns {Promise<string>} HTML content
|
|
248
|
-
*/
|
|
249
|
-
async exportPageToHtml(pageName, ctx, user = null) {
|
|
250
|
-
const pageManager = this.engine.getManager('PageManager');
|
|
251
|
-
const renderingManager = this.engine.getManager('RenderingManager');
|
|
252
|
-
if (!pageManager || !renderingManager) {
|
|
253
|
-
throw new Error('Required managers not available');
|
|
254
|
-
}
|
|
255
|
-
const page = (await pageManager.getPage(pageName, ctx));
|
|
256
|
-
if (!page) {
|
|
257
|
-
throw new Error(`Page '${pageName}' not found`);
|
|
258
|
-
}
|
|
259
|
-
return this.renderPageDocument(pageName, page, user);
|
|
260
|
-
}
|
|
261
242
|
/**
|
|
262
243
|
* One page as a complete HTML document: its content rendered, with the
|
|
263
244
|
* export's styling and metadata footer. It reads nothing: the caller has
|
|
@@ -480,99 +461,6 @@ class ExportManager extends BaseManager {
|
|
|
480
461
|
</html>`;
|
|
481
462
|
return html;
|
|
482
463
|
}
|
|
483
|
-
/**
|
|
484
|
-
* Export page(s) to markdown
|
|
485
|
-
* @param {string|string[]} pageNames - Single page name or array of page names
|
|
486
|
-
* @param {ExportUser|null} user - User object for locale-aware formatting
|
|
487
|
-
* @returns {Promise<string>} Markdown content
|
|
488
|
-
*/
|
|
489
|
-
async exportToMarkdown(pageNames, ctx, user = null) {
|
|
490
|
-
const pageManager = this.engine.getManager('PageManager');
|
|
491
|
-
if (!pageManager) {
|
|
492
|
-
throw new Error('PageManager not available');
|
|
493
|
-
}
|
|
494
|
-
const names = Array.isArray(pageNames) ? pageNames : [pageNames];
|
|
495
|
-
let markdown = '';
|
|
496
|
-
if (names.length > 1) {
|
|
497
|
-
markdown += '# ngdpbase Export\n\n';
|
|
498
|
-
markdown += `*Exported on ${this.getFormattedTimestamp(user)}*\n\n`;
|
|
499
|
-
markdown += '## Table of Contents\n\n';
|
|
500
|
-
for (const pageName of names) {
|
|
501
|
-
markdown += `- [${pageName}](#${pageName.toLowerCase().replace(/[^a-z0-9]/g, '-')})\n`;
|
|
502
|
-
}
|
|
503
|
-
markdown += '\n---\n\n';
|
|
504
|
-
}
|
|
505
|
-
for (const pageName of names) {
|
|
506
|
-
const page = (await pageManager.getPage(pageName, ctx));
|
|
507
|
-
if (page) {
|
|
508
|
-
if (names.length > 1) {
|
|
509
|
-
markdown += `# ${pageName}\n\n`;
|
|
510
|
-
}
|
|
511
|
-
markdown += page.content;
|
|
512
|
-
if (names.length > 1) {
|
|
513
|
-
markdown += '\n\n---\n\n';
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
return markdown;
|
|
518
|
-
}
|
|
519
|
-
/**
|
|
520
|
-
* Save export to file
|
|
521
|
-
* @param {string} content - Content to save
|
|
522
|
-
* @param {string} filename - Filename
|
|
523
|
-
* @param {string} format - File format
|
|
524
|
-
* @returns {Promise<string>} File path
|
|
525
|
-
*/
|
|
526
|
-
async saveExport(content, filename, format) {
|
|
527
|
-
const sanitizedFilename = filename.replace(/[^a-zA-Z0-9\-_]/g, '-');
|
|
528
|
-
const timestamp = new Date().toISOString().slice(0, 10);
|
|
529
|
-
const fullFilename = `${sanitizedFilename}_${timestamp}.${format}`;
|
|
530
|
-
const filePath = path.join(this.exportDirectory, fullFilename);
|
|
531
|
-
await fs.writeFile(filePath, content, 'utf8');
|
|
532
|
-
logger.info(`Exported to: ${fullFilename}`);
|
|
533
|
-
return filePath;
|
|
534
|
-
}
|
|
535
|
-
/**
|
|
536
|
-
* Get list of available exports
|
|
537
|
-
* @returns {Promise<ExportFileInfo[]>} List of export files
|
|
538
|
-
*/
|
|
539
|
-
async getExports() {
|
|
540
|
-
try {
|
|
541
|
-
const files = await fs.readdir(this.exportDirectory);
|
|
542
|
-
const exports = [];
|
|
543
|
-
for (const file of files) {
|
|
544
|
-
const filePath = path.join(this.exportDirectory, file);
|
|
545
|
-
const stats = await fs.stat(filePath);
|
|
546
|
-
exports.push({
|
|
547
|
-
filename: file,
|
|
548
|
-
path: filePath,
|
|
549
|
-
size: stats.size,
|
|
550
|
-
created: stats.birthtime,
|
|
551
|
-
modified: stats.mtime
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
return exports.sort((a, b) => b.created.getTime() - a.created.getTime());
|
|
555
|
-
}
|
|
556
|
-
catch (err) {
|
|
557
|
-
logger.error('Error getting exports:', err);
|
|
558
|
-
return [];
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Delete an export file
|
|
563
|
-
* @param {string} filename - Export filename
|
|
564
|
-
* @returns {Promise<void>}
|
|
565
|
-
*/
|
|
566
|
-
async deleteExport(filename) {
|
|
567
|
-
// Only a plain file name inside the export folder: never a path that
|
|
568
|
-
// reaches another folder.
|
|
569
|
-
if (!filename || filename !== path.basename(filename) || filename === '.' || filename === '..') {
|
|
570
|
-
throw new Error(`Not an export file name: ${filename}`);
|
|
571
|
-
}
|
|
572
|
-
const filePath = path.join(this.exportDirectory, filename);
|
|
573
|
-
await fs.unlink(filePath);
|
|
574
|
-
logger.info(`Deleted export: ${filename}`);
|
|
575
|
-
}
|
|
576
464
|
/**
|
|
577
465
|
* Get formatted timestamp using user's locale
|
|
578
466
|
* @param {ExportUser|null} user - User object (optional)
|
|
@@ -633,6 +633,13 @@ declare class ImportManager extends BaseManager {
|
|
|
633
633
|
* is permitted, and a refusal to someone who holds it is step-up's.
|
|
634
634
|
*/
|
|
635
635
|
private mayExportPage;
|
|
636
|
+
/**
|
|
637
|
+
* `core.search` as an export source: a search's pages and files as one zip
|
|
638
|
+
* ({@link searchExportSource}). Like a page, a search is offered to a user
|
|
639
|
+
* who holds `page-export`, and read as the user who asked, each page and
|
|
640
|
+
* file by its own read check. Recorded as one `page-export` of the search.
|
|
641
|
+
*/
|
|
642
|
+
private searchExportSource;
|
|
636
643
|
/**
|
|
637
644
|
* `core.pages` as an export source (#2120, D5: pages export checks
|
|
638
645
|
* `page-export`). A page is offered for export when its reader may see it
|
|
@@ -1226,6 +1226,15 @@ declare class PageManager extends BaseManager implements CatalogSource {
|
|
|
1226
1226
|
title: string;
|
|
1227
1227
|
uuid: string;
|
|
1228
1228
|
}>>;
|
|
1229
|
+
/**
|
|
1230
|
+
* Every public page stamped with one system-category (#2299), by the name
|
|
1231
|
+
* `mayShowPage` takes. Vault pages are not listed: they are their owners', not
|
|
1232
|
+
* an administrator's to be told about. Says nothing about who may see a page;
|
|
1233
|
+
* the caller asks `mayShowPage` as the reader.
|
|
1234
|
+
*
|
|
1235
|
+
* @param category - The category's key or label, any case
|
|
1236
|
+
*/
|
|
1237
|
+
listCategoryPages(category: string): Promise<string[]>;
|
|
1229
1238
|
/** The store ids the requester has of their own (#1387). */
|
|
1230
1239
|
listOwnStoreIds(ctx: ActorContext): Promise<string[]>;
|
|
1231
1240
|
/**
|